Merge branch 'develop' into refactor/types
This commit is contained in:
commit
1b143ebfaa
62 changed files with 2805 additions and 955 deletions
|
@ -13,8 +13,6 @@ redis:
|
||||||
host: firefish_redis
|
host: firefish_redis
|
||||||
port: 6379
|
port: 6379
|
||||||
|
|
||||||
id: 'aid'
|
|
||||||
|
|
||||||
#allowedPrivateNetworks: [
|
#allowedPrivateNetworks: [
|
||||||
# '10.69.1.0/24'
|
# '10.69.1.0/24'
|
||||||
#]
|
#]
|
||||||
|
|
1242
Cargo.lock
generated
1242
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
24
Cargo.toml
24
Cargo.toml
|
@ -5,36 +5,40 @@ resolver = "2"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
macro_rs = { path = "packages/macro-rs" }
|
macro_rs = { path = "packages/macro-rs" }
|
||||||
|
|
||||||
napi = { version = "2.16.2", default-features = false }
|
napi = { version = "2.16.4", default-features = false }
|
||||||
napi-derive = "2.16.2"
|
napi-derive = "2.16.3"
|
||||||
napi-build = "2.1.3"
|
napi-build = "2.1.3"
|
||||||
|
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
basen = "0.1.0"
|
basen = "0.1.0"
|
||||||
bcrypt = "0.15.1"
|
bcrypt = "0.15.1"
|
||||||
chrono = "0.4.37"
|
chrono = "0.4.38"
|
||||||
convert_case = "0.6.0"
|
convert_case = "0.6.0"
|
||||||
cuid2 = "0.1.2"
|
cuid2 = "0.1.2"
|
||||||
emojis = "0.6.1"
|
emojis = "0.6.2"
|
||||||
idna = "0.5.0"
|
idna = "0.5.0"
|
||||||
|
image = "0.25.1"
|
||||||
|
nom-exif = "1.2.0"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
openssl = "0.10.64"
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
proc-macro2 = "1.0.79"
|
proc-macro2 = "1.0.81"
|
||||||
quote = "1.0.36"
|
quote = "1.0.36"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
redis = "0.25.3"
|
redis = "0.25.3"
|
||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
|
reqwest = "0.12.4"
|
||||||
rmp-serde = "1.2.0"
|
rmp-serde = "1.2.0"
|
||||||
sea-orm = "0.12.15"
|
sea-orm = "0.12.15"
|
||||||
serde = "1.0.197"
|
serde = "1.0.198"
|
||||||
serde_json = "1.0.115"
|
serde_json = "1.0.116"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
strum = "0.26.2"
|
strum = "0.26.2"
|
||||||
syn = "2.0.58"
|
syn = "2.0.60"
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.59"
|
||||||
tokio = "1.37.0"
|
tokio = "1.37.0"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.1"
|
tracing-subscriber = "0.3.18"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ FROM docker.io/node:20-alpine as build
|
||||||
WORKDIR /firefish
|
WORKDIR /firefish
|
||||||
|
|
||||||
# Install compilation dependencies
|
# Install compilation dependencies
|
||||||
RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3
|
RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3 perl
|
||||||
RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
|
RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm install -
|
||||||
|
|
||||||
# Copy in the rest of the rust files
|
# Copy in the rest of the rust files
|
||||||
COPY packages/backend-rs packages/backend-rs/
|
COPY packages/backend-rs packages/backend-rs/
|
||||||
|
# COPY packages/macro-rs packages/macro-rs/
|
||||||
|
|
||||||
# Compile backend-rs
|
# Compile backend-rs
|
||||||
RUN NODE_ENV='production' pnpm run --filter backend-rs build
|
RUN NODE_ENV='production' pnpm run --filter backend-rs build
|
||||||
|
|
|
@ -5,6 +5,12 @@ Critical security updates are indicated by the :warning: icon.
|
||||||
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
|
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
|
||||||
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
|
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Add features to share links to an account in the three dots menu on the profile page
|
||||||
|
- Improve server logs
|
||||||
|
- Fix bugs
|
||||||
|
|
||||||
## [v20240424](https://firefish.dev/firefish/firefish/-/merge_requests/10765/commits)
|
## [v20240424](https://firefish.dev/firefish/firefish/-/merge_requests/10765/commits)
|
||||||
|
|
||||||
- Improve the usability of the feature to prevent forgetting to write alt texts
|
- Improve the usability of the feature to prevent forgetting to write alt texts
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
DELETE FROM "migrations" WHERE name IN (
|
DELETE FROM "migrations" WHERE name IN (
|
||||||
|
'AlterAkaType1714099399879',
|
||||||
'AddDriveFileUsage1713451569342',
|
'AddDriveFileUsage1713451569342',
|
||||||
'ConvertCwVarcharToText1713225866247',
|
'ConvertCwVarcharToText1713225866247',
|
||||||
'FixChatFileConstraint1712855579316',
|
'FixChatFileConstraint1712855579316',
|
||||||
|
@ -24,6 +25,13 @@ DELETE FROM "migrations" WHERE name IN (
|
||||||
'RemoveNativeUtilsMigration1705877093218'
|
'RemoveNativeUtilsMigration1705877093218'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- alter-aka-type
|
||||||
|
ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld";
|
||||||
|
ALTER TABLE "user" ADD COLUMN "alsoKnownAs" text;
|
||||||
|
UPDATE "user" SET "alsoKnownAs" = array_to_string("alsoKnownAsOld", ',');
|
||||||
|
COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too';
|
||||||
|
ALTER TABLE "user" DROP COLUMN "alsoKnownAsOld";
|
||||||
|
|
||||||
-- AddDriveFileUsage
|
-- AddDriveFileUsage
|
||||||
ALTER TABLE "drive_file" DROP COLUMN "usageHint";
|
ALTER TABLE "drive_file" DROP COLUMN "usageHint";
|
||||||
DROP TYPE "drive_file_usage_hint_enum";
|
DROP TYPE "drive_file_usage_hint_enum";
|
||||||
|
|
|
@ -19,7 +19,7 @@ deleteAndEditConfirm: Сигурни ли сте, че искате да изт
|
||||||
copyUsername: Копиране на потребителското име
|
copyUsername: Копиране на потребителското име
|
||||||
searchUser: Търсене на потребител
|
searchUser: Търсене на потребител
|
||||||
reply: Отговор
|
reply: Отговор
|
||||||
showMore: Покажи още
|
showMore: Показване на повече
|
||||||
loadMore: Зареди още
|
loadMore: Зареди още
|
||||||
followRequestAccepted: Заявката за последване е приета
|
followRequestAccepted: Заявката за последване е приета
|
||||||
importAndExport: Импорт/експорт на данни
|
importAndExport: Импорт/експорт на данни
|
||||||
|
@ -336,6 +336,10 @@ _pages:
|
||||||
title: Заглавие
|
title: Заглавие
|
||||||
my: Моите страници
|
my: Моите страници
|
||||||
pageSetting: Настройки на страницата
|
pageSetting: Настройки на страницата
|
||||||
|
url: Адрес на страницата
|
||||||
|
summary: Кратко обобщение
|
||||||
|
alignCenter: Центриране на елементите
|
||||||
|
variables: Променливи
|
||||||
_deck:
|
_deck:
|
||||||
_columns:
|
_columns:
|
||||||
notifications: Известия
|
notifications: Известия
|
||||||
|
@ -398,7 +402,7 @@ sendMessage: Изпращане на съобщение
|
||||||
jumpToPrevious: Премини към предишно
|
jumpToPrevious: Премини към предишно
|
||||||
newer: по-ново
|
newer: по-ново
|
||||||
older: по-старо
|
older: по-старо
|
||||||
showLess: Покажи по-малко
|
showLess: Показване на по-малко
|
||||||
youGotNewFollower: те последва
|
youGotNewFollower: те последва
|
||||||
receiveFollowRequest: Заявка за последване получена
|
receiveFollowRequest: Заявка за последване получена
|
||||||
mention: Споменаване
|
mention: Споменаване
|
||||||
|
@ -754,7 +758,7 @@ _feeds:
|
||||||
general: Общи
|
general: Общи
|
||||||
metadata: Метаданни
|
metadata: Метаданни
|
||||||
disk: Диск
|
disk: Диск
|
||||||
featured: Препоръчани
|
featured: Препоръчано
|
||||||
yearsOld: на {age} години
|
yearsOld: на {age} години
|
||||||
reload: Опресняване
|
reload: Опресняване
|
||||||
invites: Покани
|
invites: Покани
|
||||||
|
@ -940,3 +944,11 @@ showGapBetweenNotesInTimeline: Показване на празнина межд
|
||||||
lookup: Поглеждане
|
lookup: Поглеждане
|
||||||
media: Мултимедия
|
media: Мултимедия
|
||||||
welcomeBackWithName: Добре дошли отново, {name}
|
welcomeBackWithName: Добре дошли отново, {name}
|
||||||
|
reduceUiAnimation: Намаляване на UI анимациите
|
||||||
|
clickToFinishEmailVerification: Моля, натиснете [{ok}], за да завършите потвърждаването
|
||||||
|
на ел. поща.
|
||||||
|
_cw:
|
||||||
|
show: Показване на съдържанието
|
||||||
|
remoteFollow: Отдалечено последване
|
||||||
|
messagingUnencryptedInfo: Чатовете във Firefish не са шифровани от край до край. Не
|
||||||
|
споделяйте чувствителна информация през Firefish.
|
||||||
|
|
|
@ -1011,6 +1011,8 @@ isSystemAccount: "This account is created and automatically operated by the syst
|
||||||
Please do not moderate, edit, delete, or otherwise tamper with this account, or
|
Please do not moderate, edit, delete, or otherwise tamper with this account, or
|
||||||
it may break your server."
|
it may break your server."
|
||||||
typeToConfirm: "Please enter {x} to confirm"
|
typeToConfirm: "Please enter {x} to confirm"
|
||||||
|
useThisAccountConfirm: "Do you want to continue with this account?"
|
||||||
|
inputAccountId: "Please input your account (e.g., @firefish@info.firefish.dev)"
|
||||||
deleteAccount: "Delete account"
|
deleteAccount: "Delete account"
|
||||||
document: "Documentation"
|
document: "Documentation"
|
||||||
numberOfPageCache: "Number of cached pages"
|
numberOfPageCache: "Number of cached pages"
|
||||||
|
@ -1157,6 +1159,9 @@ addRe: "Add \"re:\" at the beginning of comment in reply to a post with a conten
|
||||||
confirm: "Confirm"
|
confirm: "Confirm"
|
||||||
importZip: "Import ZIP"
|
importZip: "Import ZIP"
|
||||||
exportZip: "Export ZIP"
|
exportZip: "Export ZIP"
|
||||||
|
getQrCode: "Show QR code"
|
||||||
|
remoteFollow: "Remote follow"
|
||||||
|
copyRemoteFollowUrl: "Copy remote follow URL"
|
||||||
emojiPackCreator: "Emoji pack creator"
|
emojiPackCreator: "Emoji pack creator"
|
||||||
indexable: "Indexable"
|
indexable: "Indexable"
|
||||||
indexableDescription: "Allow built-in search to show your public posts"
|
indexableDescription: "Allow built-in search to show your public posts"
|
||||||
|
|
|
@ -928,6 +928,8 @@ colored: "Coloré"
|
||||||
label: "Étiquette"
|
label: "Étiquette"
|
||||||
localOnly: "Local seulement"
|
localOnly: "Local seulement"
|
||||||
account: "Comptes"
|
account: "Comptes"
|
||||||
|
getQrCode: "Obtenir le code QR"
|
||||||
|
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "Adresse non disponible"
|
used: "Adresse non disponible"
|
||||||
format: "Le format de cette adresse de courriel est invalide"
|
format: "Le format de cette adresse de courriel est invalide"
|
||||||
|
|
|
@ -1825,6 +1825,7 @@ _notification:
|
||||||
reacted: mereaksi postinganmu
|
reacted: mereaksi postinganmu
|
||||||
renoted: memposting ulang postinganmu
|
renoted: memposting ulang postinganmu
|
||||||
voted: memilih di angketmu
|
voted: memilih di angketmu
|
||||||
|
andCountUsers: dan {count} lebih banyak pengguna {acted}
|
||||||
_deck:
|
_deck:
|
||||||
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
|
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
|
||||||
columnAlign: "Luruskan kolom"
|
columnAlign: "Luruskan kolom"
|
||||||
|
@ -2267,3 +2268,13 @@ markLocalFilesNsfwByDefaultDescription: Terlepas dari pengaturan ini, pengguna d
|
||||||
menghapus sendiri tanda NSFW. Berkas yang ada tidak berpengaruh.
|
menghapus sendiri tanda NSFW. Berkas yang ada tidak berpengaruh.
|
||||||
noteEditHistory: Riwayat penyuntingan kiriman
|
noteEditHistory: Riwayat penyuntingan kiriman
|
||||||
media: Media
|
media: Media
|
||||||
|
antennaLimit: Jumlah antena maksimum yang dapat dibuat oleh setiap pengguna
|
||||||
|
showAddFileDescriptionAtFirstPost: Buka formulir secara otomatis untuk menulis deskripsi
|
||||||
|
ketika mencoba mengirim berkas tanpa deskripsi
|
||||||
|
remoteFollow: Ikuti jarak jauh
|
||||||
|
foldNotification: Kelompokkan notifikasi yang sama
|
||||||
|
getQrCode: Tampilkan kode QR
|
||||||
|
cannotEditVisibility: Kamu tidak bisa menyunting keterlihatan
|
||||||
|
useThisAccountConfirm: Apakah kamu ingin melanjutkan dengan akun ini?
|
||||||
|
inputAccountId: Silakan memasukkan akunmu (misalnya, @firefish@info.firefish.dev)
|
||||||
|
copyRemoteFollowUrl: Salin URL ikuti jarak jauh
|
||||||
|
|
|
@ -1902,6 +1902,7 @@ _notification:
|
||||||
reacted: がリアクションしました
|
reacted: がリアクションしました
|
||||||
renoted: がブーストしました
|
renoted: がブーストしました
|
||||||
voted: が投票しました
|
voted: が投票しました
|
||||||
|
andCountUsers: と{count}人が{acted}しました
|
||||||
_deck:
|
_deck:
|
||||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||||
columnAlign: "カラムの寄せ"
|
columnAlign: "カラムの寄せ"
|
||||||
|
@ -2059,3 +2060,10 @@ markLocalFilesNsfwByDefaultDescription: この設定が有効でも、ユーザ
|
||||||
noteEditHistory: 編集履歴
|
noteEditHistory: 編集履歴
|
||||||
showAddFileDescriptionAtFirstPost: 説明の無い添付ファイルを投稿しようとした際に説明を書く画面を自動で開く
|
showAddFileDescriptionAtFirstPost: 説明の無い添付ファイルを投稿しようとした際に説明を書く画面を自動で開く
|
||||||
antennaLimit: 各ユーザーが作れるアンテナの最大数
|
antennaLimit: 各ユーザーが作れるアンテナの最大数
|
||||||
|
inputAccountId: 'あなたのアカウントを入力してください(例: @firefish@info.firefish.dev)'
|
||||||
|
remoteFollow: リモートフォロー
|
||||||
|
cannotEditVisibility: 公開範囲は変更できません
|
||||||
|
useThisAccountConfirm: このアカウントで操作を続けますか?
|
||||||
|
getQrCode: QRコードを表示
|
||||||
|
copyRemoteFollowUrl: リモートからフォローするURLをコピー
|
||||||
|
foldNotification: 同じ種類の通知をまとめて表示する
|
||||||
|
|
|
@ -879,6 +879,8 @@ driveCapOverrideCaption: "输入 0 或以下的值将容量重置为默认值。
|
||||||
requireAdminForView: "您需要使用管理员账号登录才能查看。"
|
requireAdminForView: "您需要使用管理员账号登录才能查看。"
|
||||||
isSystemAccount: "该账号由系统自动创建。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。"
|
isSystemAccount: "该账号由系统自动创建。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。"
|
||||||
typeToConfirm: "输入 {x} 以确认操作"
|
typeToConfirm: "输入 {x} 以确认操作"
|
||||||
|
useThisAccountConfirm: "您想使用此帐户继续执行此操作吗?"
|
||||||
|
inputAccountId: "请输入您的帐户(例如 @firefish@info.firefish.dev )"
|
||||||
deleteAccount: "删除账号"
|
deleteAccount: "删除账号"
|
||||||
document: "文档"
|
document: "文档"
|
||||||
numberOfPageCache: "缓存页数"
|
numberOfPageCache: "缓存页数"
|
||||||
|
@ -1974,6 +1976,9 @@ origin: 起源
|
||||||
confirm: 确认
|
confirm: 确认
|
||||||
importZip: 导入 ZIP
|
importZip: 导入 ZIP
|
||||||
exportZip: 导出 ZIP
|
exportZip: 导出 ZIP
|
||||||
|
getQrCode: "获取二维码"
|
||||||
|
remoteFollow: "远程关注"
|
||||||
|
copyRemoteFollowUrl: "复制远程关注 URL"
|
||||||
emojiPackCreator: 表情包创建工具
|
emojiPackCreator: 表情包创建工具
|
||||||
objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
|
objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
|
||||||
的端点 URL。
|
的端点 URL。
|
||||||
|
|
10
package.json
10
package.json
|
@ -45,11 +45,11 @@
|
||||||
"js-yaml": "4.1.0"
|
"js-yaml": "4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.6.4",
|
"@biomejs/biome": "1.7.1",
|
||||||
"@biomejs/cli-darwin-arm64": "^1.6.4",
|
"@biomejs/cli-darwin-arm64": "^1.7.1",
|
||||||
"@biomejs/cli-darwin-x64": "^1.6.4",
|
"@biomejs/cli-darwin-x64": "^1.7.1",
|
||||||
"@biomejs/cli-linux-arm64": "^1.6.4",
|
"@biomejs/cli-linux-arm64": "^1.7.1",
|
||||||
"@biomejs/cli-linux-x64": "^1.6.4",
|
"@biomejs/cli-linux-x64": "^1.7.1",
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.12.7",
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"pnpm": "8.15.7",
|
"pnpm": "8.15.7",
|
||||||
|
|
|
@ -24,10 +24,14 @@ chrono = { workspace = true }
|
||||||
cuid2 = { workspace = true }
|
cuid2 = { workspace = true }
|
||||||
emojis = { workspace = true }
|
emojis = { workspace = true }
|
||||||
idna = { workspace = true }
|
idna = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
|
nom-exif = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
openssl = { workspace = true, features = ["vendored"] }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
redis = { workspace = true }
|
redis = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
reqwest = { workspace = true, features = ["blocking"] }
|
||||||
rmp-serde = { workspace = true }
|
rmp-serde = { workspace = true }
|
||||||
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|
33
packages/backend-rs/index.d.ts
vendored
33
packages/backend-rs/index.d.ts
vendored
|
@ -248,6 +248,11 @@ export function sqlLikeEscape(src: string): string
|
||||||
export function safeForSql(src: string): boolean
|
export function safeForSql(src: string): boolean
|
||||||
/** Convert milliseconds to a human readable string */
|
/** Convert milliseconds to a human readable string */
|
||||||
export function formatMilliseconds(milliseconds: number): string
|
export function formatMilliseconds(milliseconds: number): string
|
||||||
|
export interface ImageSize {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
export function getImageSizeFromUrl(url: string): Promise<ImageSize>
|
||||||
/** TODO: handle name collisions better */
|
/** TODO: handle name collisions better */
|
||||||
export interface NoteLikeForGetNoteSummary {
|
export interface NoteLikeForGetNoteSummary {
|
||||||
fileIds: Array<string>
|
fileIds: Array<string>
|
||||||
|
@ -1012,10 +1017,10 @@ export interface User {
|
||||||
isDeleted: boolean
|
isDeleted: boolean
|
||||||
driveCapacityOverrideMb: number | null
|
driveCapacityOverrideMb: number | null
|
||||||
movedToUri: string | null
|
movedToUri: string | null
|
||||||
alsoKnownAs: string | null
|
|
||||||
speakAsCat: boolean
|
speakAsCat: boolean
|
||||||
emojiModPerm: UserEmojimodpermEnum
|
emojiModPerm: UserEmojimodpermEnum
|
||||||
isIndexable: boolean
|
isIndexable: boolean
|
||||||
|
alsoKnownAs: Array<string> | null
|
||||||
}
|
}
|
||||||
export interface UserGroup {
|
export interface UserGroup {
|
||||||
id: string
|
id: string
|
||||||
|
@ -1144,6 +1149,7 @@ export interface Webhook {
|
||||||
export function initializeRustLogger(): void
|
export function initializeRustLogger(): void
|
||||||
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
|
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
|
||||||
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
|
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
|
||||||
|
export function publishToChannelStream(channelId: string, userId: string): void
|
||||||
export enum ChatEvent {
|
export enum ChatEvent {
|
||||||
Message = 'message',
|
Message = 'message',
|
||||||
Read = 'read',
|
Read = 'read',
|
||||||
|
@ -1151,6 +1157,31 @@ export enum ChatEvent {
|
||||||
Typing = 'typing'
|
Typing = 'typing'
|
||||||
}
|
}
|
||||||
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
|
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
|
||||||
|
export enum ChatIndexEvent {
|
||||||
|
Message = 'message',
|
||||||
|
Read = 'read'
|
||||||
|
}
|
||||||
|
export function publishToChatIndexStream(userId: string, kind: ChatIndexEvent, object: any): void
|
||||||
|
export interface PackedEmoji {
|
||||||
|
id: string
|
||||||
|
aliases: Array<string>
|
||||||
|
name: string
|
||||||
|
category: string | null
|
||||||
|
host: string | null
|
||||||
|
url: string
|
||||||
|
license: string | null
|
||||||
|
width: number | null
|
||||||
|
height: number | null
|
||||||
|
}
|
||||||
|
export function publishToBroadcastStream(emoji: PackedEmoji): void
|
||||||
|
export function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): void
|
||||||
|
export interface AbuseUserReportLike {
|
||||||
|
id: string
|
||||||
|
targetUserId: string
|
||||||
|
reporterId: string
|
||||||
|
comment: string
|
||||||
|
}
|
||||||
|
export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): void
|
||||||
export function getTimestamp(id: string): number
|
export function getTimestamp(id: string): number
|
||||||
/**
|
/**
|
||||||
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, watchNote, unwatchNote, ChatEvent, publishToChatStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||||
|
|
||||||
module.exports.SECOND = SECOND
|
module.exports.SECOND = SECOND
|
||||||
module.exports.MINUTE = MINUTE
|
module.exports.MINUTE = MINUTE
|
||||||
|
@ -337,6 +337,7 @@ module.exports.isUnicodeEmoji = isUnicodeEmoji
|
||||||
module.exports.sqlLikeEscape = sqlLikeEscape
|
module.exports.sqlLikeEscape = sqlLikeEscape
|
||||||
module.exports.safeForSql = safeForSql
|
module.exports.safeForSql = safeForSql
|
||||||
module.exports.formatMilliseconds = formatMilliseconds
|
module.exports.formatMilliseconds = formatMilliseconds
|
||||||
|
module.exports.getImageSizeFromUrl = getImageSizeFromUrl
|
||||||
module.exports.getNoteSummary = getNoteSummary
|
module.exports.getNoteSummary = getNoteSummary
|
||||||
module.exports.toMastodonId = toMastodonId
|
module.exports.toMastodonId = toMastodonId
|
||||||
module.exports.fromMastodonId = fromMastodonId
|
module.exports.fromMastodonId = fromMastodonId
|
||||||
|
@ -364,8 +365,14 @@ module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotific
|
||||||
module.exports.initializeRustLogger = initializeRustLogger
|
module.exports.initializeRustLogger = initializeRustLogger
|
||||||
module.exports.watchNote = watchNote
|
module.exports.watchNote = watchNote
|
||||||
module.exports.unwatchNote = unwatchNote
|
module.exports.unwatchNote = unwatchNote
|
||||||
|
module.exports.publishToChannelStream = publishToChannelStream
|
||||||
module.exports.ChatEvent = ChatEvent
|
module.exports.ChatEvent = ChatEvent
|
||||||
module.exports.publishToChatStream = publishToChatStream
|
module.exports.publishToChatStream = publishToChatStream
|
||||||
|
module.exports.ChatIndexEvent = ChatIndexEvent
|
||||||
|
module.exports.publishToChatIndexStream = publishToChatIndexStream
|
||||||
|
module.exports.publishToBroadcastStream = publishToBroadcastStream
|
||||||
|
module.exports.publishToGroupChatStream = publishToGroupChatStream
|
||||||
|
module.exports.publishToModerationStream = publishToModerationStream
|
||||||
module.exports.getTimestamp = getTimestamp
|
module.exports.getTimestamp = getTimestamp
|
||||||
module.exports.genId = genId
|
module.exports.genId = genId
|
||||||
module.exports.genIdAt = genIdAt
|
module.exports.genIdAt = genIdAt
|
||||||
|
|
200
packages/backend-rs/src/misc/get_image_size.rs
Normal file
200
packages/backend-rs/src/misc/get_image_size.rs
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
use crate::misc::redis_cache::{get_cache, set_cache, CacheError};
|
||||||
|
use crate::util::http_client;
|
||||||
|
use image::{io::Reader, ImageError, ImageFormat};
|
||||||
|
use nom_exif::{parse_jpeg_exif, EntryValue, ExifTag};
|
||||||
|
use std::io::Cursor;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Redis cache error: {0}")]
|
||||||
|
CacheErr(#[from] CacheError),
|
||||||
|
#[error("Reqwest error: {0}")]
|
||||||
|
ReqwestErr(#[from] reqwest::Error),
|
||||||
|
#[error("Image decoding error: {0}")]
|
||||||
|
ImageErr(#[from] ImageError),
|
||||||
|
#[error("Image decoding error: {0}")]
|
||||||
|
IoErr(#[from] std::io::Error),
|
||||||
|
#[error("Exif extraction error: {0}")]
|
||||||
|
ExifErr(#[from] nom_exif::Error),
|
||||||
|
#[error("Emoji meta attempt limit exceeded: {0}")]
|
||||||
|
TooManyAttempts(String),
|
||||||
|
#[error("Unsupported image type: {0}")]
|
||||||
|
UnsupportedImageErr(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
const BROWSER_SAFE_IMAGE_TYPES: [ImageFormat; 8] = [
|
||||||
|
ImageFormat::Png,
|
||||||
|
ImageFormat::Jpeg,
|
||||||
|
ImageFormat::Gif,
|
||||||
|
ImageFormat::WebP,
|
||||||
|
ImageFormat::Tiff,
|
||||||
|
ImageFormat::Bmp,
|
||||||
|
ImageFormat::Ico,
|
||||||
|
ImageFormat::Avif,
|
||||||
|
];
|
||||||
|
|
||||||
|
static MTX_GUARD: Mutex<()> = Mutex::const_new(());
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
#[crate::export(object)]
|
||||||
|
pub struct ImageSize {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[crate::export]
|
||||||
|
pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
|
||||||
|
let attempted: bool;
|
||||||
|
|
||||||
|
{
|
||||||
|
let _ = MTX_GUARD.lock().await;
|
||||||
|
|
||||||
|
let key = format!("fetchImage:{}", url);
|
||||||
|
attempted = get_cache::<bool>(&key)?.is_some();
|
||||||
|
|
||||||
|
if !attempted {
|
||||||
|
set_cache(&key, &true, 10 * 60)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempted {
|
||||||
|
tracing::warn!("attempt limit exceeded: {}", url);
|
||||||
|
return Err(Error::TooManyAttempts(url.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("retrieving image size from {}", url);
|
||||||
|
|
||||||
|
let image_bytes = http_client()?.get(url).send().await?.bytes().await?;
|
||||||
|
let reader = Reader::new(Cursor::new(&image_bytes)).with_guessed_format()?;
|
||||||
|
|
||||||
|
let format = reader.format();
|
||||||
|
if format.is_none() || !BROWSER_SAFE_IMAGE_TYPES.contains(&format.unwrap()) {
|
||||||
|
return Err(Error::UnsupportedImageErr(format!("{:?}", format)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = reader.into_dimensions()?;
|
||||||
|
|
||||||
|
let res = ImageSize {
|
||||||
|
width: size.0,
|
||||||
|
height: size.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if format.unwrap() != ImageFormat::Jpeg {
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle jpeg orientation
|
||||||
|
// https://magnushoff.com/articles/jpeg-orientation/
|
||||||
|
|
||||||
|
let exif = parse_jpeg_exif(&*image_bytes)?;
|
||||||
|
if exif.is_none() {
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let orientation = exif.unwrap().get_value(&ExifTag::Orientation)?;
|
||||||
|
let rotated =
|
||||||
|
orientation.is_some() && matches!(orientation.unwrap(), EntryValue::U32(v) if v >= 5);
|
||||||
|
|
||||||
|
if !rotated {
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ImageSize {
|
||||||
|
width: size.1,
|
||||||
|
height: size.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod unit_test {
|
||||||
|
use super::{get_image_size_from_url, ImageSize};
|
||||||
|
use crate::misc::redis_cache::delete_cache;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_image_size() {
|
||||||
|
let png_url_1 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png";
|
||||||
|
let png_url_2 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/notification-badges/at.png";
|
||||||
|
let png_url_3 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/api-doc.png";
|
||||||
|
let rotated_jpeg_url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/test/resources/rotate.jpg";
|
||||||
|
let webp_url_1 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/custom/assets/badges/error.webp";
|
||||||
|
let webp_url_2 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/screenshots/1.webp";
|
||||||
|
let ico_url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/favicon.ico";
|
||||||
|
let gif_url = "https://firefish.dev/firefish/firefish/-/raw/b9c3dfbd3d473cb2cee20c467eeae780bc401271/packages/backend/test/resources/anime.gif";
|
||||||
|
let mp3_url = "https://firefish.dev/firefish/firefish/-/blob/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/sounds/aisha/1.mp3";
|
||||||
|
|
||||||
|
// Delete caches in case you run this test multiple times
|
||||||
|
// (should be disabled in CI tasks)
|
||||||
|
delete_cache(&format!("fetchImage:{}", png_url_1)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", png_url_2)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", png_url_3)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", rotated_jpeg_url)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", webp_url_1)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", webp_url_2)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", ico_url)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", gif_url)).unwrap();
|
||||||
|
delete_cache(&format!("fetchImage:{}", mp3_url)).unwrap();
|
||||||
|
|
||||||
|
let png_size_1 = ImageSize {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
};
|
||||||
|
let png_size_2 = ImageSize {
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
};
|
||||||
|
let png_size_3 = ImageSize {
|
||||||
|
width: 1024,
|
||||||
|
height: 354,
|
||||||
|
};
|
||||||
|
let rotated_jpeg_size = ImageSize {
|
||||||
|
width: 256,
|
||||||
|
height: 512,
|
||||||
|
};
|
||||||
|
let webp_size_1 = ImageSize {
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
};
|
||||||
|
let webp_size_2 = ImageSize {
|
||||||
|
width: 1080,
|
||||||
|
height: 2340,
|
||||||
|
};
|
||||||
|
let ico_size = ImageSize {
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
};
|
||||||
|
let gif_size = ImageSize {
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
png_size_1,
|
||||||
|
get_image_size_from_url(png_url_1).await.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
png_size_2,
|
||||||
|
get_image_size_from_url(png_url_2).await.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
png_size_3,
|
||||||
|
get_image_size_from_url(png_url_3).await.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rotated_jpeg_size,
|
||||||
|
get_image_size_from_url(rotated_jpeg_url).await.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
webp_size_1,
|
||||||
|
get_image_size_from_url(webp_url_1).await.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
webp_size_2,
|
||||||
|
get_image_size_from_url(webp_url_2).await.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(ico_size, get_image_size_from_url(ico_url).await.unwrap());
|
||||||
|
assert_eq!(gif_size, get_image_size_from_url(gif_url).await.unwrap());
|
||||||
|
assert!(get_image_size_from_url(mp3_url).await.is_err());
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ pub mod convert_host;
|
||||||
pub mod emoji;
|
pub mod emoji;
|
||||||
pub mod escape_sql;
|
pub mod escape_sql;
|
||||||
pub mod format_milliseconds;
|
pub mod format_milliseconds;
|
||||||
|
pub mod get_image_size;
|
||||||
pub mod get_note_summary;
|
pub mod get_note_summary;
|
||||||
pub mod mastodon_id;
|
pub mod mastodon_id;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use redis::{Commands, RedisError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum CacheError {
|
||||||
#[error("Redis error: {0}")]
|
#[error("Redis error: {0}")]
|
||||||
RedisError(#[from] RedisError),
|
RedisError(#[from] RedisError),
|
||||||
#[error("Data serialization error: {0}")]
|
#[error("Data serialization error: {0}")]
|
||||||
|
@ -12,27 +12,37 @@ pub enum Error {
|
||||||
DeserializeError(#[from] rmp_serde::decode::Error),
|
DeserializeError(#[from] rmp_serde::decode::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prefix_key(key: &str) -> String {
|
||||||
|
redis_key(format!("cache:{}", key))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_cache<V: for<'a> Deserialize<'a> + Serialize>(
|
pub fn set_cache<V: for<'a> Deserialize<'a> + Serialize>(
|
||||||
key: &str,
|
key: &str,
|
||||||
value: &V,
|
value: &V,
|
||||||
expire_seconds: u64,
|
expire_seconds: u64,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), CacheError> {
|
||||||
redis_conn()?.set_ex(
|
redis_conn()?.set_ex(
|
||||||
redis_key(key),
|
prefix_key(key),
|
||||||
rmp_serde::encode::to_vec(&value)?,
|
rmp_serde::encode::to_vec(&value)?,
|
||||||
expire_seconds,
|
expire_seconds,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cache<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
|
pub fn get_cache<V: for<'a> Deserialize<'a> + Serialize>(
|
||||||
let serialized_value: Option<Vec<u8>> = redis_conn()?.get(redis_key(key))?;
|
key: &str,
|
||||||
|
) -> Result<Option<V>, CacheError> {
|
||||||
|
let serialized_value: Option<Vec<u8>> = redis_conn()?.get(prefix_key(key))?;
|
||||||
Ok(match serialized_value {
|
Ok(match serialized_value {
|
||||||
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
|
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
|
||||||
None => None,
|
None => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_cache(key: &str) -> Result<(), CacheError> {
|
||||||
|
Ok(redis_conn()?.del(prefix_key(key))?)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod unit_test {
|
mod unit_test {
|
||||||
use super::{get_cache, set_cache};
|
use super::{get_cache, set_cache};
|
||||||
|
|
|
@ -71,14 +71,14 @@ pub struct Model {
|
||||||
pub drive_capacity_override_mb: Option<i32>,
|
pub drive_capacity_override_mb: Option<i32>,
|
||||||
#[sea_orm(column_name = "movedToUri")]
|
#[sea_orm(column_name = "movedToUri")]
|
||||||
pub moved_to_uri: Option<String>,
|
pub moved_to_uri: Option<String>,
|
||||||
#[sea_orm(column_name = "alsoKnownAs", column_type = "Text", nullable)]
|
|
||||||
pub also_known_as: Option<String>,
|
|
||||||
#[sea_orm(column_name = "speakAsCat")]
|
#[sea_orm(column_name = "speakAsCat")]
|
||||||
pub speak_as_cat: bool,
|
pub speak_as_cat: bool,
|
||||||
#[sea_orm(column_name = "emojiModPerm")]
|
#[sea_orm(column_name = "emojiModPerm")]
|
||||||
pub emoji_mod_perm: UserEmojimodpermEnum,
|
pub emoji_mod_perm: UserEmojimodpermEnum,
|
||||||
#[sea_orm(column_name = "isIndexable")]
|
#[sea_orm(column_name = "isIndexable")]
|
||||||
pub is_indexable: bool,
|
pub is_indexable: bool,
|
||||||
|
#[sea_orm(column_name = "alsoKnownAs")]
|
||||||
|
pub also_known_as: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub fn initialize_logger() {
|
||||||
"info" => Level::INFO,
|
"info" => Level::INFO,
|
||||||
"debug" => Level::DEBUG,
|
"debug" => Level::DEBUG,
|
||||||
"trace" => Level::TRACE,
|
"trace" => Level::TRACE,
|
||||||
_ => Level::INFO,
|
_ => Level::INFO, // Fallback
|
||||||
});
|
});
|
||||||
} else if let Some(levels) = &CONFIG.log_level {
|
} else if let Some(levels) = &CONFIG.log_level {
|
||||||
// `logLevel` config is Deprecated
|
// `logLevel` config is Deprecated
|
||||||
|
@ -27,13 +27,25 @@ pub fn initialize_logger() {
|
||||||
builder = builder.with_max_level(Level::WARN);
|
builder = builder.with_max_level(Level::WARN);
|
||||||
} else if levels.contains(&"error".to_string()) {
|
} else if levels.contains(&"error".to_string()) {
|
||||||
builder = builder.with_max_level(Level::ERROR);
|
builder = builder.with_max_level(Level::ERROR);
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
builder = builder.with_max_level(Level::INFO);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback
|
// Fallback
|
||||||
builder = builder.with_max_level(Level::INFO);
|
builder = builder.with_max_level(Level::INFO);
|
||||||
};
|
};
|
||||||
|
|
||||||
let subscriber = builder.with_level(true).pretty().finish();
|
let subscriber = builder
|
||||||
|
.without_time()
|
||||||
|
.with_level(true)
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_target(true)
|
||||||
|
.with_thread_names(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.log_internal_errors(true)
|
||||||
|
.compact()
|
||||||
|
.finish();
|
||||||
|
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("Failed to initialize the logger");
|
tracing::subscriber::set_global_default(subscriber).expect("Failed to initialize the logger");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
pub mod antenna;
|
pub mod antenna;
|
||||||
|
pub mod channel;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
|
pub mod chat_index;
|
||||||
|
pub mod custom_emoji;
|
||||||
|
pub mod group_chat;
|
||||||
|
pub mod moderation;
|
||||||
|
|
||||||
use crate::config::CONFIG;
|
use crate::config::CONFIG;
|
||||||
use crate::database::redis_conn;
|
use crate::database::redis_conn;
|
||||||
|
@ -10,9 +15,9 @@ pub enum Stream {
|
||||||
#[strum(serialize = "internal")]
|
#[strum(serialize = "internal")]
|
||||||
Internal,
|
Internal,
|
||||||
#[strum(serialize = "broadcast")]
|
#[strum(serialize = "broadcast")]
|
||||||
Broadcast,
|
CustomEmoji,
|
||||||
#[strum(to_string = "adminStream:{user_id}")]
|
#[strum(to_string = "adminStream:{moderator_id}")]
|
||||||
Admin { user_id: String },
|
Moderation { moderator_id: String },
|
||||||
#[strum(to_string = "user:{user_id}")]
|
#[strum(to_string = "user:{user_id}")]
|
||||||
User { user_id: String },
|
User { user_id: String },
|
||||||
#[strum(to_string = "channelStream:{channel_id}")]
|
#[strum(to_string = "channelStream:{channel_id}")]
|
||||||
|
@ -37,7 +42,7 @@ pub enum Stream {
|
||||||
#[strum(to_string = "messagingStream:{group_id}")]
|
#[strum(to_string = "messagingStream:{group_id}")]
|
||||||
GroupChat { group_id: String },
|
GroupChat { group_id: String },
|
||||||
#[strum(to_string = "messagingIndexStream:{user_id}")]
|
#[strum(to_string = "messagingIndexStream:{user_id}")]
|
||||||
MessagingIndex { user_id: String },
|
ChatIndex { user_id: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -57,7 +62,7 @@ pub fn publish_to_stream(
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let message = if let Some(kind) = kind {
|
let message = if let Some(kind) = kind {
|
||||||
format!(
|
format!(
|
||||||
"{{ \"type\": \"{}\", \"body\": {} }}",
|
"{{\"type\":\"{}\",\"body\":{}}}",
|
||||||
kind,
|
kind,
|
||||||
value.unwrap_or("null".to_string()),
|
value.unwrap_or("null".to_string()),
|
||||||
)
|
)
|
||||||
|
@ -67,10 +72,7 @@ pub fn publish_to_stream(
|
||||||
|
|
||||||
redis_conn()?.publish(
|
redis_conn()?.publish(
|
||||||
&CONFIG.host,
|
&CONFIG.host,
|
||||||
format!(
|
format!("{{\"channel\":\"{}\",\"message\":{}}}", stream, message),
|
||||||
"{{ \"channel\": \"{}\", \"message\": {} }}",
|
|
||||||
stream, message,
|
|
||||||
),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -84,10 +86,10 @@ mod unit_test {
|
||||||
#[test]
|
#[test]
|
||||||
fn channel_to_string() {
|
fn channel_to_string() {
|
||||||
assert_eq!(Stream::Internal.to_string(), "internal");
|
assert_eq!(Stream::Internal.to_string(), "internal");
|
||||||
assert_eq!(Stream::Broadcast.to_string(), "broadcast");
|
assert_eq!(Stream::CustomEmoji.to_string(), "broadcast");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Stream::Admin {
|
Stream::Moderation {
|
||||||
user_id: "9tb42br63g5apjcq".to_string()
|
moderator_id: "9tb42br63g5apjcq".to_string()
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
"adminStream:9tb42br63g5apjcq"
|
"adminStream:9tb42br63g5apjcq"
|
||||||
|
|
10
packages/backend-rs/src/service/stream/channel.rs
Normal file
10
packages/backend-rs/src/service/stream/channel.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||||
|
|
||||||
|
#[crate::export(js_name = "publishToChannelStream")]
|
||||||
|
pub fn publish(channel_id: String, user_id: String) -> Result<(), Error> {
|
||||||
|
publish_to_stream(
|
||||||
|
&Stream::Channel { channel_id },
|
||||||
|
Some("typing".to_string()),
|
||||||
|
Some(format!("\"{}\"", user_id)),
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,12 +13,15 @@ pub enum ChatEvent {
|
||||||
Typing,
|
Typing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want to merge `kind` and `object` into a single enum
|
||||||
|
// https://github.com/napi-rs/napi-rs/issues/2036
|
||||||
|
|
||||||
#[crate::export(js_name = "publishToChatStream")]
|
#[crate::export(js_name = "publishToChatStream")]
|
||||||
pub fn publish(
|
pub fn publish(
|
||||||
sender_user_id: String,
|
sender_user_id: String,
|
||||||
receiver_user_id: String,
|
receiver_user_id: String,
|
||||||
kind: ChatEvent,
|
kind: ChatEvent,
|
||||||
object: &serde_json::Value, // TODO?: change this to enum
|
object: &serde_json::Value,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
publish_to_stream(
|
publish_to_stream(
|
||||||
&Stream::Chat {
|
&Stream::Chat {
|
||||||
|
|
26
packages/backend-rs/src/service/stream/chat_index.rs
Normal file
26
packages/backend-rs/src/service/stream/chat_index.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||||
|
|
||||||
|
#[derive(strum::Display)]
|
||||||
|
#[crate::export(string_enum = "camelCase")]
|
||||||
|
pub enum ChatIndexEvent {
|
||||||
|
#[strum(serialize = "message")]
|
||||||
|
Message,
|
||||||
|
#[strum(serialize = "read")]
|
||||||
|
Read,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to merge `kind` and `object` into a single enum
|
||||||
|
// https://github.com/napi-rs/napi-rs/issues/2036
|
||||||
|
|
||||||
|
#[crate::export(js_name = "publishToChatIndexStream")]
|
||||||
|
pub fn publish(
|
||||||
|
user_id: String,
|
||||||
|
kind: ChatIndexEvent,
|
||||||
|
object: &serde_json::Value,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
publish_to_stream(
|
||||||
|
&Stream::ChatIndex { user_id },
|
||||||
|
Some(kind.to_string()),
|
||||||
|
Some(serde_json::to_string(object)?),
|
||||||
|
)
|
||||||
|
}
|
27
packages/backend-rs/src/service/stream/custom_emoji.rs
Normal file
27
packages/backend-rs/src/service/stream/custom_emoji.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// TODO: define schema type in other place
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[crate::export(object)]
|
||||||
|
pub struct PackedEmoji {
|
||||||
|
pub id: String,
|
||||||
|
pub aliases: Vec<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub category: Option<String>,
|
||||||
|
pub host: Option<String>,
|
||||||
|
pub url: String,
|
||||||
|
pub license: Option<String>,
|
||||||
|
pub width: Option<i32>,
|
||||||
|
pub height: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[crate::export(js_name = "publishToBroadcastStream")]
|
||||||
|
pub fn publish(emoji: &PackedEmoji) -> Result<(), Error> {
|
||||||
|
publish_to_stream(
|
||||||
|
&Stream::CustomEmoji,
|
||||||
|
Some("emojiAdded".to_string()),
|
||||||
|
Some(format!("{{\"emoji\":{}}}", serde_json::to_string(emoji)?)),
|
||||||
|
)
|
||||||
|
}
|
13
packages/backend-rs/src/service/stream/group_chat.rs
Normal file
13
packages/backend-rs/src/service/stream/group_chat.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::service::stream::{chat::ChatEvent, publish_to_stream, Error, Stream};
|
||||||
|
|
||||||
|
// We want to merge `kind` and `object` into a single enum
|
||||||
|
// https://github.com/napi-rs/napi-rs/issues/2036
|
||||||
|
|
||||||
|
#[crate::export(js_name = "publishToGroupChatStream")]
|
||||||
|
pub fn publish(group_id: String, kind: ChatEvent, object: &serde_json::Value) -> Result<(), Error> {
|
||||||
|
publish_to_stream(
|
||||||
|
&Stream::GroupChat { group_id },
|
||||||
|
Some(kind.to_string()),
|
||||||
|
Some(serde_json::to_string(object)?),
|
||||||
|
)
|
||||||
|
}
|
21
packages/backend-rs/src/service/stream/moderation.rs
Normal file
21
packages/backend-rs/src/service/stream/moderation.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[crate::export(object)]
|
||||||
|
pub struct AbuseUserReportLike {
|
||||||
|
pub id: String,
|
||||||
|
pub target_user_id: String,
|
||||||
|
pub reporter_id: String,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[crate::export(js_name = "publishToModerationStream")]
|
||||||
|
pub fn publish(moderator_id: String, report: &AbuseUserReportLike) -> Result<(), Error> {
|
||||||
|
publish_to_stream(
|
||||||
|
&Stream::Moderation { moderator_id },
|
||||||
|
Some("newAbuseUserReport".to_string()),
|
||||||
|
Some(serde_json::to_string(report)?),
|
||||||
|
)
|
||||||
|
}
|
24
packages/backend-rs/src/util/http_client.rs
Normal file
24
packages/backend-rs/src/util/http_client.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use crate::config::CONFIG;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use reqwest::{Client, Error, NoProxy, Proxy};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
static CLIENT: OnceCell<Client> = OnceCell::new();
|
||||||
|
|
||||||
|
pub fn http_client() -> Result<Client, Error> {
|
||||||
|
CLIENT
|
||||||
|
.get_or_try_init(|| {
|
||||||
|
let mut builder = Client::builder().timeout(Duration::from_secs(5));
|
||||||
|
|
||||||
|
if let Some(proxy_url) = &CONFIG.proxy {
|
||||||
|
let mut proxy = Proxy::all(proxy_url)?;
|
||||||
|
if let Some(proxy_bypass_hosts) = &CONFIG.proxy_bypass_hosts {
|
||||||
|
proxy = proxy.no_proxy(NoProxy::from_string(&proxy_bypass_hosts.join(",")));
|
||||||
|
}
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
}
|
|
@ -1,2 +1,5 @@
|
||||||
|
pub use http_client::http_client;
|
||||||
|
|
||||||
|
pub mod http_client;
|
||||||
pub mod id;
|
pub mod id;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
|
|
|
@ -22,21 +22,21 @@
|
||||||
"@swc/core-android-arm64": "1.3.11"
|
"@swc/core-android-arm64": "1.3.11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "5.15.5",
|
"@bull-board/api": "5.16.0",
|
||||||
"@bull-board/koa": "5.15.5",
|
"@bull-board/koa": "5.16.0",
|
||||||
"@bull-board/ui": "5.15.5",
|
"@bull-board/ui": "5.16.0",
|
||||||
"@discordapp/twemoji": "^15.0.3",
|
"@discordapp/twemoji": "^15.0.3",
|
||||||
"@koa/cors": "5.0.0",
|
"@koa/cors": "5.0.0",
|
||||||
"@koa/multer": "3.0.2",
|
"@koa/multer": "3.0.2",
|
||||||
"@koa/router": "12.0.1",
|
"@koa/router": "12.0.1",
|
||||||
"@ladjs/koa-views": "9.0.0",
|
"@ladjs/koa-views": "9.0.0",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@redocly/openapi-core": "1.11.0",
|
"@redocly/openapi-core": "1.12.0",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"adm-zip": "0.5.10",
|
"adm-zip": "0.5.10",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"aws-sdk": "2.1599.0",
|
"aws-sdk": "2.1608.0",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"backend-rs": "workspace:*",
|
"backend-rs": "workspace:*",
|
||||||
"firefish-js": "workspace:*",
|
"firefish-js": "workspace:*",
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
"gunzip-maybe": "^1.4.2",
|
"gunzip-maybe": "^1.4.2",
|
||||||
"happy-dom": "^14.7.1",
|
"happy-dom": "^14.7.1",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.4.1",
|
||||||
"ip-cidr": "4.0.0",
|
"ip-cidr": "4.0.0",
|
||||||
"is-svg": "5.0.0",
|
"is-svg": "5.0.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
|
@ -126,7 +126,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "0.3.12",
|
"@swc/cli": "0.3.12",
|
||||||
"@swc/core": "1.4.13",
|
"@swc/core": "1.5.0",
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/color-convert": "^2.0.3",
|
"@types/color-convert": "^2.0.3",
|
||||||
"@types/content-disposition": "^0.5.8",
|
"@types/content-disposition": "^0.5.8",
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
"@types/qs": "6.9.14",
|
"@types/qs": "6.9.15",
|
||||||
"@types/random-seed": "0.3.5",
|
"@types/random-seed": "0.3.5",
|
||||||
"@types/ratelimiter": "3.4.6",
|
"@types/ratelimiter": "3.4.6",
|
||||||
"@types/rename": "1.0.7",
|
"@types/rename": "1.0.7",
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
"@types/websocket": "1.0.10",
|
"@types/websocket": "1.0.10",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.1.1",
|
||||||
"mocha": "10.4.0",
|
"mocha": "10.4.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
|
@ -179,7 +179,7 @@
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"type-fest": "4.15.0",
|
"type-fest": "4.17.0",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.4.5",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.91.0",
|
||||||
"ws": "8.16.0"
|
"ws": "8.16.0"
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AlterAkaType1714099399879 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user" ADD COLUMN "alsoKnownAs" character varying(512)[]`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "user" SET "alsoKnownAs" = string_to_array("alsoKnownAsOld", ',')::character varying[]`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "user" SET "alsoKnownAs" = NULL WHERE "alsoKnownAs" = '{}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAsOld"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "alsoKnownAs" text`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "user" SET "alsoKnownAs" = array_to_string("alsoKnownAsOld", ',')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAsOld"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,59 +0,0 @@
|
||||||
import probeImageSize from "probe-image-size";
|
|
||||||
import { Mutex } from "redis-semaphore";
|
|
||||||
|
|
||||||
import { FILE_TYPE_BROWSERSAFE } from "backend-rs";
|
|
||||||
import Logger from "@/services/logger.js";
|
|
||||||
import { Cache } from "./cache.js";
|
|
||||||
import { redisClient } from "@/db/redis.js";
|
|
||||||
import { inspect } from "node:util";
|
|
||||||
|
|
||||||
export type Size = {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cache = new Cache<boolean>("emojiMeta", 60 * 10); // once every 10 minutes for the same url
|
|
||||||
const logger = new Logger("emoji");
|
|
||||||
|
|
||||||
export async function getEmojiSize(url: string): Promise<Size> {
|
|
||||||
let attempted = true;
|
|
||||||
|
|
||||||
const lock = new Mutex(redisClient, "getEmojiSize");
|
|
||||||
await lock.acquire();
|
|
||||||
|
|
||||||
try {
|
|
||||||
attempted = (await cache.get(url)) === true;
|
|
||||||
if (!attempted) {
|
|
||||||
await cache.set(url, true);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await lock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempted) {
|
|
||||||
logger.warn(`Attempt limit exceeded: ${url}`);
|
|
||||||
throw new Error("Too many attempts");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.debug(`Retrieving emoji size from ${url}`);
|
|
||||||
const { width, height, mime } = await probeImageSize(url, {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
if (!(mime.startsWith("image/") && FILE_TYPE_BROWSERSAFE.includes(mime))) {
|
|
||||||
throw new Error("Unsupported image type");
|
|
||||||
}
|
|
||||||
return { width, height };
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Unable to retrieve metadata:\n${inspect(e)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNormalSize(
|
|
||||||
{ width, height }: Size,
|
|
||||||
orientation?: number,
|
|
||||||
): Size {
|
|
||||||
return (orientation || 0) >= 5
|
|
||||||
? { width: height, height: width }
|
|
||||||
: { width, height };
|
|
||||||
}
|
|
|
@ -88,7 +88,9 @@ export class User {
|
||||||
})
|
})
|
||||||
public movedToUri: string | null;
|
public movedToUri: string | null;
|
||||||
|
|
||||||
@Column("simple-array", {
|
@Column("varchar", {
|
||||||
|
length: 512,
|
||||||
|
array: true,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
comment: "URIs the user is known as too",
|
comment: "URIs the user is known as too",
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { IsNull } from "typeorm";
|
||||||
import { Emojis } from "@/models/index.js";
|
import { Emojis } from "@/models/index.js";
|
||||||
|
|
||||||
import { queueLogger } from "../../logger.js";
|
import { queueLogger } from "../../logger.js";
|
||||||
import { getEmojiSize } from "@/misc/emoji-meta.js";
|
import { getImageSizeFromUrl } from "backend-rs";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger("local-emoji-size");
|
const logger = queueLogger.createSubLogger("local-emoji-size");
|
||||||
|
@ -21,7 +21,7 @@ export async function setLocalEmojiSizes(
|
||||||
|
|
||||||
for (let i = 0; i < emojis.length; i++) {
|
for (let i = 0; i < emojis.length; i++) {
|
||||||
try {
|
try {
|
||||||
const size = await getEmojiSize(emojis[i].publicUrl);
|
const size = await getImageSizeFromUrl(emojis[i].publicUrl);
|
||||||
await Emojis.update(emojis[i].id, {
|
await Emojis.update(emojis[i].id, {
|
||||||
width: size.width || null,
|
width: size.width || null,
|
||||||
height: size.height || null,
|
height: size.height || null,
|
||||||
|
|
|
@ -13,7 +13,13 @@ import { extractPollFromQuestion } from "./question.js";
|
||||||
import vote from "@/services/note/polls/vote.js";
|
import vote from "@/services/note/polls/vote.js";
|
||||||
import { apLogger } from "../logger.js";
|
import { apLogger } from "../logger.js";
|
||||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
import { extractHost, isSameOrigin, toPuny } from "backend-rs";
|
import {
|
||||||
|
type ImageSize,
|
||||||
|
extractHost,
|
||||||
|
getImageSizeFromUrl,
|
||||||
|
isSameOrigin,
|
||||||
|
toPuny,
|
||||||
|
} from "backend-rs";
|
||||||
import {
|
import {
|
||||||
Emojis,
|
Emojis,
|
||||||
Polls,
|
Polls,
|
||||||
|
@ -46,7 +52,6 @@ import { UserProfiles } from "@/models/index.js";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
import { config } from "@/config.js";
|
import { config } from "@/config.js";
|
||||||
import { truncate } from "@/misc/truncate.js";
|
import { truncate } from "@/misc/truncate.js";
|
||||||
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
|
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "@/misc/langmap.js";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
|
@ -488,11 +493,16 @@ export async function extractEmojis(
|
||||||
tag.icon!.url !== exists.originalUrl ||
|
tag.icon!.url !== exists.originalUrl ||
|
||||||
!(exists.width && exists.height)
|
!(exists.width && exists.height)
|
||||||
) {
|
) {
|
||||||
let size: Size = { width: 0, height: 0 };
|
let size: ImageSize | null = null;
|
||||||
try {
|
if (tag.icon?.url != null) {
|
||||||
size = await getEmojiSize(tag.icon!.url);
|
try {
|
||||||
} catch {
|
size = await getImageSizeFromUrl(tag.icon.url);
|
||||||
/* skip if any error happens */
|
} catch (err) {
|
||||||
|
apLogger.info(
|
||||||
|
`Failed to determine the size of the image: ${tag.icon.url}`,
|
||||||
|
);
|
||||||
|
apLogger.debug(inspect(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await Emojis.update(
|
await Emojis.update(
|
||||||
{
|
{
|
||||||
|
@ -504,8 +514,8 @@ export async function extractEmojis(
|
||||||
originalUrl: tag.icon!.url,
|
originalUrl: tag.icon!.url,
|
||||||
publicUrl: tag.icon!.url,
|
publicUrl: tag.icon!.url,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
width: size.width || null,
|
width: size?.width || null,
|
||||||
height: size.height || null,
|
height: size?.height || null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -520,9 +530,9 @@ export async function extractEmojis(
|
||||||
|
|
||||||
apLogger.info(`register emoji host=${host}, name=${name}`);
|
apLogger.info(`register emoji host=${host}, name=${name}`);
|
||||||
|
|
||||||
let size: Size = { width: 0, height: 0 };
|
let size: ImageSize = { width: 0, height: 0 };
|
||||||
try {
|
try {
|
||||||
size = await getEmojiSize(tag.icon!.url);
|
size = await getImageSizeFromUrl(tag.icon!.url);
|
||||||
} catch {
|
} catch {
|
||||||
/* skip if any error happens */
|
/* skip if any error happens */
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { publishMainStream } from "@/services/stream.js";
|
||||||
import {
|
import {
|
||||||
publishMainStream,
|
publishToChatStream,
|
||||||
publishGroupMessagingStream,
|
publishToGroupChatStream,
|
||||||
} from "@/services/stream.js";
|
publishToChatIndexStream,
|
||||||
import { publishToChatStream, ChatEvent } from "backend-rs";
|
ChatEvent,
|
||||||
import { publishMessagingIndexStream } from "@/services/stream.js";
|
ChatIndexEvent,
|
||||||
|
} from "backend-rs";
|
||||||
import { pushNotification } from "@/services/push-notification.js";
|
import { pushNotification } from "@/services/push-notification.js";
|
||||||
import type { User, IRemoteUser } from "@/models/entities/user.js";
|
import type { User, IRemoteUser } from "@/models/entities/user.js";
|
||||||
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
||||||
|
@ -55,7 +57,7 @@ export async function readUserMessagingMessage(
|
||||||
|
|
||||||
// Publish event
|
// Publish event
|
||||||
publishToChatStream(otherpartyId, userId, ChatEvent.Read, messageIds);
|
publishToChatStream(otherpartyId, userId, ChatEvent.Read, messageIds);
|
||||||
publishMessagingIndexStream(userId, "read", messageIds);
|
publishToChatIndexStream(userId, ChatIndexEvent.Read, messageIds);
|
||||||
|
|
||||||
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
|
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
|
||||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||||
|
@ -126,11 +128,11 @@ export async function readGroupMessagingMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish event
|
// Publish event
|
||||||
publishGroupMessagingStream(groupId, "read", {
|
publishToGroupChatStream(groupId, ChatEvent.Read, {
|
||||||
ids: reads,
|
ids: reads,
|
||||||
userId: userId,
|
userId,
|
||||||
});
|
});
|
||||||
publishMessagingIndexStream(userId, "read", reads);
|
publishToChatIndexStream(userId, ChatIndexEvent.Read, reads);
|
||||||
|
|
||||||
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
|
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
|
||||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
import { Emojis, DriveFiles } from "@/models/index.js";
|
import { Emojis, DriveFiles } from "@/models/index.js";
|
||||||
import { genId } from "backend-rs";
|
import {
|
||||||
|
type ImageSize,
|
||||||
|
genId,
|
||||||
|
getImageSizeFromUrl,
|
||||||
|
publishToBroadcastStream,
|
||||||
|
} from "backend-rs";
|
||||||
import { insertModerationLog } from "@/services/insert-moderation-log.js";
|
import { insertModerationLog } from "@/services/insert-moderation-log.js";
|
||||||
import { ApiError } from "@/server/api/error.js";
|
import { ApiError } from "@/server/api/error.js";
|
||||||
import rndstr from "rndstr";
|
import rndstr from "rndstr";
|
||||||
import { publishBroadcastStream } from "@/services/stream.js";
|
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
import { getEmojiSize } from "@/misc/emoji-meta.js";
|
import { apiLogger } from "@/server/api/logger.js";
|
||||||
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["admin", "emoji"],
|
tags: ["admin", "emoji"],
|
||||||
|
@ -49,7 +54,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
? file.name.split(".")[0]
|
? file.name.split(".")[0]
|
||||||
: `_${rndstr("a-z0-9", 8)}_`;
|
: `_${rndstr("a-z0-9", 8)}_`;
|
||||||
|
|
||||||
const size = await getEmojiSize(file.url);
|
let size: ImageSize | null = null;
|
||||||
|
try {
|
||||||
|
size = await getImageSizeFromUrl(file.url);
|
||||||
|
} catch (err) {
|
||||||
|
apiLogger.info(`Failed to determine the image size: ${file.url}`);
|
||||||
|
apiLogger.debug(inspect(err));
|
||||||
|
}
|
||||||
|
|
||||||
const emoji = await Emojis.insert({
|
const emoji = await Emojis.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
@ -62,15 +73,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
publicUrl: file.webpublicUrl ?? file.url,
|
publicUrl: file.webpublicUrl ?? file.url,
|
||||||
type: file.webpublicType ?? file.type,
|
type: file.webpublicType ?? file.type,
|
||||||
license: null,
|
license: null,
|
||||||
width: size.width || null,
|
width: size?.width || null,
|
||||||
height: size.height || null,
|
height: size?.height || null,
|
||||||
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
await db.queryResultCache!.remove(["meta_emojis"]);
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
|
||||||
publishBroadcastStream("emojiAdded", {
|
publishToBroadcastStream(await Emojis.pack(emoji));
|
||||||
emoji: await Emojis.pack(emoji.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
insertModerationLog(me, "addEmoji", {
|
insertModerationLog(me, "addEmoji", {
|
||||||
emojiId: emoji.id,
|
emojiId: emoji.id,
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
import { Emojis } from "@/models/index.js";
|
import { Emojis } from "@/models/index.js";
|
||||||
import { genId } from "backend-rs";
|
import {
|
||||||
|
type ImageSize,
|
||||||
|
genId,
|
||||||
|
getImageSizeFromUrl,
|
||||||
|
publishToBroadcastStream,
|
||||||
|
} from "backend-rs";
|
||||||
import { ApiError } from "@/server/api/error.js";
|
import { ApiError } from "@/server/api/error.js";
|
||||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
||||||
import { publishBroadcastStream } from "@/services/stream.js";
|
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
import { getEmojiSize } from "@/misc/emoji-meta.js";
|
import { apiLogger } from "@/server/api/logger.js";
|
||||||
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["admin", "emoji"],
|
tags: ["admin", "emoji"],
|
||||||
|
@ -76,7 +81,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
throw new ApiError();
|
throw new ApiError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = await getEmojiSize(driveFile.url);
|
let size: ImageSize | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
size = await getImageSizeFromUrl(driveFile.url);
|
||||||
|
} catch (err) {
|
||||||
|
apiLogger.info(`Failed to determine the image size: ${driveFile.url}`);
|
||||||
|
apiLogger.debug(inspect(err));
|
||||||
|
}
|
||||||
|
|
||||||
const copied = await Emojis.insert({
|
const copied = await Emojis.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
@ -88,15 +100,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
type: driveFile.webpublicType ?? driveFile.type,
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
license: emoji.license,
|
license: emoji.license,
|
||||||
width: size.width || null,
|
width: size?.width ?? null,
|
||||||
height: size.height || null,
|
height: size?.height ?? null,
|
||||||
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
await db.queryResultCache!.remove(["meta_emojis"]);
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
|
||||||
publishBroadcastStream("emojiAdded", {
|
publishToBroadcastStream(await Emojis.pack(copied));
|
||||||
emoji: await Emojis.pack(copied.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: copied.id,
|
id: copied.id,
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import * as mfm from "mfm-js";
|
import * as mfm from "mfm-js";
|
||||||
import sanitizeHtml from "sanitize-html";
|
import sanitizeHtml from "sanitize-html";
|
||||||
import { publishAdminStream } from "@/services/stream.js";
|
|
||||||
import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js";
|
import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js";
|
||||||
import { genId } from "backend-rs";
|
import { genId, publishToModerationStream } from "backend-rs";
|
||||||
import { sendEmail } from "@/services/send-email.js";
|
import { sendEmail } from "@/services/send-email.js";
|
||||||
import { fetchMeta } from "backend-rs";
|
|
||||||
import { getUser } from "@/server/api/common/getters.js";
|
import { getUser } from "@/server/api/common/getters.js";
|
||||||
import { ApiError } from "@/server/api/error.js";
|
import { ApiError } from "@/server/api/error.js";
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
|
@ -86,9 +84,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const meta = await fetchMeta(true);
|
|
||||||
for (const moderator of moderators) {
|
for (const moderator of moderators) {
|
||||||
publishAdminStream(moderator.id, "newAbuseUserReport", {
|
publishToModerationStream(moderator.id, {
|
||||||
id: report.id,
|
id: report.id,
|
||||||
targetUserId: report.targetUserId,
|
targetUserId: report.targetUserId,
|
||||||
reporterId: report.reporterId,
|
reporterId: report.reporterId,
|
||||||
|
|
|
@ -15,10 +15,11 @@ import {
|
||||||
import type { AccessToken } from "@/models/entities/access-token.js";
|
import type { AccessToken } from "@/models/entities/access-token.js";
|
||||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
import {
|
import {
|
||||||
publishChannelStream,
|
publishToChannelStream,
|
||||||
publishGroupMessagingStream,
|
publishToChatStream,
|
||||||
} from "@/services/stream.js";
|
publishToGroupChatStream,
|
||||||
import { publishToChatStream, ChatEvent } from "backend-rs";
|
ChatEvent,
|
||||||
|
} from "backend-rs";
|
||||||
import type { UserGroup } from "@/models/entities/user-group.js";
|
import type { UserGroup } from "@/models/entities/user-group.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
import { readNotification } from "@/server/api/common/read-notification.js";
|
import { readNotification } from "@/server/api/common/read-notification.js";
|
||||||
|
@ -512,9 +513,9 @@ export default class Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private typingOnChannel(channel: ChannelModel["id"]) {
|
private typingOnChannel(channelId: ChannelModel["id"]) {
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
publishChannelStream(channel, "typing", this.user.id);
|
publishToChannelStream(channelId, this.user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -530,8 +531,8 @@ export default class Connection {
|
||||||
ChatEvent.Typing,
|
ChatEvent.Typing,
|
||||||
this.user.id,
|
this.user.id,
|
||||||
);
|
);
|
||||||
} else if (param.group) {
|
} else if (param.group != null) {
|
||||||
publishGroupMessagingStream(param.group, "typing", this.user.id);
|
publishToGroupChatStream(param.group, ChatEvent.Typing, this.user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,8 @@ export default class Logger {
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private showThisLog(logLevel: Level, configLevel: string) {
|
private showThisLog(logLevel: Level, configMaxLevel: string) {
|
||||||
switch (configLevel) {
|
switch (configMaxLevel) {
|
||||||
case "error":
|
case "error":
|
||||||
return ["error"].includes(logLevel);
|
return ["error"].includes(logLevel);
|
||||||
case "warning":
|
case "warning":
|
||||||
|
@ -75,7 +75,10 @@ export default class Logger {
|
||||||
if (
|
if (
|
||||||
(config.maxLogLevel != null &&
|
(config.maxLogLevel != null &&
|
||||||
!this.showThisLog(level, config.maxLogLevel)) ||
|
!this.showThisLog(level, config.maxLogLevel)) ||
|
||||||
(config.logLevel != null && !config.logLevel.includes(level))
|
(config.logLevel != null && !config.logLevel.includes(level)) ||
|
||||||
|
(config.maxLogLevel == null &&
|
||||||
|
config.logLevel == null &&
|
||||||
|
!this.showThisLog(level, "info"))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
if (!this.store) store = false;
|
if (!this.store) store = false;
|
||||||
|
|
|
@ -7,13 +7,17 @@ import {
|
||||||
Mutings,
|
Mutings,
|
||||||
Users,
|
Users,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import { genId, publishToChatStream, toPuny, ChatEvent } from "backend-rs";
|
|
||||||
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
|
||||||
import {
|
import {
|
||||||
publishMessagingIndexStream,
|
genId,
|
||||||
publishMainStream,
|
publishToChatStream,
|
||||||
publishGroupMessagingStream,
|
publishToGroupChatStream,
|
||||||
} from "@/services/stream.js";
|
publishToChatIndexStream,
|
||||||
|
toPuny,
|
||||||
|
ChatEvent,
|
||||||
|
ChatIndexEvent,
|
||||||
|
} from "backend-rs";
|
||||||
|
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
||||||
|
import { publishMainStream } from "@/services/stream.js";
|
||||||
import { pushNotification } from "@/services/push-notification.js";
|
import { pushNotification } from "@/services/push-notification.js";
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
|
@ -57,7 +61,11 @@ export async function createMessage(
|
||||||
ChatEvent.Message,
|
ChatEvent.Message,
|
||||||
messageObj,
|
messageObj,
|
||||||
);
|
);
|
||||||
publishMessagingIndexStream(message.userId, "message", messageObj);
|
publishToChatIndexStream(
|
||||||
|
message.userId,
|
||||||
|
ChatIndexEvent.Message,
|
||||||
|
messageObj,
|
||||||
|
);
|
||||||
publishMainStream(message.userId, "messagingMessage", messageObj);
|
publishMainStream(message.userId, "messagingMessage", messageObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,19 +77,27 @@ export async function createMessage(
|
||||||
ChatEvent.Message,
|
ChatEvent.Message,
|
||||||
messageObj,
|
messageObj,
|
||||||
);
|
);
|
||||||
publishMessagingIndexStream(recipientUser.id, "message", messageObj);
|
publishToChatIndexStream(
|
||||||
|
recipientUser.id,
|
||||||
|
ChatIndexEvent.Message,
|
||||||
|
messageObj,
|
||||||
|
);
|
||||||
publishMainStream(recipientUser.id, "messagingMessage", messageObj);
|
publishMainStream(recipientUser.id, "messagingMessage", messageObj);
|
||||||
}
|
}
|
||||||
} else if (recipientGroup) {
|
} else if (recipientGroup != null) {
|
||||||
// グループのストリーム
|
// group's stream
|
||||||
publishGroupMessagingStream(recipientGroup.id, "message", messageObj);
|
publishToGroupChatStream(recipientGroup.id, ChatEvent.Message, messageObj);
|
||||||
|
|
||||||
// メンバーのストリーム
|
// member's stream
|
||||||
const joinings = await UserGroupJoinings.findBy({
|
const joinings = await UserGroupJoinings.findBy({
|
||||||
userGroupId: recipientGroup.id,
|
userGroupId: recipientGroup.id,
|
||||||
});
|
});
|
||||||
for (const joining of joinings) {
|
for (const joining of joinings) {
|
||||||
publishMessagingIndexStream(joining.userId, "message", messageObj);
|
publishToChatIndexStream(
|
||||||
|
joining.userId,
|
||||||
|
ChatIndexEvent.Message,
|
||||||
|
messageObj,
|
||||||
|
);
|
||||||
publishMainStream(joining.userId, "messagingMessage", messageObj);
|
publishMainStream(joining.userId, "messagingMessage", messageObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { config } from "@/config.js";
|
import { config } from "@/config.js";
|
||||||
import { MessagingMessages, Users } from "@/models/index.js";
|
import { MessagingMessages, Users } from "@/models/index.js";
|
||||||
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
||||||
import { publishGroupMessagingStream } from "@/services/stream.js";
|
import {
|
||||||
import { publishToChatStream, ChatEvent } from "backend-rs";
|
publishToChatStream,
|
||||||
|
publishToGroupChatStream,
|
||||||
|
ChatEvent,
|
||||||
|
} from "backend-rs";
|
||||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||||
import renderDelete from "@/remote/activitypub/renderer/delete.js";
|
import renderDelete from "@/remote/activitypub/renderer/delete.js";
|
||||||
import renderTombstone from "@/remote/activitypub/renderer/tombstone.js";
|
import renderTombstone from "@/remote/activitypub/renderer/tombstone.js";
|
||||||
|
@ -42,7 +45,7 @@ async function postDeleteMessage(message: MessagingMessage) {
|
||||||
);
|
);
|
||||||
deliver(user, activity, recipient.inbox);
|
deliver(user, activity, recipient.inbox);
|
||||||
}
|
}
|
||||||
} else if (message.groupId) {
|
} else if (message.groupId != null) {
|
||||||
publishGroupMessagingStream(message.groupId, "deleted", message.id);
|
publishToGroupChatStream(message.groupId, ChatEvent.Deleted, message.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,21 @@ import { redisClient } from "@/db/redis.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import type { UserList } from "@/models/entities/user-list.js";
|
import type { UserList } from "@/models/entities/user-list.js";
|
||||||
import type { UserGroup } from "@/models/entities/user-group.js";
|
// import type { UserGroup } from "@/models/entities/user-group.js";
|
||||||
import { config } from "@/config.js";
|
import { config } from "@/config.js";
|
||||||
// import type { Antenna } from "@/models/entities/antenna.js";
|
// import type { Antenna } from "@/models/entities/antenna.js";
|
||||||
import type { Channel } from "@/models/entities/channel.js";
|
// import type { Channel } from "@/models/entities/channel.js";
|
||||||
import type {
|
import type {
|
||||||
StreamChannels,
|
StreamChannels,
|
||||||
AdminStreamTypes,
|
// AdminStreamTypes,
|
||||||
// AntennaStreamTypes,
|
// AntennaStreamTypes,
|
||||||
BroadcastTypes,
|
// BroadcastTypes,
|
||||||
ChannelStreamTypes,
|
// ChannelStreamTypes,
|
||||||
DriveStreamTypes,
|
DriveStreamTypes,
|
||||||
GroupMessagingStreamTypes,
|
// GroupMessagingStreamTypes,
|
||||||
InternalStreamTypes,
|
InternalStreamTypes,
|
||||||
MainStreamTypes,
|
MainStreamTypes,
|
||||||
MessagingIndexStreamTypes,
|
// MessagingIndexStreamTypes,
|
||||||
// MessagingStreamTypes,
|
// MessagingStreamTypes,
|
||||||
NoteStreamTypes,
|
NoteStreamTypes,
|
||||||
UserListStreamTypes,
|
UserListStreamTypes,
|
||||||
|
@ -64,16 +64,17 @@ class Publisher {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public publishBroadcastStream = <K extends keyof BroadcastTypes>(
|
/* ported to backend-rs */
|
||||||
type: K,
|
// public publishBroadcastStream = <K extends keyof BroadcastTypes>(
|
||||||
value?: BroadcastTypes[K],
|
// type: K,
|
||||||
): void => {
|
// value?: BroadcastTypes[K],
|
||||||
this.publish(
|
// ): void => {
|
||||||
"broadcast",
|
// this.publish(
|
||||||
type,
|
// "broadcast",
|
||||||
typeof value === "undefined" ? null : value,
|
// type,
|
||||||
);
|
// typeof value === "undefined" ? null : value,
|
||||||
};
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
public publishMainStream = <K extends keyof MainStreamTypes>(
|
public publishMainStream = <K extends keyof MainStreamTypes>(
|
||||||
userId: User["id"],
|
userId: User["id"],
|
||||||
|
@ -110,17 +111,18 @@ class Publisher {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public publishChannelStream = <K extends keyof ChannelStreamTypes>(
|
/* ported to backend-rs */
|
||||||
channelId: Channel["id"],
|
// public publishChannelStream = <K extends keyof ChannelStreamTypes>(
|
||||||
type: K,
|
// channelId: Channel["id"],
|
||||||
value?: ChannelStreamTypes[K],
|
// type: K,
|
||||||
): void => {
|
// value?: ChannelStreamTypes[K],
|
||||||
this.publish(
|
// ): void => {
|
||||||
`channelStream:${channelId}`,
|
// this.publish(
|
||||||
type,
|
// `channelStream:${channelId}`,
|
||||||
typeof value === "undefined" ? null : value,
|
// type,
|
||||||
);
|
// typeof value === "undefined" ? null : value,
|
||||||
};
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
public publishUserListStream = <K extends keyof UserListStreamTypes>(
|
public publishUserListStream = <K extends keyof UserListStreamTypes>(
|
||||||
listId: UserList["id"],
|
listId: UserList["id"],
|
||||||
|
@ -161,49 +163,52 @@ class Publisher {
|
||||||
// );
|
// );
|
||||||
// };
|
// };
|
||||||
|
|
||||||
public publishGroupMessagingStream = <
|
/* ported to backend-rs */
|
||||||
K extends keyof GroupMessagingStreamTypes,
|
// public publishGroupMessagingStream = <
|
||||||
>(
|
// K extends keyof GroupMessagingStreamTypes,
|
||||||
groupId: UserGroup["id"],
|
// >(
|
||||||
type: K,
|
// groupId: UserGroup["id"],
|
||||||
value?: GroupMessagingStreamTypes[K],
|
// type: K,
|
||||||
): void => {
|
// value?: GroupMessagingStreamTypes[K],
|
||||||
this.publish(
|
// ): void => {
|
||||||
`messagingStream:${groupId}`,
|
// this.publish(
|
||||||
type,
|
// `messagingStream:${groupId}`,
|
||||||
typeof value === "undefined" ? null : value,
|
// type,
|
||||||
);
|
// typeof value === "undefined" ? null : value,
|
||||||
};
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
public publishMessagingIndexStream = <
|
/* ported to backend-rs */
|
||||||
K extends keyof MessagingIndexStreamTypes,
|
// public publishMessagingIndexStream = <
|
||||||
>(
|
// K extends keyof MessagingIndexStreamTypes,
|
||||||
userId: User["id"],
|
// >(
|
||||||
type: K,
|
// userId: User["id"],
|
||||||
value?: MessagingIndexStreamTypes[K],
|
// type: K,
|
||||||
): void => {
|
// value?: MessagingIndexStreamTypes[K],
|
||||||
this.publish(
|
// ): void => {
|
||||||
`messagingIndexStream:${userId}`,
|
// this.publish(
|
||||||
type,
|
// `messagingIndexStream:${userId}`,
|
||||||
typeof value === "undefined" ? null : value,
|
// type,
|
||||||
);
|
// typeof value === "undefined" ? null : value,
|
||||||
};
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
public publishNotesStream = (note: Note): void => {
|
public publishNotesStream = (note: Note): void => {
|
||||||
this.publish("notesStream", null, note);
|
this.publish("notesStream", null, note);
|
||||||
};
|
};
|
||||||
|
|
||||||
public publishAdminStream = <K extends keyof AdminStreamTypes>(
|
/* ported to backend-rs */
|
||||||
userId: User["id"],
|
// public publishAdminStream = <K extends keyof AdminStreamTypes>(
|
||||||
type: K,
|
// userId: User["id"],
|
||||||
value?: AdminStreamTypes[K],
|
// type: K,
|
||||||
): void => {
|
// value?: AdminStreamTypes[K],
|
||||||
this.publish(
|
// ): void => {
|
||||||
`adminStream:${userId}`,
|
// this.publish(
|
||||||
type,
|
// `adminStream:${userId}`,
|
||||||
typeof value === "undefined" ? null : value,
|
// type,
|
||||||
);
|
// typeof value === "undefined" ? null : value,
|
||||||
};
|
// );
|
||||||
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
const publisher = new Publisher();
|
const publisher = new Publisher();
|
||||||
|
@ -212,17 +217,15 @@ export default publisher;
|
||||||
|
|
||||||
export const publishInternalEvent = publisher.publishInternalEvent;
|
export const publishInternalEvent = publisher.publishInternalEvent;
|
||||||
export const publishUserEvent = publisher.publishUserEvent;
|
export const publishUserEvent = publisher.publishUserEvent;
|
||||||
export const publishBroadcastStream = publisher.publishBroadcastStream;
|
// export const publishBroadcastStream = publisher.publishBroadcastStream;
|
||||||
export const publishMainStream = publisher.publishMainStream;
|
export const publishMainStream = publisher.publishMainStream;
|
||||||
export const publishDriveStream = publisher.publishDriveStream;
|
export const publishDriveStream = publisher.publishDriveStream;
|
||||||
export const publishNoteStream = publisher.publishNoteStream;
|
export const publishNoteStream = publisher.publishNoteStream;
|
||||||
export const publishNotesStream = publisher.publishNotesStream;
|
export const publishNotesStream = publisher.publishNotesStream;
|
||||||
export const publishChannelStream = publisher.publishChannelStream;
|
// export const publishChannelStream = publisher.publishChannelStream;
|
||||||
export const publishUserListStream = publisher.publishUserListStream;
|
export const publishUserListStream = publisher.publishUserListStream;
|
||||||
// export const publishAntennaStream = publisher.publishAntennaStream;
|
// export const publishAntennaStream = publisher.publishAntennaStream;
|
||||||
// export const publishMessagingStream = publisher.publishMessagingStream;
|
// export const publishMessagingStream = publisher.publishMessagingStream;
|
||||||
export const publishGroupMessagingStream =
|
// export const publishGroupMessagingStream = publisher.publishGroupMessagingStream;
|
||||||
publisher.publishGroupMessagingStream;
|
// export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
|
||||||
export const publishMessagingIndexStream =
|
// export const publishAdminStream = publisher.publishAdminStream;
|
||||||
publisher.publishMessagingIndexStream;
|
|
||||||
export const publishAdminStream = publisher.publishAdminStream;
|
|
||||||
|
|
|
@ -28,13 +28,14 @@
|
||||||
"@types/matter-js": "0.19.6",
|
"@types/matter-js": "0.19.6",
|
||||||
"@types/prismjs": "^1.26.3",
|
"@types/prismjs": "^1.26.3",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
|
"@types/qrcode": "1.5.5",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/textarea-caret": "^3.0.3",
|
"@types/textarea-caret": "^3.0.3",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/uuid": "9.0.8",
|
"@types/uuid": "9.0.8",
|
||||||
"@vitejs/plugin-vue": "5.0.4",
|
"@vitejs/plugin-vue": "5.0.4",
|
||||||
"@vue/runtime-core": "3.4.21",
|
"@vue/runtime-core": "3.4.25",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
"autosize": "6.0.1",
|
"autosize": "6.0.1",
|
||||||
"broadcast-channel": "7.0.0",
|
"broadcast-channel": "7.0.0",
|
||||||
|
@ -60,14 +61,18 @@
|
||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"katex": "0.16.10",
|
"katex": "0.16.10",
|
||||||
|
"long": "^5.2.3",
|
||||||
"libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build",
|
"libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build",
|
||||||
"matter-js": "0.19.0",
|
"matter-js": "0.19.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
|
"multer": "1.4.5-lts.1",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"photoswipe": "5.4.3",
|
"photoswipe": "5.4.3",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"rollup": "4.14.2",
|
"qrcode": "1.5.3",
|
||||||
|
"qrcode-vue3": "^1.6.8",
|
||||||
|
"rollup": "4.16.4",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sass": "1.75.0",
|
"sass": "1.75.0",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
|
@ -75,19 +80,19 @@
|
||||||
"swiper": "11.1.1",
|
"swiper": "11.1.1",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.163.0",
|
"three": "0.164.1",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tinyld": "^1.3.4",
|
"tinyld": "^1.3.4",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.4.5",
|
||||||
"unicode-emoji-json": "^0.6.0",
|
"unicode-emoji-json": "^0.6.0",
|
||||||
"uuid": "9.0.1",
|
"uuid": "9.0.1",
|
||||||
"vite": "5.2.8",
|
"vite": "5.2.10",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "3.4.21",
|
"vue": "3.4.25",
|
||||||
"vue-draggable-plus": "^0.4.0",
|
"vue-draggable-plus": "^0.4.0",
|
||||||
"vue-plyr": "^7.0.0",
|
"vue-plyr": "^7.0.0",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
"vue-prism-editor": "2.0.0-alpha.2",
|
||||||
"vue-tsc": "2.0.13"
|
"vue-tsc": "2.0.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,7 @@
|
||||||
:detailed="true"
|
:detailed="true"
|
||||||
:detailed-view="detailedView"
|
:detailed-view="detailedView"
|
||||||
:parent-id="appearNote.id"
|
:parent-id="appearNote.id"
|
||||||
|
:is-long-judger="isLongJudger"
|
||||||
@push="(e) => router.push(notePage(e))"
|
@push="(e) => router.push(notePage(e))"
|
||||||
@focusfooter="footerEl!.focus()"
|
@focusfooter="footerEl!.focus()"
|
||||||
@expanded="(e) => setPostExpanded(e)"
|
@expanded="(e) => setPostExpanded(e)"
|
||||||
|
@ -325,6 +326,7 @@ const props = defineProps<{
|
||||||
collapsedReply?: boolean;
|
collapsedReply?: boolean;
|
||||||
hideFooter?: boolean;
|
hideFooter?: boolean;
|
||||||
hideEmojiViewer?: boolean;
|
hideEmojiViewer?: boolean;
|
||||||
|
isLongJudger?: (note: entities.Note) => boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inChannel = inject("inChannel", null);
|
const inChannel = inject("inChannel", null);
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
: notification.reaction
|
: notification.reaction
|
||||||
"
|
"
|
||||||
:custom-emojis="notification.note.emojis"
|
:custom-emojis="notification.note.emojis"
|
||||||
:no-style="true"
|
|
||||||
/>
|
/>
|
||||||
<XReactionIcon
|
<XReactionIcon
|
||||||
v-else-if="
|
v-else-if="
|
||||||
|
@ -60,18 +59,20 @@
|
||||||
class="content"
|
class="content"
|
||||||
:note="removeReplyTo(notification.note.renote)"
|
:note="removeReplyTo(notification.note.renote)"
|
||||||
:hide-emoji-viewer="true"
|
:hide-emoji-viewer="true"
|
||||||
|
:is-long-judger="isLongJudger"
|
||||||
/>
|
/>
|
||||||
<XNote
|
<XNote
|
||||||
v-else
|
v-else
|
||||||
class="content"
|
class="content"
|
||||||
:note="removeReplyTo(notification.note)"
|
:note="removeReplyTo(notification.note)"
|
||||||
:hide-emoji-viewer="true"
|
:hide-emoji-viewer="true"
|
||||||
|
:is-long-judger="isLongJudger"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||||
import type { Connection } from "firefish-js/src/streaming";
|
import type { Connection } from "firefish-js/src/streaming";
|
||||||
import type { Channels } from "firefish-js/src/streaming.types";
|
import type { Channels } from "firefish-js/src/streaming.types";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
|
@ -114,12 +115,23 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
|
||||||
? instance.defaultReaction
|
? instance.defaultReaction
|
||||||
: "⭐";
|
: "⭐";
|
||||||
|
|
||||||
const users = ref(props.notification.users.slice(0, 5));
|
const users = computed(() => props.notification.users.slice(0, 5));
|
||||||
const userleft = ref(props.notification.users.length - users.value.length);
|
const userleft = computed(
|
||||||
|
() => props.notification.users.length - users.value.length,
|
||||||
|
);
|
||||||
|
|
||||||
let readObserver: IntersectionObserver | undefined;
|
let readObserver: IntersectionObserver | undefined;
|
||||||
let connection: Connection<Channels["main"]> | null = null;
|
let connection: Connection<Channels["main"]> | null = null;
|
||||||
|
|
||||||
|
function isLongJudger(note: entities.Note) {
|
||||||
|
return (
|
||||||
|
note.text != null &&
|
||||||
|
(note.text.split("\n").length > 5 ||
|
||||||
|
note.text.length > 300 ||
|
||||||
|
note.files.length > 4)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getText() {
|
function getText() {
|
||||||
let res = "";
|
let res = "";
|
||||||
switch (props.notification.type) {
|
switch (props.notification.type) {
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination
|
||||||
|
ref="pagingComponent"
|
||||||
|
:pagination="pagination"
|
||||||
|
:folder="convertNotification"
|
||||||
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img
|
<img
|
||||||
|
@ -11,9 +15,9 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
<template #default="{ foldedItems: notifications }">
|
||||||
<XList
|
<XList
|
||||||
:items="convertNotification(notifications)"
|
:items="notifications"
|
||||||
v-slot="{ item: notification }"
|
v-slot="{ item: notification }"
|
||||||
class="elsfgstc"
|
class="elsfgstc"
|
||||||
:no-gap="true"
|
:no-gap="true"
|
||||||
|
@ -92,7 +96,7 @@ const pagination = Object.assign(
|
||||||
},
|
},
|
||||||
shouldFold
|
shouldFold
|
||||||
? {
|
? {
|
||||||
limit: FETCH_LIMIT,
|
limit: 50,
|
||||||
secondFetchLimit: FETCH_LIMIT,
|
secondFetchLimit: FETCH_LIMIT,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
@ -134,11 +138,11 @@ const onNotification = (notification: entities.Notification) => {
|
||||||
|
|
||||||
let connection: StreamTypes.ChannelOf<"main"> | undefined;
|
let connection: StreamTypes.ChannelOf<"main"> | undefined;
|
||||||
|
|
||||||
function convertNotification(n: entities.Notification[]) {
|
function convertNotification(ns: entities.Notification[]) {
|
||||||
if (shouldFold) {
|
if (shouldFold) {
|
||||||
return foldNotifications(n, FETCH_LIMIT);
|
return foldNotifications(ns);
|
||||||
} else {
|
} else {
|
||||||
return n;
|
return ns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading" />
|
<MkLoading v-else class="loading" />
|
||||||
</div>
|
</div>
|
||||||
<slot :items="items"></slot>
|
<slot :items="items" :foldedItems="foldedItems"></slot>
|
||||||
<div
|
<div
|
||||||
v-show="!pagination.reversed && more"
|
v-show="!pagination.reversed && more"
|
||||||
key="_more_"
|
key="_more_"
|
||||||
|
@ -66,8 +66,8 @@
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup generic="E extends PagingKey">
|
<script lang="ts" setup generic="E extends PagingKey, Fold extends PagingAble">
|
||||||
import type { ComponentPublicInstance, ComputedRef } from "vue";
|
import type { ComponentPublicInstance, ComputedRef, Ref } from "vue";
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
isRef,
|
isRef,
|
||||||
|
@ -79,12 +79,7 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import type { Endpoints, TypeUtils } from "firefish-js";
|
import type { Endpoints, TypeUtils } from "firefish-js";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import {
|
import { isTopVisible, onScrollTop } from "@/scripts/scroll";
|
||||||
getScrollContainer,
|
|
||||||
getScrollPosition,
|
|
||||||
isTopVisible,
|
|
||||||
onScrollTop,
|
|
||||||
} from "@/scripts/scroll";
|
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
@ -105,11 +100,15 @@ export type MkPaginationType<
|
||||||
reload: () => Promise<void>;
|
reload: () => Promise<void>;
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
prepend: (item: Item) => Promise<void>;
|
prepend: (item: Item) => Promise<void>;
|
||||||
append: (item: Item) => Promise<void>;
|
append: (...item: Item[]) => Promise<void>;
|
||||||
removeItem: (finder: (item: Item) => boolean) => boolean;
|
removeItem: (finder: (item: Item) => boolean) => boolean;
|
||||||
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
|
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PagingAble = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
|
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
||||||
export type PagingKey = PagingKeyOf<any>;
|
export type PagingKey = PagingKeyOf<any>;
|
||||||
|
@ -142,13 +141,18 @@ export interface Paging<E extends PagingKey = PagingKey> {
|
||||||
|
|
||||||
export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>;
|
export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>;
|
||||||
|
|
||||||
|
type Item = Endpoints[E]["res"][number];
|
||||||
|
type Param = Endpoints[E]["req"] | Record<string, never>;
|
||||||
|
|
||||||
const SECOND_FETCH_LIMIT_DEFAULT = 30;
|
const SECOND_FETCH_LIMIT_DEFAULT = 30;
|
||||||
|
const FIRST_FETCH_LIMIT_DEFAULT = 10;
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
pagination: Paging<E>;
|
pagination: Paging<E>;
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
|
folder?: (i: Item[]) => Fold[];
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
displayLimit: 30,
|
displayLimit: 30,
|
||||||
|
@ -156,7 +160,7 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const slots = defineSlots<{
|
const slots = defineSlots<{
|
||||||
default(props: { items: Item[] }): unknown;
|
default(props: { items: Item[]; foldedItems: Fold[] }): unknown;
|
||||||
empty(props: Record<string, never>): never;
|
empty(props: Record<string, never>): never;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -165,13 +169,59 @@ const emit = defineEmits<{
|
||||||
(ev: "status", hasError: boolean): void;
|
(ev: "status", hasError: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type Param = Endpoints[E]["req"] | Record<string, never>;
|
|
||||||
type Item = Endpoints[E]["res"][number];
|
|
||||||
|
|
||||||
const rootEl = ref<HTMLElement>();
|
const rootEl = ref<HTMLElement>();
|
||||||
const items = ref<Item[]>([]);
|
const items = ref<Item[]>([]);
|
||||||
|
const foldedItems = ref([]) as Ref<Fold[]>;
|
||||||
|
|
||||||
|
// To improve performance, we do not use vue’s `computed` here
|
||||||
|
function calculateItems() {
|
||||||
|
function getItems<T>(folder: (ns: Item[]) => T[]) {
|
||||||
|
const res = [
|
||||||
|
folder(prepended.value.toReversed()),
|
||||||
|
...arrItems.value.map((arr) => folder(arr)),
|
||||||
|
folder(appended.value),
|
||||||
|
].flat(1);
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
res.reverse();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
items.value = getItems((x) => x);
|
||||||
|
if (props.folder) foldedItems.value = getItems(props.folder);
|
||||||
|
}
|
||||||
|
|
||||||
const queue = ref<Item[]>([]);
|
const queue = ref<Item[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cached elements inserted front by `prepend` function
|
||||||
|
*/
|
||||||
|
const prepended = ref<Item[]>([]);
|
||||||
|
/**
|
||||||
|
* The array of "frozen" items
|
||||||
|
*/
|
||||||
|
const arrItems = ref<Item[][]>([]);
|
||||||
|
/**
|
||||||
|
* The cached elements inserted back by `append` function
|
||||||
|
*/
|
||||||
|
const appended = ref<Item[]>([]);
|
||||||
|
|
||||||
|
const idMap = new Map<string, boolean>();
|
||||||
|
|
||||||
const offset = ref(0);
|
const offset = ref(0);
|
||||||
|
|
||||||
|
type PagingByParam =
|
||||||
|
| {
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
sinceId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
untilId: string;
|
||||||
|
}
|
||||||
|
| Record<string, never>;
|
||||||
|
let nextPagingBy: PagingByParam = {};
|
||||||
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const moreFetching = ref(false);
|
const moreFetching = ref(false);
|
||||||
const more = ref(false);
|
const more = ref(false);
|
||||||
|
@ -184,54 +234,14 @@ const init = async (): Promise<void> => {
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
await fetch(true);
|
||||||
await os
|
|
||||||
.api(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: props.pagination.noPaging
|
|
||||||
? props.pagination.limit || 10
|
|
||||||
: (props.pagination.limit || 10) + 1,
|
|
||||||
...(props.pagination.ascending
|
|
||||||
? {
|
|
||||||
// An initial value smaller than all possible ids must be filled in here.
|
|
||||||
sinceId: "0",
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
(res: Item[]) => {
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
|
||||||
const item = res[i];
|
|
||||||
if (props.pagination.reversed) {
|
|
||||||
if (i === res.length - 2) item._shouldInsertAd_ = true;
|
|
||||||
} else {
|
|
||||||
if (i === 3) item._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!props.pagination.noPaging &&
|
|
||||||
res.length > (props.pagination.limit || 10)
|
|
||||||
) {
|
|
||||||
res.pop();
|
|
||||||
items.value = props.pagination.reversed ? res.toReversed() : res;
|
|
||||||
more.value = true;
|
|
||||||
} else {
|
|
||||||
items.value = props.pagination.reversed ? res.toReversed() : res;
|
|
||||||
more.value = false;
|
|
||||||
}
|
|
||||||
offset.value = res.length;
|
|
||||||
error.value = false;
|
|
||||||
fetching.value = false;
|
|
||||||
},
|
|
||||||
(_err) => {
|
|
||||||
error.value = true;
|
|
||||||
fetching.value = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const reload = (): Promise<void> => {
|
const reload = (): Promise<void> => {
|
||||||
items.value = [];
|
arrItems.value = [];
|
||||||
|
appended.value = [];
|
||||||
|
prepended.value = [];
|
||||||
|
idMap.clear();
|
||||||
return init();
|
return init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,30 +250,18 @@ const refresh = async (): Promise<void> => {
|
||||||
await os
|
await os
|
||||||
.api(props.pagination.endpoint, {
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: items.value.length + 1,
|
limit: (items.value.length || foldedItems.value.length) + 1,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(res: Item[]) => {
|
(res: Item[]) => {
|
||||||
const ids = items.value.reduce(
|
appended.value = [];
|
||||||
(a, b) => {
|
prepended.value = [];
|
||||||
a[b.id] = true;
|
|
||||||
return a;
|
|
||||||
},
|
|
||||||
{} as Record<string, boolean>,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
// appended should be inserted into arrItems to fix the element position
|
||||||
const item = res[i];
|
arrItems.value = [res];
|
||||||
if (!updateItem(item.id, (_old) => item)) {
|
|
||||||
append(item);
|
|
||||||
}
|
|
||||||
delete ids[item.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id in ids) {
|
calculateItems();
|
||||||
removeItem((i) => i.id === id);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
|
@ -272,155 +270,145 @@ const refresh = async (): Promise<void> => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMore = async (): Promise<void> => {
|
async function fetch(firstFetching?: boolean) {
|
||||||
if (
|
let limit: number;
|
||||||
!more.value ||
|
|
||||||
fetching.value ||
|
if (firstFetching) {
|
||||||
moreFetching.value ||
|
limit = props.pagination.noPaging
|
||||||
items.value.length === 0
|
? props.pagination.limit || FIRST_FETCH_LIMIT_DEFAULT
|
||||||
)
|
: (props.pagination.limit || FIRST_FETCH_LIMIT_DEFAULT) + 1;
|
||||||
return;
|
|
||||||
moreFetching.value = true;
|
if (props.pagination.ascending) {
|
||||||
backed.value = true;
|
nextPagingBy = {
|
||||||
|
// An initial value smaller than all possible ids must be filled in here.
|
||||||
|
sinceId: "0",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!more.value ||
|
||||||
|
fetching.value ||
|
||||||
|
moreFetching.value ||
|
||||||
|
items.value.length === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
moreFetching.value = true;
|
||||||
|
backed.value = true;
|
||||||
|
|
||||||
|
limit =
|
||||||
|
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
||||||
|
|
||||||
await os
|
await os
|
||||||
.api(props.pagination.endpoint, {
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit:
|
limit,
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1,
|
...nextPagingBy,
|
||||||
...(props.pagination.offsetMode
|
|
||||||
? {
|
|
||||||
offset: offset.value,
|
|
||||||
}
|
|
||||||
: props.pagination.reversed
|
|
||||||
? {
|
|
||||||
sinceId: items.value[0].id,
|
|
||||||
}
|
|
||||||
: props.pagination.ascending
|
|
||||||
? {
|
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
untilId: items.value[items.value.length - 1].id,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(res: Item[]) => {
|
(res: Item[]) => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
if (!props.pagination.reversed)
|
||||||
const item = res[i];
|
for (let i = 0; i < res.length; i++) {
|
||||||
if (props.pagination.reversed) {
|
const item = res[i];
|
||||||
if (i === res.length - 9) item._shouldInsertAd_ = true;
|
if (props.pagination.reversed) {
|
||||||
} else {
|
if (i === res.length - (firstFetching ? 2 : 9))
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
item._shouldInsertAd_ = true;
|
||||||
|
} else {
|
||||||
|
if (i === (firstFetching ? 3 : 10)) item._shouldInsertAd_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!props.pagination.noPaging && res.length > limit - 1) {
|
||||||
if (
|
|
||||||
res.length >
|
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT)
|
|
||||||
) {
|
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
|
error.value = false;
|
||||||
|
fetching.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
|
|
||||||
|
const lastRes = res[res.length - 1];
|
||||||
|
|
||||||
|
if (props.pagination.offsetMode) {
|
||||||
|
nextPagingBy = {
|
||||||
|
offset: offset.value,
|
||||||
|
};
|
||||||
|
} else if (props.pagination.ascending) {
|
||||||
|
nextPagingBy = {
|
||||||
|
sinceId: lastRes?.id,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
nextPagingBy = {
|
||||||
|
untilId: lastRes?.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstFetching && props.folder != null) {
|
||||||
|
// In this way, prepended has some initial values for folding
|
||||||
|
prepended.value = res.toReversed();
|
||||||
|
} else {
|
||||||
|
// For ascending and offset modes, append and prepend may cause item duplication
|
||||||
|
// so they need to be filtered out.
|
||||||
|
if (props.pagination.offsetMode || props.pagination.ascending) {
|
||||||
|
for (const item of appended.value) {
|
||||||
|
idMap.set(item.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentially
|
||||||
|
res = res.filter((item) => {
|
||||||
|
if (idMap.has(item)) return false;
|
||||||
|
idMap.set(item, true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// appended should be inserted into arrItems to fix the element position
|
||||||
|
arrItems.value.push(appended.value);
|
||||||
|
arrItems.value.push(res);
|
||||||
|
appended.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateItems();
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
error.value = true;
|
||||||
|
fetching.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMore = async (): Promise<void> => {
|
||||||
|
await fetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMoreAhead = async (): Promise<void> => {
|
const fetchMoreAhead = async (): Promise<void> => {
|
||||||
if (
|
await fetch();
|
||||||
!more.value ||
|
|
||||||
fetching.value ||
|
|
||||||
moreFetching.value ||
|
|
||||||
items.value.length === 0
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
moreFetching.value = true;
|
|
||||||
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
|
||||||
await os
|
|
||||||
.api(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit:
|
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1,
|
|
||||||
...(props.pagination.offsetMode
|
|
||||||
? {
|
|
||||||
offset: offset.value,
|
|
||||||
}
|
|
||||||
: props.pagination.reversed
|
|
||||||
? {
|
|
||||||
untilId: items.value[0].id,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
(res: Item[]) => {
|
|
||||||
if (
|
|
||||||
res.length >
|
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT)
|
|
||||||
) {
|
|
||||||
res.pop();
|
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = true;
|
|
||||||
} else {
|
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = false;
|
|
||||||
}
|
|
||||||
offset.value += res.length;
|
|
||||||
moreFetching.value = false;
|
|
||||||
},
|
|
||||||
(_err) => {
|
|
||||||
moreFetching.value = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepend = (item: Item): void => {
|
const prepend = (...item: Item[]): void => {
|
||||||
|
// If there are too many prepended, merge them into arrItems
|
||||||
|
if (
|
||||||
|
prepended.value.length >
|
||||||
|
(props.pagination.secondFetchLimit || SECOND_FETCH_LIMIT_DEFAULT)
|
||||||
|
) {
|
||||||
|
arrItems.value.unshift(prepended.value.toReversed());
|
||||||
|
prepended.value = [];
|
||||||
|
// We don't need to calculate here because it won't cause any changes in items
|
||||||
|
}
|
||||||
|
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
if (rootEl.value) {
|
prepended.value.push(...item);
|
||||||
const container = getScrollContainer(rootEl.value);
|
calculateItems();
|
||||||
if (container == null) {
|
|
||||||
// TODO?
|
|
||||||
} else {
|
|
||||||
const pos = getScrollPosition(rootEl.value);
|
|
||||||
const viewHeight = container.clientHeight;
|
|
||||||
const height = container.scrollHeight;
|
|
||||||
const isBottom = pos + viewHeight > height - 32;
|
|
||||||
if (isBottom) {
|
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
|
||||||
if (items.value.length >= props.displayLimit) {
|
|
||||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
|
||||||
// items.value = items.value.slice(-props.displayLimit);
|
|
||||||
while (items.value.length >= props.displayLimit) {
|
|
||||||
items.value.shift();
|
|
||||||
}
|
|
||||||
more.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items.value.push(item);
|
|
||||||
// TODO
|
|
||||||
} else {
|
} else {
|
||||||
// 初回表示時はunshiftだけでOK
|
// When displaying for the first time, just do this is OK
|
||||||
if (!rootEl.value) {
|
if (!rootEl.value) {
|
||||||
items.value.unshift(item);
|
prepended.value.push(...item);
|
||||||
|
calculateItems();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,52 +417,63 @@ const prepend = (item: Item): void => {
|
||||||
(document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
(document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
||||||
|
|
||||||
if (isTop) {
|
if (isTop) {
|
||||||
// Prepend the item
|
prepended.value.push(...item);
|
||||||
items.value.unshift(item);
|
calculateItems();
|
||||||
|
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
|
||||||
if (items.value.length >= props.displayLimit) {
|
|
||||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
|
||||||
// this.items = items.value.slice(0, props.displayLimit);
|
|
||||||
while (items.value.length >= props.displayLimit) {
|
|
||||||
items.value.pop();
|
|
||||||
}
|
|
||||||
more.value = true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
queue.value.push(item);
|
queue.value.push(...item);
|
||||||
onScrollTop(rootEl.value, () => {
|
onScrollTop(rootEl.value, () => {
|
||||||
for (const queueItem of queue.value) {
|
prepend(...queue.value);
|
||||||
prepend(queueItem);
|
|
||||||
}
|
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const append = (item: Item): void => {
|
const append = (...items: Item[]): void => {
|
||||||
items.value.push(item);
|
appended.value.push(...items);
|
||||||
|
calculateItems();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _removeItem = (arr: Item[], finder: (item: Item) => boolean): boolean => {
|
||||||
|
const i = arr.findIndex(finder);
|
||||||
|
if (i === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.splice(i, 1);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _updateItem = (
|
||||||
|
arr: Item[],
|
||||||
|
id: Item["id"],
|
||||||
|
replacer: (old: Item) => Item,
|
||||||
|
): boolean => {
|
||||||
|
const i = arr.findIndex((item) => item.id === id);
|
||||||
|
if (i === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr[i] = replacer(arr[i]);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeItem = (finder: (item: Item) => boolean): boolean => {
|
const removeItem = (finder: (item: Item) => boolean): boolean => {
|
||||||
const i = items.value.findIndex(finder);
|
const res =
|
||||||
if (i === -1) {
|
_removeItem(prepended.value, finder) ||
|
||||||
return false;
|
_removeItem(appended.value, finder) ||
|
||||||
}
|
arrItems.value.filter((arr) => _removeItem(arr, finder)).length > 0;
|
||||||
|
calculateItems();
|
||||||
items.value.splice(i, 1);
|
return res;
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
|
const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
|
||||||
const i = items.value.findIndex((item) => item.id === id);
|
const res =
|
||||||
if (i === -1) {
|
_updateItem(prepended.value, id, replacer) ||
|
||||||
return false;
|
_updateItem(appended.value, id, replacer) ||
|
||||||
}
|
arrItems.value.filter((arr) => _updateItem(arr, id, replacer)).length > 0;
|
||||||
|
calculateItems();
|
||||||
items.value[i] = replacer(items.value[i]);
|
return res;
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.pagination.params && isRef<Param>(props.pagination.params)) {
|
if (props.pagination.params && isRef<Param>(props.pagination.params)) {
|
||||||
|
|
68
packages/client/src/components/MkQrCode.vue
Normal file
68
packages/client/src/components/MkQrCode.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||||
|
<div :class="$style.root">
|
||||||
|
<div :class="$style.title">
|
||||||
|
<QRCodeVue3
|
||||||
|
:value="qrCode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MkButton :class="$style.gotIt" primary full @click="gotIt()">{{
|
||||||
|
i18n.ts.gotIt
|
||||||
|
}}</MkButton>
|
||||||
|
</div>
|
||||||
|
</MkModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { shallowRef } from "vue";
|
||||||
|
import MkModal from "@/components/MkModal.vue";
|
||||||
|
import MkButton from "@/components/MkButton.vue";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import QRCodeVue3 from "qrcode-vue3";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
qrCode: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
|
const gotIt = () => {
|
||||||
|
modal.value.close();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
margin: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 32px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
|
> img {
|
||||||
|
border-radius: 10px;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gotIt {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -196,13 +196,26 @@ import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
note: entities.Note;
|
defineProps<{
|
||||||
parentId?: string;
|
note: entities.Note;
|
||||||
conversation?: entities.Note[];
|
parentId?: string;
|
||||||
detailed?: boolean;
|
conversation?: entities.Note[];
|
||||||
detailedView?: boolean;
|
detailed?: boolean;
|
||||||
}>();
|
detailedView?: boolean;
|
||||||
|
isLongJudger?: (note: entities.Note) => boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
isLongJudger: (note: entities.Note) => {
|
||||||
|
return (
|
||||||
|
note.text != null &&
|
||||||
|
(note.text.split("\n").length > 10 ||
|
||||||
|
note.text.length > 800 ||
|
||||||
|
note.files.length > 4)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: "push", v): void;
|
(ev: "push", v): void;
|
||||||
|
@ -216,10 +229,7 @@ const showMoreButton = ref<HTMLElement>();
|
||||||
const isLong =
|
const isLong =
|
||||||
!props.detailedView &&
|
!props.detailedView &&
|
||||||
props.note.cw == null &&
|
props.note.cw == null &&
|
||||||
((props.note.text != null &&
|
props.isLongJudger(props.note);
|
||||||
(props.note.text.split("\n").length > 10 ||
|
|
||||||
props.note.text.length > 800)) ||
|
|
||||||
props.note.files.length > 4);
|
|
||||||
const collapsed = ref(props.note.cw == null && isLong);
|
const collapsed = ref(props.note.cw == null && isLong);
|
||||||
const urls = props.note.text
|
const urls = props.note.text
|
||||||
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
|
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
|
||||||
|
|
|
@ -338,7 +338,7 @@ defineExpose({
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -2px 0;
|
inset: -2px 0;
|
||||||
border: 2px solid var(--accentDarken);
|
border-bottom: 2px solid var(--accentDarken);
|
||||||
mask: linear-gradient(
|
mask: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent,
|
transparent,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type { Component, MaybeRef, Ref } from "vue";
|
||||||
import { defineAsyncComponent, markRaw, ref } from "vue";
|
import { defineAsyncComponent, markRaw, ref } from "vue";
|
||||||
import { i18n } from "./i18n";
|
import { i18n } from "./i18n";
|
||||||
import MkDialog from "@/components/MkDialog.vue";
|
import MkDialog from "@/components/MkDialog.vue";
|
||||||
|
import MkQrCode from "@/components/MkQrCode.vue";
|
||||||
import MkPostFormDialog from "@/components/MkPostFormDialog.vue";
|
import MkPostFormDialog from "@/components/MkPostFormDialog.vue";
|
||||||
import MkToast from "@/components/MkToast.vue";
|
import MkToast from "@/components/MkToast.vue";
|
||||||
import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
|
import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
|
||||||
|
@ -1000,6 +1001,25 @@ export function post(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function displayQrCode(qrCode: string) {
|
||||||
|
(
|
||||||
|
await new Promise<(() => void) | undefined>((resolve) => {
|
||||||
|
let dispose: (() => void) | undefined;
|
||||||
|
popup(
|
||||||
|
MkQrCode,
|
||||||
|
{ qrCode },
|
||||||
|
{
|
||||||
|
closed: () => {
|
||||||
|
resolve(dispose);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).then((res) => {
|
||||||
|
dispose = res.dispose;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)?.();
|
||||||
|
}
|
||||||
|
|
||||||
export const deckGlobalEvents = new EventEmitter();
|
export const deckGlobalEvents = new EventEmitter();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
75
packages/client/src/pages/follow-me.vue
Normal file
75
packages/client/src/pages/follow-me.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="mk-follow-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { acct } from "firefish-js";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import { host as hostRaw } from "@/config";
|
||||||
|
import { isSignedIn, me } from "@/me";
|
||||||
|
import { waiting } from "@/os";
|
||||||
|
|
||||||
|
const acctUri = new URL(location.href).searchParams.get("acct");
|
||||||
|
if (acctUri == null) {
|
||||||
|
throw new Error("acct required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is already logged in, ask whether to follow using the current account.
|
||||||
|
if (isSignedIn(me)) {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: "question",
|
||||||
|
text: i18n.ts.useThisAccountConfirm,
|
||||||
|
});
|
||||||
|
|
||||||
|
// use the current account
|
||||||
|
if (!canceled) {
|
||||||
|
waiting();
|
||||||
|
window.location.href = `/authorize-follow?acct=${acctUri}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise ask the user what the other account ID is
|
||||||
|
const remoteAccountId = await os.inputText({
|
||||||
|
text: i18n.ts.inputAccountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the user do not want enter uri, the user will be redirected to the user page.
|
||||||
|
if (!remoteAccountId.result) {
|
||||||
|
waiting();
|
||||||
|
window.location.href = `/@${acctUri}`;
|
||||||
|
} else {
|
||||||
|
const remoteAcctInfo = acct.parse(remoteAccountId.result);
|
||||||
|
|
||||||
|
// If the user on this server, redirect directly
|
||||||
|
if (remoteAcctInfo.host === hostRaw || remoteAcctInfo.host === null) {
|
||||||
|
waiting();
|
||||||
|
window.location.href = `/authorize-follow?acct=${acctUri}`;
|
||||||
|
} else {
|
||||||
|
waiting();
|
||||||
|
// If not, find the interaction url through webfinger interface
|
||||||
|
fetch(
|
||||||
|
`https://${remoteAcctInfo.host}/.well-known/webfinger?resource=${remoteAcctInfo.username}@${remoteAcctInfo.host}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
const subscribeUri = data.links.find(
|
||||||
|
(link: { rel: string }) =>
|
||||||
|
link.rel === "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
).template;
|
||||||
|
window.location.href = subscribeUri.replace(
|
||||||
|
"{uri}",
|
||||||
|
acctUri.includes("@") ? acctUri : `${acctUri}@${hostRaw}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
// TODO: It would be better to provide more information, but the priority of
|
||||||
|
// waiting component is too high and the pop-up window will be blocked.
|
||||||
|
window.location.href = `/@${acctUri}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -325,6 +325,10 @@ export const routes: RouteDef[] = [
|
||||||
component: page(() => import("./pages/follow.vue")),
|
component: page(() => import("./pages/follow.vue")),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/follow-me",
|
||||||
|
component: page(() => import("./pages/follow-me.vue")),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/authorize_interaction",
|
path: "/authorize_interaction",
|
||||||
component: page(() => import("./pages/authorize_interaction.vue")),
|
component: page(() => import("./pages/authorize_interaction.vue")),
|
||||||
|
|
|
@ -20,7 +20,6 @@ interface FoldOption {
|
||||||
*/
|
*/
|
||||||
export function foldItems<ItemFolded, Item>(
|
export function foldItems<ItemFolded, Item>(
|
||||||
ns: Item[],
|
ns: Item[],
|
||||||
fetch_limit: number,
|
|
||||||
classfier: (n: Item, index: number) => string,
|
classfier: (n: Item, index: number) => string,
|
||||||
aggregator: (ns: Item[], key: string) => ItemFolded,
|
aggregator: (ns: Item[], key: string) => ItemFolded,
|
||||||
_options?: FoldOption,
|
_options?: FoldOption,
|
||||||
|
@ -30,55 +29,48 @@ export function foldItems<ItemFolded, Item>(
|
||||||
const options: FoldOption = _options ?? {};
|
const options: FoldOption = _options ?? {};
|
||||||
options.skipSingleElement ??= true;
|
options.skipSingleElement ??= true;
|
||||||
|
|
||||||
for (let i = 0; i < ns.length; i += fetch_limit) {
|
const toAppendKeys: string[] = [];
|
||||||
const toFold = ns.slice(i, i + fetch_limit);
|
const foldMap = new Map<string, Item[]>();
|
||||||
const toAppendKeys: string[] = [];
|
|
||||||
const foldMap = new Map<string, Item[]>();
|
|
||||||
|
|
||||||
for (const [index, n] of toFold.entries()) {
|
for (const [index, n] of ns.entries()) {
|
||||||
const key = classfier(n, index);
|
const key = classfier(n, index);
|
||||||
const arr = foldMap.get(key);
|
const arr = foldMap.get(key);
|
||||||
if (arr != null) {
|
if (arr != null) {
|
||||||
arr.push(n);
|
arr.push(n);
|
||||||
} else {
|
} else {
|
||||||
foldMap.set(key, [n]);
|
foldMap.set(key, [n]);
|
||||||
toAppendKeys.push(key);
|
toAppendKeys.push(key);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res = res.concat(
|
|
||||||
toAppendKeys.map((key) => {
|
|
||||||
const arr = foldMap.get(key)!;
|
|
||||||
if (arr?.length === 1 && options?.skipSingleElement) {
|
|
||||||
return arr[0];
|
|
||||||
}
|
|
||||||
return aggregator(arr, key);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res = toAppendKeys.map((key) => {
|
||||||
|
const arr = foldMap.get(key)!;
|
||||||
|
if (arr?.length === 1 && options?.skipSingleElement) {
|
||||||
|
return arr[0];
|
||||||
|
}
|
||||||
|
return aggregator(arr, key);
|
||||||
|
});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function foldNotifications(
|
export function foldNotifications(ns: entities.Notification[]) {
|
||||||
ns: entities.Notification[],
|
// By the implement of MkPagination, lastId is unique and is safe for key
|
||||||
fetch_limit: number,
|
const lastId = ns[ns.length - 1]?.id ?? "prepend";
|
||||||
) {
|
|
||||||
return foldItems(
|
return foldItems(
|
||||||
ns,
|
ns,
|
||||||
fetch_limit,
|
|
||||||
(n) => {
|
(n) => {
|
||||||
switch (n.type) {
|
switch (n.type) {
|
||||||
case "renote":
|
case "renote":
|
||||||
return `renote-of:${n.note.renote.id}`;
|
return `renote-${n.note.renote.id}`;
|
||||||
case "reaction":
|
case "reaction":
|
||||||
return `reaction:${n.reaction}:of:${n.note.id}`;
|
return `reaction-${n.reaction}-of-${n.note.id}`;
|
||||||
default: {
|
default: {
|
||||||
return `${n.id}`;
|
return `${n.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(ns) => {
|
(ns, key) => {
|
||||||
const represent = ns[0];
|
const represent = ns[0];
|
||||||
function check(
|
function check(
|
||||||
ns: entities.Notification[],
|
ns: entities.Notification[],
|
||||||
|
@ -94,6 +86,7 @@ export function foldNotifications(
|
||||||
userIds: ns.map((nn) => nn.userId),
|
userIds: ns.map((nn) => nn.userId),
|
||||||
users: ns.map((nn) => nn.user),
|
users: ns.map((nn) => nn.user),
|
||||||
notifications: ns!,
|
notifications: ns!,
|
||||||
|
id: `G-${lastId}-${key}`,
|
||||||
} as NotificationFolded;
|
} as NotificationFolded;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -255,6 +255,27 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
||||||
router.push(`/user-info/${user.id}`);
|
router.push(`/user-info/${user.id}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: `${icon("ph-share")}`,
|
||||||
|
text: i18n.ts.share,
|
||||||
|
type: "parent",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
icon: "ph-qr-code ph-bold ph-lg",
|
||||||
|
text: i18n.ts.getQrCode,
|
||||||
|
action: () => {
|
||||||
|
os.displayQrCode(`https://${host}/follow-me?acct=${user.username}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: `${icon("ph-hand-waving")}`,
|
||||||
|
text: i18n.ts.copyRemoteFollowUrl,
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(`https://${host}/follow-me?acct=${user.username}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: `${icon("ph-newspaper")}`,
|
icon: `${icon("ph-newspaper")}`,
|
||||||
text: i18n.ts._feeds.copyFeed,
|
text: i18n.ts._feeds.copyFeed,
|
||||||
|
@ -290,6 +311,15 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
||||||
os.post({ specified: user });
|
os.post({ specified: user });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
!isSignedIn(me)
|
||||||
|
? {
|
||||||
|
icon: `${icon("ph-hand-waving")}`,
|
||||||
|
text: i18n.ts.remoteFollow,
|
||||||
|
action: () => {
|
||||||
|
router.push(`/follow-me?acct=${user.username}`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
isSignedIn(me) && me.id !== user.id
|
isSignedIn(me) && me.id !== user.id
|
||||||
? {
|
? {
|
||||||
type: "link",
|
type: "link",
|
||||||
|
|
|
@ -4,6 +4,9 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||||
if (el == null) return null;
|
if (el == null) return null;
|
||||||
const overflow = window.getComputedStyle(el).getPropertyValue("overflow-y");
|
const overflow = window.getComputedStyle(el).getPropertyValue("overflow-y");
|
||||||
if (overflow === "scroll" || overflow === "auto") {
|
if (overflow === "scroll" || overflow === "auto") {
|
||||||
|
if (el.tagName === "HTML") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return el;
|
return el;
|
||||||
} else {
|
} else {
|
||||||
return getScrollContainer(el.parentElement);
|
return getScrollContainer(el.parentElement);
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "0.3.12",
|
"@swc/cli": "0.3.12",
|
||||||
"@swc/core": "1.4.13",
|
"@swc/core": "1.5.0",
|
||||||
"@swc/types": "^0.1.6",
|
"@swc/types": "^0.1.6",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.12.7",
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"firefish-js": "workspace:*",
|
"firefish-js": "workspace:*",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"vite": "5.2.8",
|
"vite": "5.2.10",
|
||||||
"vite-plugin-compression": "^0.5.1"
|
"vite-plugin-compression": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
700
pnpm-lock.yaml
700
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue