diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6fa2cf4f..b1cd253a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,52 @@ --> +## 13.x.x (unreleased) + +### General +- カスタム絵文字関連の変更 + * ノートなどに含まれるemojis(populateEmojiの結果)は(プロキシされたURLではなく)オリジナルのURLを指すように + * MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように + +### Client +- + +### Server +- ノート作成時のアンテナ追加パフォーマンスを改善 +- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io + +## 13.11.2 + +### Note +- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照) + +### General +- チャンネルの検索用ページの追加 + +### Client +- 常に広告を見られるオプションを追加 +- ユーザーページの画像一覧が表示されない問題を修正 +- webhook, 連携アプリ一覧でコンテンツが重複して表示される問題を修正 +- iPhoneで絵文字ピッカーの表示が崩れる問題を修正 +- iPhoneでウィジェットドロワーの「ウィジェットを編集」が押しにくい問題を修正 +- 投稿フォームのデザインを調整 +- ギャラリーの人気の投稿が無限にページングされる問題を修正 + +### Server +- channels/search Endpoint APIの追加 +- APIパラメータサイズ上限を32kbから1mbに緩和 +- プッシュ通知送信時のパフォーマンスを改善 +- ローカルのカスタム絵文字のキャッシュが効いていなかった問題を修正 +- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正 +- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正 + +### Service Worker +- 「通知が既読になったらプッシュ通知を削除する」を復活 + * 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように) +- プッシュ通知が実績を解除 (achievementEarned) に対応 +- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正 +- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように + ## 13.11.1 ### General diff --git a/locales/de-DE.yml b/locales/de-DE.yml index ef1cea6e72..7cba849a79 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -991,6 +991,7 @@ largeNoteReactions: "Reaktionen vergrößert anzeigen" noteIdOrUrl: "Notiz-ID oder URL" accountMigration: "Konto-Umzug" accountMoved: "Dieser Benutzer ist zu einem neuen Konto umgezogen:" +forceShowAds: "Werbung immer anzeigen" _accountMigration: moveTo: "Dieses Konto zu einem neuen umziehen" moveToLabel: "Umzugsziel:" diff --git a/locales/en-US.yml b/locales/en-US.yml index 1137199739..9cfb3d5225 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -67,7 +67,7 @@ import: "Import" export: "Export" files: "Files" download: "Download" -driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted." +driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? It will also vanish from all contents that use it." unfollowConfirm: "Are you sure you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." @@ -500,7 +500,7 @@ objectStoragePrefixDesc: "Files will be stored under directories with this prefi objectStorageEndpoint: "Endpoint" objectStorageEndpointDesc: "Leave this empty if you are using AWS S3, otherwise specify the endpoint as '' or ':', depending on the service you are using." objectStorageRegion: "Region" -objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, leave this blank or enter 'us-east-1'." +objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, enter 'us-east-1'. Leave empty if using AWS configuration files or environment variables." objectStorageUseSSL: "Use SSL" objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for API connections" objectStorageUseProxy: "Connect over Proxy" @@ -918,7 +918,7 @@ unsubscribePushNotification: "Disable push notifications" pushNotificationAlreadySubscribed: "Push notifications are already enabled" pushNotificationNotSupported: "Your browser or instance does not support push notifications" sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read" -sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable." +sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the power consumption of your device." windowMaximize: "Maximize" windowMinimize: "Minimize" windowRestore: "Restore" @@ -991,6 +991,7 @@ largeNoteReactions: "Enlargen displayed reactions" noteIdOrUrl: "Note ID or URL" accountMigration: "Account Migration" accountMoved: "This user has moved to a new account:" +forceShowAds: "Always show ads" _accountMigration: moveTo: "Migrate this account to a different one" moveToLabel: "Account to move to:" @@ -1868,7 +1869,7 @@ _deck: swapRight: "Swap with the right column" swapUp: "Swap with the above column" swapDown: "Swap with the below column" - stackLeft: "Stack with the left column" + stackLeft: "Stack on left column" popRight: "Pop column to the right" profile: "Profile" newProfile: "New profile" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 1272da9b3e..97c0b02fe5 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -122,6 +122,8 @@ unmarkAsSensitive: "Hapus tanda konten sensitif" enterFileName: "Masukkan nama berkas" mute: "Bisukan" unmute: "Hapus bisukan" +renoteMute: "Matikan renote" +renoteUnmute: "Batal mematikan renote" block: "Blokir" unblock: "Buka blokir" suspend: "Bekukan" @@ -393,11 +395,15 @@ about: "Informasi" aboutMisskey: "Tentang Misskey" administrator: "Admin" token: "Token" +totp: "Aplikasi autentikator" +totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai" moderator: "Moderator" moderation: "Moderasi" nUsersMentioned: "{n} pengguna disebut" +securityKeyAndPasskey: "Security key dan passkey" securityKey: "Kunci keamanan" lastUsed: "Terakhir digunakan" +lastUsedAt: "Penggunaan terakhir: {t}" unregister: "Batalkan pendaftaran" passwordLessLogin: "Setel login tanpa kata sandi" resetPassword: "Atur ulang kata sandi" @@ -844,6 +850,7 @@ tenMinutes: "10 Menit" oneHour: "1 Jam" oneDay: "1 Hari" oneWeek: "1 Bulan" +oneMonth: "satu bulan" reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan." failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" rateLimitExceeded: "Batas sudah terlampaui" @@ -901,6 +908,7 @@ pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pember sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca" sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu." windowMaximize: "Maksimalkan" +windowMinimize: "Minimalkan" windowRestore: "Kembalikan" caption: "Keterangan" loggedInAsBot: "Sedang login sebagai bot" @@ -939,6 +947,12 @@ collapseRenotes: "Tutup renote yang sudah kamu lihat" internalServerError: "Kesalahan internal peladen" internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga" copyErrorInfo: "Salin detil galat" +joinThisServer: "Gabung server ini" +exploreOtherServers: "Cari server lain" +letsLookAtTimeline: "LIhat timeline" +disableFederationConfirm: "Matikan federasi?" +disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan." +disableFederationOk: "Matikan federasi" _achievements: earnedAt: "Terbuka pada" _types: diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b187fe2473..2383883cf8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -20,6 +20,7 @@ noNotes: "ノートはありません" noNotifications: "通知はありません" instance: "サーバー" settings: "設定" +notificationSettings: "通知の設定" basicSettings: "基本設定" otherSettings: "その他の設定" openInWindow: "ウィンドウで開く" @@ -920,8 +921,8 @@ subscribePushNotification: "プッシュ通知を有効化" unsubscribePushNotification: "プッシュ通知を停止する" pushNotificationAlreadySubscribed: "プッシュ通知は有効です" pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応" -sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する" -sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。" +sendPushNotificationReadMessage: "通知が既読になったらプッシュ通知を削除する" +sendPushNotificationReadMessageCaption: "端末の電池消費量が増加する可能性があります。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "元に戻す" @@ -994,6 +995,7 @@ largeNoteReactions: "ノートのリアクションを大きく表示" noteIdOrUrl: "ノートIDまたはURL" accountMigration: "アカウントの引っ越し" accountMoved: "このユーザーは新しいアカウントに引っ越しました:" +forceShowAds: "常に広告を表示する" _accountMigration: moveTo: "このアカウントを新しいアカウントに引っ越す" @@ -1429,6 +1431,8 @@ _channel: following: "フォロー中" usersCount: "{n}人が参加中" notesCount: "{n}投稿があります" + nameAndDescription: "名前と説明" + nameOnly: "名前のみ" _menuDisplay: sideFull: "横" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 81f99aace2..82cf17eaf1 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -991,6 +991,7 @@ largeNoteReactions: "ノートのリアクションを大きする" noteIdOrUrl: "ノートIDかURL" accountMigration: "アカウントのお引っ越し" accountMoved: "このユーザーはさらのアカウントに引っ越したで:" +forceShowAds: "常に広告を表示しとく" _accountMigration: moveTo: "このアカウントをさらのアカウントに引っ越すで" moveToLabel: "引っ越し先のアカウント:" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 9c1a48c67c..3f97b3982c 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -163,11 +163,15 @@ instanceInfo: "ອີນສະແຕນ" statistics: "ສະຖິຕິ" clearQueue: "ລ້າງຄິວ" clearCachedFiles: "ລຶບລ້າງແຄສ" +noUsers: "ບໍ່ພົບຜູ້ໃຊ້" editProfile: "ແກ້ໄຂໂປຣໄຟລ໌" done: "ສຳເລັດ" processing: "ກຳລັງປະມວນຜົນ" preview: "ສະແດງເປັນຕົວຢ່າງ" default: "ຄ່າເລີ່ມຕົ້ນ" +defaultValueIs: "ຄ່າເລີ່ມຕົ້ນ: {value}" +noCustomEmojis: "ບໍ່ມີອີໂມຈິ" +noJobs: "ບໍ່ມີຊິ້ນວຽກ" federating: "ສະຫະພັນ" blocked: "ບລັອກແລ້ວ " suspended: "ໂຈະ" @@ -182,6 +186,9 @@ changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ" security: "ຄວາມປອດໄພ" retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ" currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ" +newPassword: "ລະຫັດຜ່ານໃໝ່" +newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ" +attachFile: "ແນບໄຟລ໌" more: "ເພີ່ມເຕີມ!" featured: "ໄຮໄລທ໌" usernameOrUserId: "ຊື່ຜູ້ໃຊ້ ຫຼື id ຜູ້ໃຊ້" @@ -196,25 +203,31 @@ saved: "ບັນທຶກແລ້ວ" messaging: "ແຊ໋ດ" upload: "ອັບໂຫຼດ" keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ" +fromDrive: "ຈາກ Drive" fromUrl: "ຈາກ URL" uploadFromUrl: "ອັບໂຫຼດຈາກ URL" uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ" +uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ" messageRead: "ອ່ານແລ້ວ" startMessaging: "ເລີ່ມການສົນທະນາໃໝ່" nUsersRead: "ອ່ານໂດຍ {n}" tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ" start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ" home: "ໜ້າຫຼັກ" +activity: "ກິດຈະກຳ" images: "ຮູບພາບ" birthday: "ວັນເກີດ" yearsOld: "{age} ປີ" registeredDate: "ວັນທີ່ເປັນສະມາຊິກ" location: "ທີ່ຕັ້ງ" theme: "ແທ໋ມ" +themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ" +themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ" light: "ສະຫວ່າງ" dark: "ມືດ" lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ" darkThemes: "ຮູບແບບສີສັນມືດ" +syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ" drive: "ຂັບ" fileName: "ຊື່ໄຟລ໌" selectFile: "ເລືອກໄຟລ໌" @@ -265,6 +278,9 @@ invite: "ເຊີນ" driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ" driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ" pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້" +turnstileSiteKey: "ກະແຈໄຊທ໌" +turnstileSecretKey: "ກະແຈລັບ" +name: "ຊື່" userList: "ລາຍການ" about: "ກ່ຽວກັບ" aboutMisskey: "ກ່ຽວກັບ Misskey" @@ -326,6 +342,7 @@ _widgets: instanceInfo: "ອີນສະແຕນ" notifications: "ການແຈ້ງເຕືອນ" timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" + activity: "ກິດຈະກຳ" federation: "ສະຫະພັນ" _userList: chooseList: "ເລືອກບັນຊີລາຍການ" @@ -335,6 +352,7 @@ _visibility: home: "ໜ້າຫຼັກ" followers: "ຜູ້ຕິດຕາມ" _profile: + name: "ຊື່" username: "ຊື່ຜູ້ໃຊ້" _exportOrImport: followingList: "ກຳລັງຕິດຕາມ" @@ -368,3 +386,5 @@ _deck: list: "ລາຍການ" channel: "ຊ່ອງ" mentions: "ກ່າວເຖິງ" +_webhookSettings: + name: "ຊື່" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 8d4a41bf34..a863103e2c 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -122,6 +122,8 @@ unmarkAsSensitive: "ยกเลิกทำเครื่องหมายเ enterFileName: "พิมพ์ชื่อไฟล์" mute: "ปิดเสียง" unmute: "ยกเลิกการปิดเสียง" +renoteMute: "ปิดเสียงรีโน้ต" +renoteUnmute: "เปิดเสียง รีโน้ต" block: "บล็อค" unblock: "เลิกปิดกั้น" suspend: "ถูกระงับ" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์ flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้" autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม" addAccount: "เพิ่มบัญชี" +reloadAccountsList: "รีโหลดรายการบัญชีใหม่" loginFailed: "การเข้าสู่ระบบไม่สำเร็จ" showOnRemote: "ดูบนอินสแตนซ์ระยะไกล" general: "ทั่วไป" @@ -503,6 +506,7 @@ objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้ objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี" objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API" objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด" +s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ" serverLogs: "บันทึกของเซิร์ฟเวอร์" deleteAll: "ลบทั้งหมด" showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์" @@ -545,7 +549,9 @@ userSilenced: "ผู้ใช้รายนี้กำลังถูกป yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ" yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่" tokenRevoked: "โทเค็นไม่ถูกต้อง" +tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ" accountDeleted: "ลบบัญชีแล้ว" +accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ" menu: "เมนู" divider: "ตัวแบ่ง" addItem: "เพิ่มรายการ" @@ -914,6 +920,7 @@ pushNotificationNotSupported: "เบราว์เซอร์หรืออ sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว" sendPushNotificationReadMessageCaption: "การแจ้งเตือนที่มีข้อความ \"{emptyPushNotificationMessage}\" จะแสดงขึ้นมาในช่วงระยะเวลาสั้นๆ การดำเนินการนี้อาจทำให้เพิ่มการใช้งานแบตเตอรี่ของอุปกรณ์ถ้าหากมีนะ" windowMaximize: "ขยายใหญ่สุดแล้ว" +windowMinimize: "ย่อเล็กที่สุด" windowRestore: "เลิกทำ" caption: "รายละเอียด" loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้" @@ -955,11 +962,17 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้" exploreOtherServers: "มองหาอินสแตนซ์อื่น" letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" +disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?" +disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ" +disableFederationOk: "ปิดการใช้งาน" invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ" emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" postToTheChannel: "โพสต์ลงช่อง" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" +reactionAcceptance: "การยอมรับรีแอคชั่น" likeOnly: "ที่ชอบเท่านั้น" +likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น" +rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน" resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" sensitiveWords: "คำที่ละเอียดอ่อน" sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" @@ -971,6 +984,22 @@ drivecleaner: "ทำความสะอาดไดรฟ์" retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง" retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?" retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ" +enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล" +enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล" +showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน" +largeNoteReactions: "ขยายรีแอคชั่นการแสดงผล" +noteIdOrUrl: "โน้ต ID หรือ URL" +accountMigration: "การโยกย้ายบัญชี" +accountMoved: "ผู้ใช้รายนี้ได้ย้ายไปยังบัญชีใหม่แล้ว:" +forceShowAds: "แสดงโฆษณาเสมอ" +_accountMigration: + moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง" + moveToLabel: "บัญชีที่จะย้ายไปที่:" + moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com" + moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" + moveFromLabel: "บัญชีที่จะย้ายจาก:" + moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com" + migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ" _achievements: earnedAt: "ได้รับเมื่อ" _types: @@ -1267,6 +1296,8 @@ _role: followersMoreThanOrEq: "จำนวนผู้ติดตามมากกว่าหรือเท่ากับ\n" followingLessThanOrEq: "จำนวนบัญชีต่อไปนี้คือ น้อยกว่าหรือเท่ากับ" followingMoreThanOrEq: "จำนวนบัญชีต่อไปนี้คือ มากกว่าหรือเท่ากับ" + notesLessThanOrEq: "จำนวนโพสต์น้อยกว่าเท่ากับ" + notesMoreThanOrEq: "จำนวนโพสต์มากกว่าเท่ากับ" and: "และ" or: "หรือ" not: "ไม่" @@ -1866,5 +1897,16 @@ _drivecleaner: orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย" orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก" _webhookSettings: + createWebhook: "สร้าง Webhook" name: "ชื่อ" + secret: "ความลับ" + events: "อีเว้นท์ Webhook" active: "เปิดใช้งาน" + _events: + follow: "เมื่อกำลังติดตามผู้ใช้" + followed: "เมื่อกำลังติดตามแล้ว" + note: "เมื่อกำลังโพสต์โน้ต" + reply: "เมื่อได้รับการตอบกลับ" + renote: "รีโน้ตแล้วเมื่อ" + reaction: "เมื่อได้รับรีแอคชั่น" + mention: "เมื่อกำลังถูกกล่าวถึง" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index d9df760186..1843237a46 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -991,6 +991,7 @@ largeNoteReactions: "使用大图标来显示回应" noteIdOrUrl: "帖子ID或URL" accountMigration: "账户迁移" accountMoved: "此用户已迁移账户" +forceShowAds: "总是显示广告" _accountMigration: moveTo: "把这个账户迁移到新的账户" moveToLabel: "迁移后的账户" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 48934463d0..0069f1c4a7 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -985,9 +985,15 @@ showClipButtonInNoteFooter: "將摘錄添加至貼文" largeNoteReactions: "將貼文的反應放大顯示" noteIdOrUrl: "貼文ID或URL" accountMigration: "遷移帳戶" +forceShowAds: "總是顯示廣告" _accountMigration: moveTo: "將這個帳戶遷移至新的帳戶" - moveToLabel: "要遷移的帳戶:" + moveToLabel: "要遷移到的帳戶:" + moveAccountDescription: "這個操作不可撤銷。首先,請確認已在要遷移到的帳戶中為這個帳戶建立了一個別名。建立別名之後,像這樣輸入你要遷移到的帳戶:@person@instance.com" + moveFrom: "從其他帳戶遷移到這個帳戶" + moveFromLabel: "要遷移過來的帳戶:" + moveFromDescription: "如果你想把跟隨者從別的帳戶遷移過來,必須先在這裡建立別名。請務必在執行遷移之前建立別名!請像這樣輸入要遷移的帳戶:@person@instance.com" + migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。" _achievements: earnedAt: "獲得日期" _types: diff --git a/package.json b/package.json index 89733ea179..233a6a58c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.11.1", + "version": "13.11.2", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/assets/tabler-badges/medal.png b/packages/backend/assets/tabler-badges/medal.png new file mode 100644 index 0000000000..44dc7b8c1e Binary files /dev/null and b/packages/backend/assets/tabler-badges/medal.png differ diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index cb713b25ad..174d0d8beb 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -37,8 +37,24 @@ const $redis: Provider = { inject: [DI.config], }; -const $redisForPubsub: Provider = { - provide: DI.redisForPubsub, +const $redisForPub: Provider = { + provide: DI.redisForPub, + useFactory: (config) => { + const redis = new Redis({ + port: config.redisForPubsub.port, + host: config.redisForPubsub.host, + family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family, + password: config.redisForPubsub.pass, + keyPrefix: `${config.redisForPubsub.prefix}:`, + db: config.redisForPubsub.db ?? 0, + }); + return redis; + }, + inject: [DI.config], +}; + +const $redisForSub: Provider = { + provide: DI.redisForSub, useFactory: (config) => { const redis = new Redis({ port: config.redisForPubsub.port, @@ -57,14 +73,15 @@ const $redisForPubsub: Provider = { @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $redis, $redisForPubsub], - exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule], + providers: [$config, $db, $redis, $redisForPub, $redisForSub], + exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( @Inject(DI.db) private db: DataSource, @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.redisForPubsub) private redisForPubsub: Redis.Redis, + @Inject(DI.redisForPub) private redisForPub: Redis.Redis, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, ) {} async onApplicationShutdown(signal: string): Promise { @@ -79,7 +96,8 @@ export class GlobalModule implements OnApplicationShutdown { await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), - this.redisForPubsub.disconnect(), + this.redisForPub.disconnect(), + this.redisForSub.disconnect(), ]); } } diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 35266ac16d..166c78f479 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -27,8 +27,8 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.redisForPubsub) - private redisForPubsub: Redis.Redis, + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown { this.antennasFetched = false; this.antennas = []; - this.redisForPubsub.on('message', this.onRedisMessage); + this.redisForSub.on('message', this.onRedisMessage); } @bindThis public onApplicationShutdown(signal?: string | undefined) { - this.redisForPubsub.off('message', this.onRedisMessage); + this.redisForSub.off('message', this.onRedisMessage); } @bindThis @@ -91,14 +91,24 @@ export class AntennaService implements OnApplicationShutdown { } @bindThis - public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise { - this.redisClient.xadd( - `antennaTimeline:${antenna.id}`, - 'MAXLEN', '~', '200', - `${this.idService.parse(note.id).date.getTime()}-*`, - 'note', note.id); - - this.globalEventService.publishAntennaStream(antenna.id, 'note', note); + public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise { + const antennas = await this.getAntennas(); + const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); + const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); + + const redisPipeline = this.redisClient.pipeline(); + + for (const antenna of matchedAntennas) { + redisPipeline.xadd( + `antennaTimeline:${antenna.id}`, + 'MAXLEN', '~', '200', + '*', + 'note', note.id); + + this.globalEventService.publishAntennaStream(antenna.id, 'note', note); + } + + redisPipeline.exec(); } // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index d74f3e8788..561face5c3 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -27,8 +27,8 @@ export class CacheService implements OnApplicationShutdown { @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.redisForPubsub) - private redisForPubsub: Redis.Redis, + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -116,7 +116,7 @@ export class CacheService implements OnApplicationShutdown { fromRedisConverter: (value) => new Set(JSON.parse(value)), }); - this.redisForPubsub.on('message', this.onMessage); + this.redisForSub.on('message', this.onMessage); } @bindThis @@ -167,6 +167,6 @@ export class CacheService implements OnApplicationShutdown { @bindThis public onApplicationShutdown(signal?: string | undefined) { - this.redisForPubsub.off('message', this.onMessage); + this.redisForSub.off('message', this.onMessage); } } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index dc365986fe..416c3de5a8 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -43,12 +43,8 @@ export class CustomEmojiService { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), - toRedisConverter: (value) => JSON.stringify(value.values()), - fromRedisConverter: (value) => { - // 原因不明だが配列以外が入ってくることがあるため - if (!Array.isArray(JSON.parse(value))) return undefined; - return new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])); - }, // TODO: Date型の変換 + toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), + fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換 }); } @@ -271,16 +267,7 @@ export class CustomEmojiService { const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull); if (emoji == null) return null; - - const isLocal = emoji.host == null; - const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - const url = isLocal - ? emojiUrl - : this.config.proxyRemoteFiles - ? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}` - : emojiUrl; - - return url; + return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) } /** diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 25c064a2b4..9f4de5f985 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -26,8 +26,8 @@ export class GlobalEventService { @Inject(DI.config) private config: Config, - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForPub) + private redisForPub: Redis.Redis, ) { } @@ -37,7 +37,7 @@ export class GlobalEventService { { type: type, body: null } : { type: type, body: value }; - this.redisClient.publish(this.config.host, JSON.stringify({ + this.redisForPub.publish(this.config.host, JSON.stringify({ channel: channel, message: message, })); diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index 2b6160c82e..1322927c2c 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown { private intervalId: NodeJS.Timer; constructor( - @Inject(DI.redisForPubsub) - private redisForPubsub: Redis.Redis, + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.db) private db: DataSource, @@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown { }, 1000 * 60 * 5); } - this.redisForPubsub.on('message', this.onMessage); + this.redisForSub.on('message', this.onMessage); } @bindThis @@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown { @bindThis public onApplicationShutdown(signal?: string | undefined) { clearInterval(this.intervalId); - this.redisForPubsub.off('message', this.onMessage); + this.redisForSub.off('message', this.onMessage); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 5c4d13f178..32e4fe7f8a 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -329,7 +329,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.redisClient.xadd( `channelTimeline:${data.channel.id}`, 'MAXLEN', '~', '1000', - `${this.idService.parse(note.id).date.getTime()}-*`, + '*', 'note', note.id); } @@ -493,14 +493,7 @@ export class NoteCreateService implements OnApplicationShutdown { } }); - // Antenna - for (const antenna of (await this.antennaService.getAntennas())) { - this.antennaService.checkHitAntenna(antenna, note, user).then(hit => { - if (hit) { - this.antennaService.addNoteToAntenna(antenna, note, user); - } - }); - } + this.antennaService.addNoteToAntennas(note, user); if (data.reply) { this.saveReply(data.reply, note); diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index c44dddea41..6691c42836 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -66,6 +66,7 @@ export class NotificationService implements OnApplicationShutdown { @bindThis private postReadAllNotifications(userId: User['id']) { this.globalEventService.publishMainStream(userId, 'readAllNotifications'); + this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined); } @bindThis @@ -99,7 +100,7 @@ export class NotificationService implements OnApplicationShutdown { const redisIdPromise = this.redisClient.xadd( `notificationTimeline:${notifieeId}`, 'MAXLEN', '~', '300', - `${this.idService.parse(notification.id).date.getTime()}-*`, + '*', 'data', JSON.stringify(notification)); const packed = await this.notificationEntityService.pack(notification, notifieeId, {}); diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 69020f7e84..9b44cf6413 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,12 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import push from 'web-push'; +import Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import type { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; +import { RedisKVCache } from '@/misc/cache.js'; // Defined also packages/sw/types.ts#L13 type PushNotificationsTypes = { @@ -15,6 +17,7 @@ type PushNotificationsTypes = { antenna: { id: string, name: string }; note: Packed<'Note'>; }; + 'readAllNotifications': undefined; }; // Reduce length because push message servers have character limits @@ -40,15 +43,27 @@ function truncateBody(type: T, body: Pus @Injectable() export class PushNotificationService { + private subscriptionsCache: RedisKVCache; + constructor( @Inject(DI.config) private config: Config, + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, private metaService: MetaService, ) { + this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { + lifetime: 1000 * 60 * 60 * 1, // 1h + memoryCacheLifetime: 1000 * 60 * 3, // 3m + fetcher: (key) => this.swSubscriptionsRepository.findBy({ userId: key }), + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), + }); } @bindThis @@ -62,12 +77,13 @@ export class PushNotificationService { meta.swPublicKey, meta.swPrivateKey); - // Fetch - const subscriptions = await this.swSubscriptionsRepository.findBy({ - userId: userId, - }); + const subscriptions = await this.subscriptionsCache.fetch(userId); for (const subscription of subscriptions) { + if ([ + 'readAllNotifications', + ].includes(type) && !subscription.sendReadMessage) continue; + const pushSubscription = { endpoint: subscription.endpoint, keys: { diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 8733a7d7eb..bac85d7a15 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -3,7 +3,7 @@ import Bull from 'bull'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Provider } from '@nestjs/common'; -import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from '../queue/types.js'; +import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js'; function q(config: Config, name: string, limitPerSec = -1) { return new Bull(name, { @@ -41,7 +41,8 @@ export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; -export type DbQueue = Bull.Queue; +export type DbQueue = Bull.Queue>; +export type RelationshipQueue = Bull.Queue; export type ObjectStorageQueue = Bull.Queue; export type WebhookDeliverQueue = Bull.Queue; @@ -75,6 +76,12 @@ const $db: Provider = { inject: [DI.config], }; +const $relationship: Provider = { + provide: 'queue:relationship', + useFactory: (config: Config) => q(config, 'relationship'), + inject: [DI.config], +}; + const $objectStorage: Provider = { provide: 'queue:objectStorage', useFactory: (config: Config) => q(config, 'objectStorage'), @@ -96,6 +103,7 @@ const $webhookDeliver: Provider = { $deliver, $inbox, $db, + $relationship, $objectStorage, $webhookDeliver, ], @@ -105,6 +113,7 @@ const $webhookDeliver: Provider = { $deliver, $inbox, $db, + $relationship, $objectStorage, $webhookDeliver, ], diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 498ceced7a..375ac49911 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -6,9 +6,10 @@ import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; -import type { ThinUser } from '../queue/types.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; +import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; +import Bull from 'bull'; @Injectable() export class QueueService { @@ -21,6 +22,7 @@ export class QueueService { @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, + @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) {} @@ -56,7 +58,7 @@ export class QueueService { activity: activity, signature, }; - + return this.inboxQueue.add(data, { attempts: this.config.inboxJobMaxAttempts ?? 8, timeout: 5 * 60 * 1000, // 5min @@ -71,7 +73,7 @@ export class QueueService { @bindThis public createDeleteDriveFilesJob(user: ThinUser) { return this.dbQueue.add('deleteDriveFiles', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -81,7 +83,7 @@ export class QueueService { @bindThis public createExportCustomEmojisJob(user: ThinUser) { return this.dbQueue.add('exportCustomEmojis', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -91,7 +93,7 @@ export class QueueService { @bindThis public createExportNotesJob(user: ThinUser) { return this.dbQueue.add('exportNotes', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -101,7 +103,7 @@ export class QueueService { @bindThis public createExportFavoritesJob(user: ThinUser) { return this.dbQueue.add('exportFavorites', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -111,7 +113,7 @@ export class QueueService { @bindThis public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) { return this.dbQueue.add('exportFollowing', { - user: user, + user: { id: user.id }, excludeMuting, excludeInactive, }, { @@ -123,7 +125,7 @@ export class QueueService { @bindThis public createExportMuteJob(user: ThinUser) { return this.dbQueue.add('exportMuting', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -133,7 +135,7 @@ export class QueueService { @bindThis public createExportBlockingJob(user: ThinUser) { return this.dbQueue.add('exportBlocking', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -143,7 +145,7 @@ export class QueueService { @bindThis public createExportUserListsJob(user: ThinUser) { return this.dbQueue.add('exportUserLists', { - user: user, + user: { id: user.id }, }, { removeOnComplete: true, removeOnFail: true, @@ -153,7 +155,7 @@ export class QueueService { @bindThis public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importFollowing', { - user: user, + user: { id: user.id }, fileId: fileId, }, { removeOnComplete: true, @@ -161,10 +163,16 @@ export class QueueService { }); } + @bindThis + public createImportFollowingToDbJob(user: ThinUser, targets: string[]) { + const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel })); + return this.dbQueue.addBulk(jobs); + } + @bindThis public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importMuting', { - user: user, + user: { id: user.id }, fileId: fileId, }, { removeOnComplete: true, @@ -175,7 +183,7 @@ export class QueueService { @bindThis public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importBlocking', { - user: user, + user: { id: user.id }, fileId: fileId, }, { removeOnComplete: true, @@ -183,10 +191,32 @@ export class QueueService { }); } + @bindThis + public createImportBlockingToDbJob(user: ThinUser, targets: string[]) { + const jobs = targets.map(rel => this.generateToDbJobData('importBlockingToDb', { user, target: rel })); + return this.dbQueue.addBulk(jobs); + } + + @bindThis + private generateToDbJobData>(name: T, data: D): { + name: string, + data: D, + opts: Bull.JobOptions, + } { + return { + name, + data, + opts: { + removeOnComplete: true, + removeOnFail: true, + }, + }; + } + @bindThis public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importUserLists', { - user: user, + user: { id: user.id }, fileId: fileId, }, { removeOnComplete: true, @@ -197,7 +227,7 @@ export class QueueService { @bindThis public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importCustomEmojis', { - user: user, + user: { id: user.id }, fileId: fileId, }, { removeOnComplete: true, @@ -208,7 +238,7 @@ export class QueueService { @bindThis public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { return this.dbQueue.add('deleteAccount', { - user: user, + user: { id: user.id }, soft: opts.soft, }, { removeOnComplete: true, @@ -216,6 +246,51 @@ export class QueueService { }); } + @bindThis + public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) { + const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel)); + return this.relationshipQueue.addBulk(jobs); + } + + @bindThis + public createUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[]) { + const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel)); + return this.relationshipQueue.addBulk(jobs); + } + + @bindThis + public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) { + const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel)); + return this.relationshipQueue.addBulk(jobs); + } + + @bindThis + public createUnblockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) { + const jobs = blockings.map(rel => this.generateRelationshipJobData('unblock', rel)); + return this.relationshipQueue.addBulk(jobs); + } + + @bindThis + private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData): { + name: string, + data: RelationshipJobData, + opts: Bull.JobOptions, + } { + return { + name, + data: { + from: { id: data.from.id }, + to: { id: data.to.id }, + silent: data.silent, + requestId: data.requestId, + }, + opts: { + removeOnComplete: true, + removeOnFail: true, + }, + }; + } + @bindThis public createDeleteObjectStorageFileJob(key: string) { return this.objectStorageQueue.add('deleteFile', { @@ -246,7 +321,7 @@ export class QueueService { createdAt: Date.now(), eventId: uuid(), }; - + return this.webhookDeliverQueue.add(data, { attempts: 4, timeout: 1 * 60 * 1000, // 1min @@ -264,7 +339,7 @@ export class QueueService { //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); this.deliverQueue.clean(0, 'delayed'); - + this.inboxQueue.once('cleaned', (jobs, status) => { //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index c8ebe1adb7..77645e3f06 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -64,8 +64,8 @@ export class RoleService implements OnApplicationShutdown { public static NotAssignedError = class extends Error {}; constructor( - @Inject(DI.redisForPubsub) - private redisForPubsub: Redis.Redis, + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -87,7 +87,7 @@ export class RoleService implements OnApplicationShutdown { this.rolesCache = new MemorySingleCache(1000 * 60 * 60 * 1); this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 1); - this.redisForPubsub.on('message', this.onMessage); + this.redisForSub.on('message', this.onMessage); } @bindThis @@ -400,6 +400,6 @@ export class RoleService implements OnApplicationShutdown { @bindThis public onApplicationShutdown(signal?: string | undefined) { - this.redisForPubsub.off('message', this.onMessage); + this.redisForSub.off('message', this.onMessage); } } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index b3e306346e..3ca22f8bbc 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -24,7 +24,7 @@ export class UserBlockingService implements OnModuleInit { constructor( private moduleRef: ModuleRef, - + @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -54,12 +54,12 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async block(blocker: User, blockee: User) { + public async block(blocker: User, blockee: User, silent = false) { await Promise.all([ - this.cancelRequest(blocker, blockee), - this.cancelRequest(blockee, blocker), - this.userFollowingService.unfollow(blocker, blockee), - this.userFollowingService.unfollow(blockee, blocker), + this.cancelRequest(blocker, blockee, silent), + this.cancelRequest(blockee, blocker, silent), + this.userFollowingService.unfollow(blocker, blockee, silent), + this.userFollowingService.unfollow(blockee, blocker, silent), this.removeFromList(blockee, blocker), ]); @@ -89,7 +89,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - private async cancelRequest(follower: User, followee: User) { + private async cancelRequest(follower: User, followee: User, silent = false) { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, @@ -110,7 +110,7 @@ export class UserBlockingService implements OnModuleInit { }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } - if (this.userEntityService.isLocalUser(follower)) { + if (this.userEntityService.isLocalUser(follower) && !silent) { this.userEntityService.pack(followee, follower, { detail: true, }).then(async packed => { diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index d7bb8f3920..dacaa7263a 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -43,7 +43,7 @@ export class UserFollowingService implements OnModuleInit { constructor( private moduleRef: ModuleRef, - + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -79,7 +79,7 @@ export class UserFollowingService implements OnModuleInit { } @bindThis - public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise { + public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise { const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }), @@ -139,7 +139,7 @@ export class UserFollowingService implements OnModuleInit { } } - await this.insertFollowingDoc(followee, follower); + await this.insertFollowingDoc(followee, follower, silent); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); @@ -155,6 +155,7 @@ export class UserFollowingService implements OnModuleInit { follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }, + silent = false, ): Promise { if (follower.id === followee.id) return; @@ -233,7 +234,7 @@ export class UserFollowingService implements OnModuleInit { this.perUserFollowingChart.update(follower, followee, true); // Publish follow event - if (this.userEntityService.isLocalUser(follower)) { + if (this.userEntityService.isLocalUser(follower) && !silent) { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index bc726a1feb..08cc907ebf 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import { QueueService } from '@/core/QueueService.js'; @Injectable() export class UserListService { @@ -29,6 +30,7 @@ export class UserListService { private roleService: RoleService, private globalEventService: GlobalEventService, private proxyAccountService: ProxyAccountService, + private queueService: QueueService, ) { } @@ -47,14 +49,14 @@ export class UserListService { userId: target.id, userListId: list.id, } as UserListJoining); - + this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); - + // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする if (this.userEntityService.isRemoteUser(target)) { const proxy = await this.proxyAccountService.fetch(); if (proxy) { - this.userFollowingService.follow(proxy, target); + this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]); } } } diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 85594f8557..926115613b 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown { private webhooks: Webhook[] = []; constructor( - @Inject(DI.redisForPubsub) - private redisForPubsub: Redis.Redis, + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, ) { //this.onMessage = this.onMessage.bind(this); - this.redisForPubsub.on('message', this.onMessage); + this.redisForSub.on('message', this.onMessage); } @bindThis @@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown { @bindThis public onApplicationShutdown(signal?: string | undefined) { - this.redisForPubsub.off('message', this.onMessage); + this.redisForSub.off('message', this.onMessage); } } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 482e8f83e1..d4b1fb31b1 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -2,7 +2,8 @@ export const DI = { config: Symbol('config'), db: Symbol('db'), redis: Symbol('redis'), - redisForPubsub: Symbol('redisForPubsub'), + redisForPub: Symbol('redisForPub'), + redisForSub: Symbol('redisForSub'), //#region Repositories usersRepository: Symbol('usersRepository'), diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index a4abd4f878..d35414acf7 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -8,7 +8,7 @@ export class RedisKVCache { private memoryCache: MemoryKVCache; private fetcher: (key: string) => Promise; private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache + private fromRedisConverter: (value: string) => T; constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], opts: { lifetime: RedisKVCache['lifetime']; @@ -92,7 +92,7 @@ export class RedisSingleCache { private memoryCache: MemorySingleCache; private fetcher: () => Promise; private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache + private fromRedisConverter: (value: string) => T; constructor(redisClient: RedisSingleCache['redisClient'], name: RedisSingleCache['name'], opts: { lifetime: RedisSingleCache['lifetime']; diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts index 910e2bea45..233a36dd04 100644 --- a/packages/backend/src/queue/DbQueueProcessorsService.ts +++ b/packages/backend/src/queue/DbQueueProcessorsService.ts @@ -52,8 +52,10 @@ export class DbQueueProcessorsService { q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done)); q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done)); q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done)); + q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job)); q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done)); q.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done)); + q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job)); q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done)); q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done)); q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job)); diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 6a8f35cdda..4db9b38547 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -4,6 +4,7 @@ import { GlobalModule } from '@/GlobalModule.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DbQueueProcessorsService } from './DbQueueProcessorsService.js'; +import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js'; import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -32,6 +33,7 @@ import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessor import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; +import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; @Module({ imports: [ @@ -61,9 +63,11 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro DeleteAccountProcessorService, DeleteFileProcessorService, CleanRemoteFilesProcessorService, + RelationshipProcessorService, SystemQueueProcessorsService, ObjectStorageQueueProcessorsService, DbQueueProcessorsService, + RelationshipQueueProcessorsService, WebhookDeliverProcessorService, EndedPollNotificationProcessorService, DeliverProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 8457747cb0..706110f6fc 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; +import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js'; @Injectable() export class QueueProcessorService { @@ -27,6 +28,7 @@ export class QueueProcessorService { private systemQueueProcessorsService: SystemQueueProcessorsService, private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService, private dbQueueProcessorsService: DbQueueProcessorsService, + private relationshipQueueProcessorsService: RelationshipQueueProcessorsService, private webhookDeliverProcessorService: WebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private deliverProcessorService: DeliverProcessorService, @@ -52,14 +54,15 @@ export class QueueProcessorService { }; } } - + const systemLogger = this.logger.createSubLogger('system'); const deliverLogger = this.logger.createSubLogger('deliver'); const webhookLogger = this.logger.createSubLogger('webhook'); const inboxLogger = this.logger.createSubLogger('inbox'); const dbLogger = this.logger.createSubLogger('db'); + const relationshipLogger = this.logger.createSubLogger('relationship'); const objectStorageLogger = this.logger.createSubLogger('objectStorage'); - + this.queueService.systemQueue .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) @@ -67,7 +70,7 @@ export class QueueProcessorService { .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); - + this.queueService.deliverQueue .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) @@ -75,7 +78,7 @@ export class QueueProcessorService { .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); - + this.queueService.inboxQueue .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) @@ -83,7 +86,7 @@ export class QueueProcessorService { .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); - + this.queueService.dbQueue .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) @@ -91,7 +94,15 @@ export class QueueProcessorService { .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); - + + this.queueService.relationshipQueue + .on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`)) + .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`)); + this.queueService.objectStorageQueue .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) @@ -99,7 +110,7 @@ export class QueueProcessorService { .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); - + this.queueService.webhookDeliverQueue .on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) @@ -107,26 +118,27 @@ export class QueueProcessorService { .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); - + this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job)); this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job)); this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done)); this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job)); this.dbQueueProcessorsService.start(this.queueService.dbQueue); + this.relationshipQueueProcessorsService.start(this.queueService.relationshipQueue); this.objectStorageQueueProcessorsService.start(this.queueService.objectStorageQueue); - + this.queueService.systemQueue.add('tickCharts', { }, { repeat: { cron: '55 * * * *' }, removeOnComplete: true, }); - + this.queueService.systemQueue.add('resyncCharts', { }, { repeat: { cron: '0 0 * * *' }, removeOnComplete: true, }); - + this.queueService.systemQueue.add('cleanCharts', { }, { repeat: { cron: '0 0 * * *' }, @@ -138,19 +150,19 @@ export class QueueProcessorService { repeat: { cron: '0 0 * * *' }, removeOnComplete: true, }); - + this.queueService.systemQueue.add('clean', { }, { repeat: { cron: '0 0 * * *' }, removeOnComplete: true, }); - + this.queueService.systemQueue.add('checkExpiredMutings', { }, { repeat: { cron: '*/5 * * * *' }, removeOnComplete: true, }); - + this.systemQueueProcessorsService.start(this.queueService.systemQueue); } } diff --git a/packages/backend/src/queue/RelationshipQueueProcessorsService.ts b/packages/backend/src/queue/RelationshipQueueProcessorsService.ts new file mode 100644 index 0000000000..af086fa4e7 --- /dev/null +++ b/packages/backend/src/queue/RelationshipQueueProcessorsService.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; +import type Bull from 'bull'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; + +@Injectable() +export class RelationshipQueueProcessorsService { + constructor( + @Inject(DI.config) + private config: Config, + + private relationshipProcessorService: RelationshipProcessorService, + ) { + } + + @bindThis + public start(q: Bull.Queue): void { + const maxJobs = (this.config.deliverJobConcurrency ?? 128) / 4; // conservative? + q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job)); + q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job)); + q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job)); + q.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job)); + } +} diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index fa0c1733f6..604497cf54 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -7,7 +7,7 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -31,7 +31,7 @@ export class DeleteDriveFilesProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Deleting drive files of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index 7f2c2d08b5..a020006732 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -11,7 +11,7 @@ import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -36,7 +36,7 @@ export class ExportBlockingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting blocking of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index e9330772b9..daefcdf2f5 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -13,7 +13,7 @@ import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbJobDataWithUser } from '../types.js'; @Injectable() export class ExportFavoritesProcessorService { @@ -42,7 +42,7 @@ export class ExportFavoritesProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting favorites of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 064b126e44..59443de57f 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -12,7 +12,7 @@ import type { Following } from '@/models/entities/Following.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbExportFollowingData } from '../types.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -40,7 +40,7 @@ export class ExportFollowingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting following of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index 94c7ea8a46..a2a718b892 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -11,7 +11,7 @@ import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -39,7 +39,7 @@ export class ExportMutingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting muting of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 2f74dd63cc..1aa20d6f1d 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -13,7 +13,7 @@ import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbJobDataWithUser } from '../types.js'; @Injectable() export class ExportNotesProcessorService { @@ -39,7 +39,7 @@ export class ExportNotesProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting notes of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 6400161b8c..ce8ed2f5e8 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -11,7 +11,7 @@ import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserJobData } from '../types.js'; +import type { DbJobDataWithUser } from '../types.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -39,7 +39,7 @@ export class ExportUserListsProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job, done: () => void): Promise { this.logger.info(`Exporting user lists of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index b8d9b3a52d..3f075b02d2 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -1,38 +1,31 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, BlockingsRepository, DriveFilesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; -import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserImportJobData } from '../types.js'; +import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueService } from '@/core/QueueService.js'; @Injectable() export class ImportBlockingProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private queueService: QueueService, private utilityService: UtilityService, - private userBlockingService: UserBlockingService, private remoteUserResolveService: RemoteUserResolveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, @@ -59,46 +52,50 @@ export class ImportBlockingProcessorService { } const csv = await this.downloadService.downloadTextFile(file.url); + const targets = csv.trim().split('\n'); + this.queueService.createImportBlockingToDbJob({ id: user.id }, targets); - let linenum = 0; - - for (const line of csv.trim().split('\n')) { - linenum++; - - try { - const acct = line.split(',')[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) : await this.usersRepository.findOneBy({ - host: this.utilityService.toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await this.remoteUserResolveService.resolveUser(username, host); - } - - if (target == null) { - throw `cannot resolve user: @${username}@${host}`; - } - - // skip myself - if (target.id === job.data.user.id) continue; - - this.logger.info(`Block[${linenum}] ${target.id} ...`); - - await this.userBlockingService.block(user, target); - } catch (e) { - this.logger.warn(`Error in line:${linenum} ${e}`); - } - } - - this.logger.succ('Imported'); + this.logger.succ('Import jobs created'); done(); } + + @bindThis + public async processDb(job: Bull.Job): Promise { + const line = job.data.target; + const user = job.data.user; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = Acct.parse(acct); + + if (!host) return; + + let target = this.utilityService.isSelfHost(host) ? await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: username.toLowerCase(), + }) : await this.usersRepository.findOneBy({ + host: this.utilityService.toPuny(host), + usernameLower: username.toLowerCase(), + }); + + if (host == null && target == null) return; + + if (target == null) { + target = await this.remoteUserResolveService.resolveUser(username, host); + } + + if (target == null) { + throw `Unable to resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) return; + + this.logger.info(`Block ${target.id} ...`); + + this.queueService.createBlockJob([{ from: { id: user.id }, to: { id: target.id }, silent: true }]); + } catch (e) { + this.logger.warn(`Error: ${e}`); + } + } } diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 037a6f2456..aa5cf12c50 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -2,34 +2,30 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DbUserImportJobData } from '../types.js'; +import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; +import { QueueService } from '@/core/QueueService.js'; @Injectable() export class ImportFollowingProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + private queueService: QueueService, private utilityService: UtilityService, - private userFollowingService: UserFollowingService, private remoteUserResolveService: RemoteUserResolveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, @@ -56,46 +52,50 @@ export class ImportFollowingProcessorService { } const csv = await this.downloadService.downloadTextFile(file.url); + const targets = csv.trim().split('\n'); + this.queueService.createImportFollowingToDbJob({ id: user.id }, targets); - let linenum = 0; - - for (const line of csv.trim().split('\n')) { - linenum++; - - try { - const acct = line.split(',')[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) : await this.usersRepository.findOneBy({ - host: this.utilityService.toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await this.remoteUserResolveService.resolveUser(username, host); - } - - if (target == null) { - throw `cannot resolve user: @${username}@${host}`; - } - - // skip myself - if (target.id === job.data.user.id) continue; - - this.logger.info(`Follow[${linenum}] ${target.id} ...`); - - this.userFollowingService.follow(user, target); - } catch (e) { - this.logger.warn(`Error in line:${linenum} ${e}`); - } - } - - this.logger.succ('Imported'); + this.logger.succ('Import jobs created'); done(); } + + @bindThis + public async processDb(job: Bull.Job): Promise { + const line = job.data.target; + const user = job.data.user; + + try { + const acct = line.split(',')[0].trim(); + const { username, host } = Acct.parse(acct); + + if (!host) return; + + let target = this.utilityService.isSelfHost(host) ? await this.usersRepository.findOneBy({ + host: IsNull(), + usernameLower: username.toLowerCase(), + }) : await this.usersRepository.findOneBy({ + host: this.utilityService.toPuny(host), + usernameLower: username.toLowerCase(), + }); + + if (host == null && target == null) return; + + if (target == null) { + target = await this.remoteUserResolveService.resolveUser(username, host); + } + + if (target == null) { + throw `Unable to resolve user: @${username}@${host}`; + } + + // skip myself + if (target.id === job.data.user.id) return; + + this.logger.info(`Follow ${target.id} ...`); + + this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true }]); + } catch (e) { + this.logger.warn(`Error: ${e}`); + } + } } diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index 83d382057b..379994ee79 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -66,11 +66,13 @@ export class ImportMutingProcessorService { const acct = line.split(',')[0].trim(); const { username, host } = Acct.parse(acct); - let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ + if (!host) continue; + + let target = this.utilityService.isSelfHost(host) ? await this.usersRepository.findOneBy({ host: IsNull(), usernameLower: username.toLowerCase(), }) : await this.usersRepository.findOneBy({ - host: this.utilityService.toPuny(host!), + host: this.utilityService.toPuny(host), usernameLower: username.toLowerCase(), }); diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts new file mode 100644 index 0000000000..a5006dcf03 --- /dev/null +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type Bull from 'bull'; + +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { bindThis } from '@/decorators.js'; +import type Logger from '@/logger.js'; + +import { QueueLoggerService } from '../QueueLoggerService.js'; +import { RelationshipJobData } from '../types.js'; +import type { UsersRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; + +@Injectable() +export class RelationshipProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private queueLoggerService: QueueLoggerService, + private userFollowingService: UserFollowingService, + private userBlockingService: UserBlockingService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('follow-block'); + } + + @bindThis + public async processFollow(job: Bull.Job): Promise { + this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id}`); + await this.userFollowingService.follow(job.data.from, job.data.to, job.data.requestId, job.data.silent); + return 'ok'; + } + + @bindThis + public async processUnfollow(job: Bull.Job): Promise { + this.logger.info(`${job.data.from.id} is trying to unfollow ${job.data.to.id}`); + const [follower, followee] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: job.data.from.id }), + this.usersRepository.findOneByOrFail({ id: job.data.to.id }), + ]); + await this.userFollowingService.unfollow(follower, followee, job.data.silent); + return 'ok'; + } + + @bindThis + public async processBlock(job: Bull.Job): Promise { + this.logger.info(`${job.data.from.id} is trying to block ${job.data.to.id}`); + const [blockee, blocker] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: job.data.from.id }), + this.usersRepository.findOneByOrFail({ id: job.data.to.id }), + ]); + await this.userBlockingService.block(blockee, blocker, job.data.silent); + return 'ok'; + } + + @bindThis + public async processUnblock(job: Bull.Job): Promise { + this.logger.info(`${job.data.from.id} is trying to unblock ${job.data.to.id}`); + const [blockee, blocker] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: job.data.from.id }), + this.usersRepository.findOneByOrFail({ id: job.data.to.id }), + ]); + await this.userBlockingService.unblock(blockee, blocker); + return 'ok'; + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 5d650c6864..23c973d449 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -21,9 +21,39 @@ export type InboxJobData = { signature: httpSignature.IParsedSignature; }; -export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobData; +export type RelationshipJobData = { + from: ThinUser; + to: ThinUser; + silent?: boolean; + requestId?: string; +} -export type DbUserJobData = { +export type DbJobData = DbJobMap[T]; + +export type DbJobMap = { + deleteDriveFiles: DbJobDataWithUser; + exportCustomEmojis: DbJobDataWithUser; + exportNotes: DbJobDataWithUser; + exportFavorites: DbJobDataWithUser; + exportFollowing: DbExportFollowingData; + exportMuting: DbJobDataWithUser; + exportBlocking: DbJobDataWithUser; + exportUserLists: DbJobDataWithUser; + importFollowing: DbUserImportJobData; + importFollowingToDb: DbUserImportToDbJobData; + importMuting: DbUserImportJobData; + importBlocking: DbUserImportJobData; + importBlockingToDb: DbUserImportToDbJobData; + importUserLists: DbUserImportJobData; + importCustomEmojis: DbUserImportJobData; + deleteAccount: DbUserDeleteJobData; +} + +export type DbJobDataWithUser = { + user: ThinUser; +} + +export type DbExportFollowingData = { user: ThinUser; excludeMuting: boolean; excludeInactive: boolean; @@ -39,6 +69,11 @@ export type DbUserImportJobData = { fileId: DriveFile['id']; }; +export type DbUserImportToDbJobData = { + user: ThinUser; + target: string; +}; + export type ObjectStorageJobData = ObjectStorageFileJobData | Record; export type ObjectStorageFileJobData = { diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index b806ad5ca3..d3b1c7786d 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -89,7 +89,7 @@ export class ApiServerService { Params: { endpoint: string; }, Body: Record, Querystring: Record, - }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, async (request, reply) => { + }>('/' + endpoint.name, { bodyLimit: 1024 * 1024 }, async (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); reply.send(); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5a53b3faf7..ca89d82853 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -98,6 +98,7 @@ import * as ep___channels_update from './endpoints/channels/update.js'; import * as ep___channels_favorite from './endpoints/channels/favorite.js'; import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js'; import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js'; +import * as ep___channels_search from './endpoints/channels/search.js'; import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; import * as ep___charts_drive from './endpoints/charts/drive.js'; @@ -431,6 +432,7 @@ const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default }; const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default }; const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default }; +const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default }; const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default }; const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default }; const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default }; @@ -768,6 +770,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $channels_favorite, $channels_unfavorite, $channels_myFavorites, + $channels_search, $charts_activeUsers, $charts_apRequest, $charts_drive, @@ -1099,6 +1102,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $channels_favorite, $channels_unfavorite, $channels_myFavorites, + $channels_search, $charts_activeUsers, $charts_apRequest, $charts_drive, diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index e0e5b71a82..769a4490d6 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -22,8 +22,8 @@ export class StreamingApiServerService { @Inject(DI.config) private config: Config, - @Inject(DI.redisForPubsub) - private redisForPubsub: Redis.Redis, + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -81,7 +81,7 @@ export class StreamingApiServerService { ev.emit(parsed.channel, parsed.message); } - this.redisForPubsub.on('message', onRedisMessage); + this.redisForSub.on('message', onRedisMessage); const main = new MainStreamConnection( this.channelsService, @@ -111,7 +111,7 @@ export class StreamingApiServerService { connection.once('close', () => { ev.removeAllListeners(); main.dispose(); - this.redisForPubsub.off('message', onRedisMessage); + this.redisForSub.off('message', onRedisMessage); if (intervalId) clearInterval(intervalId); }); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index fd268c7912..dab897117d 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -98,6 +98,7 @@ import * as ep___channels_update from './endpoints/channels/update.js'; import * as ep___channels_favorite from './endpoints/channels/favorite.js'; import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js'; import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js'; +import * as ep___channels_search from './endpoints/channels/search.js'; import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; import * as ep___charts_drive from './endpoints/charts/drive.js'; @@ -429,6 +430,7 @@ const eps = [ ['channels/favorite', ep___channels_favorite], ['channels/unfavorite', ep___channels_unfavorite], ['channels/my-favorites', ep___channels_myFavorites], + ['channels/search', ep___channels_search], ['charts/active-users', ep___charts_activeUsers], ['charts/ap-request', ep___charts_apRequest], ['charts/drive', ep___charts_drive], diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index b073209a5b..54ce095488 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], @@ -29,7 +29,7 @@ export default class extends Endpoint { @Inject(DI.notesRepository) private followingsRepository: FollowingsRepository, - private userFollowingService: UserFollowingService, + private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { const followings = await this.followingsRepository.findBy({ @@ -41,9 +41,7 @@ export default class extends Endpoint { this.usersRepository.findOneByOrFail({ id: f.followeeId }), ]))); - for (const pair of pairs) { - this.userFollowingService.unfollow(pair[0], pair[1]); - } + this.queueService.createUnfollowJob(pairs.map(p => ({ to: p[0], from: p[1], silent: true }))); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 3c99225272..eabbceac0e 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,15 +1,15 @@ +import { IsNull, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { RelationshipJobData } from '@/queue/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], @@ -36,12 +36,10 @@ export default class extends Endpoint { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private userEntityService: UserEntityService, - private userFollowingService: UserFollowingService, private userSuspendService: UserSuspendService, private roleService: RoleService, private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, + private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -71,20 +69,23 @@ export default class extends Endpoint { @bindThis private async unFollowAll(follower: User) { - const followings = await this.followingsRepository.findBy({ - followerId: follower.id, + const followings = await this.followingsRepository.find({ + where: { + followerId: follower.id, + followeeId: Not(IsNull()), + }, }); - + + const jobs: RelationshipJobData[] = []; for (const following of followings) { - const followee = await this.usersRepository.findOneBy({ - id: following.followeeId, - }); - - if (followee == null) { - throw `Cant find followee ${following.followeeId}`; + if (following.followeeId && following.followerId) { + jobs.push({ + from: { id: following.followerId }, + to: { id: following.followeeId }, + silent: true, + }); } - - await this.userFollowingService.unfollow(follower, followee, true); } + this.queueService.createUnfollowJob(jobs); } } diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts new file mode 100644 index 0000000000..a954ba224c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import type { ChannelsRepository } from '@/models/index.js'; +import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; + +export const meta = { + tags: ['channels'], + + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Channel', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + query: { type: 'string' }, + type: { type: 'string', enum: ['nameAndDescription', 'nameOnly'], default: 'nameAndDescription' }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 }, + }, + required: ['query'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private channelEntityService: ChannelEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId); + + if (ps.type === 'nameAndDescription') { + query.andWhere(new Brackets(qb => { qb + .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) + .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); + })); + } else { + query.andWhere('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); + } + + const channels = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); + }); + } +} diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 836c5aae6c..09b0005ac1 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -54,10 +54,10 @@ class LocalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { + if (note.reply && this.user && !this.user.showTimelineReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 23b82a8ac5..5fd21cdf0a 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -77,6 +77,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi createdAt: '2016-12-28T22:49:51.000Z', description: 'I am a cool user!', ffVisibility: 'public', + roles: [], fields: [ { name: 'Website', diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index b3bbeeb51c..dd40bac2cc 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -398,6 +398,7 @@ function toStories(component: string): string { Promise.all([ glob('src/components/global/*.vue'), glob('src/components/MkGalleryPostPreview.vue'), + glob('src/pages/user/home.vue'), ]) .then((globs) => globs.flat()) .then((components) => Promise.all(components.map((component) => { diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue new file mode 100644 index 0000000000..408eab7399 --- /dev/null +++ b/packages/frontend/src/components/MkChannelList.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 1834224b8d..a6372b7b6f 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -82,6 +82,7 @@ export default defineComponent({ omitted: null, ignoreOmit: false, defaultStore, + i18n, }; }, mounted() { diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 0ae182ce32..9eaf16374b 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -439,7 +439,6 @@ defineExpose({ &.asDrawer { width: 100% !important; - padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; > .emojis { ::v-deep(section) { @@ -498,6 +497,10 @@ defineExpose({ background: transparent; color: var(--fg); + &:not(:focus):not(.filled) { + margin-bottom: env(safe-area-inset-bottom, 0px); + } + &:not(.filled) { order: 1; z-index: 2; diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 1576144f6b..5ca4c50518 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -31,7 +31,7 @@ import { onMounted } from 'vue'; import * as misskey from 'misskey-js'; import VuePlyr from 'vue-plyr'; -import { ColdDeviceStorage } from '@/store'; +import { soundConfigStore } from '@/scripts/sound'; import 'vue-plyr/dist/vue-plyr.css'; import { i18n } from '@/i18n'; @@ -44,11 +44,11 @@ const audioEl = $shallowRef(); let hide = $ref(true); function volumechange() { - if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume); + if (audioEl) soundConfigStore.set('mediaVolume', audioEl.volume); } onMounted(() => { - if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume'); + if (audioEl) audioEl.volume = soundConfigStore.state.mediaVolume; }); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 10cb7d96cc..42a3748d9a 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -1124,16 +1124,16 @@ defineExpose({ display: grid; grid-auto-flow: row; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); - grid-auto-rows: 46px; + grid-auto-rows: 40px; } .footerRight { - flex: 0.3; + flex: 0; margin-left: auto; display: grid; grid-auto-flow: row; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); - grid-auto-rows: 46px; + grid-auto-rows: 40px; direction: rtl; } @@ -1198,13 +1198,21 @@ defineExpose({ } } -@container (max-width: 330px) { +@container (max-width: 350px) { + .footer { + font-size: 0.9em; + } + + .footerLeft { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + } + + .footerRight { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + } + .headerRight { gap: 0; } - - .footer { - font-size: 14px; - } } diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 5799f99d5f..aa975600f0 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -83,7 +83,7 @@ const choseAd = (): Ad | null => { }; const chosen = ref(choseAd()); -const shouldHide = $ref($i && $i.policies.canHideAds && (props.specify == null)); +const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { if (chosen.value == null) return; diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 84aae1cff8..0cb31ffcba 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -5,7 +5,7 @@