diff --git a/CHANGELOG.md b/CHANGELOG.md index 36996e969..1c792b537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,22 @@ --> +## 13.10.3 (unreleased) + +### General +- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加 +- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に + +### Client +- クリップボタンをノートアクションに追加できるように +- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正 + +### Server +- リモートユーザーのチャート生成を無効にするオプションを追加 +- リモートサーバーのチャート生成を無効にするオプションを追加 +- ドライブのチャートはローカルユーザーのみ生成するように +- 空のアンテナが作成できるのを修正 + ## 13.10.2 ### Server diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 5254b20ef..c2910b90c 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب" pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات." notificationType: "أنواع الإشعارات" edit: "التعديل" -useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا" emailServer: "خادم البريد الإلكتروني" emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها." email: "البريد الإلكتروني " @@ -1275,3 +1274,7 @@ _deck: channel: "القنوات" mentions: "الإشارات" direct: "مباشرة" +_webhookSettings: + name: "الإسم" + active: "مفعّل" + diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 49b76b8ab..40af5a332 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে" notificationType: "বিজ্ঞপ্তির ধরন" edit: "সম্পাদনা" -useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন" emailServer: "ইমেইল সার্ভার" enableEmail: "ইমেইল বিতরণ চালু করুন" emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়" @@ -1354,3 +1353,7 @@ _deck: channel: "চ্যানেলগুলি" mentions: "উল্লেখসমূহ" direct: "ডাইরেক্ট নোটগুলি" +_webhookSettings: + name: "নাম" + active: "চালু" + diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 2b1168f78..bc9e66249 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -460,3 +460,4 @@ _deck: list: "Llistes" mentions: "Mencions" direct: "Publicacions directes" + diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 7f665895b..4b5919247 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -776,3 +776,7 @@ _deck: list: "Seznamy" channel: "Kanály" mentions: "Zmínění" +_webhookSettings: + name: "Jméno" + active: "Zapnuto" + diff --git a/locales/da-DK.yml b/locales/da-DK.yml index 08c15ed09..d1fbec9f6 100644 --- a/locales/da-DK.yml +++ b/locales/da-DK.yml @@ -1,2 +1,3 @@ --- _lang_: "Dansk" + diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 45e5fa59f..f6c28fbde 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -594,7 +594,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren" pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können." notificationType: "Art der Benachrichtigung" edit: "Bearbeiten" -useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist" emailServer: "Email-Server" enableEmail: "Email-Versand aktivieren" emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet" @@ -1277,6 +1276,8 @@ _role: followersMoreThanOrEq: "Hat X oder mehr Follower" followingLessThanOrEq: "Folgt X oder weniger Benutzern" followingMoreThanOrEq: "Folgt X oder mehr Benutzern" + notesLessThanOrEq: "Beitragszahl ist kleiner-gleich" + notesMoreThanOrEq: "Beitragszahl ist größer-gleich" and: "UND-Bedingung" or: "ODER-Bedingung" not: "NICHT-Bedingung" @@ -1875,3 +1876,7 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "Absteigende Dateigrößen" orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" +_webhookSettings: + name: "Name" + active: "Aktiviert" + diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 0721ba6e9..634e36c29 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -392,3 +392,6 @@ _deck: antenna: "Αντένες" list: "Λίστα" mentions: "Επισημάνσεις" +_webhookSettings: + name: "Όνομα" + diff --git a/locales/en-US.yml b/locales/en-US.yml index 423871975..6489919e8 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -594,7 +594,6 @@ tokenRequested: "Grant access to account" pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here." notificationType: "Notification type" edit: "Edit" -useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown" emailServer: "Email server" enableEmail: "Enable email distribution" emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password" @@ -981,6 +980,9 @@ drivecleaner: "Drive Cleaner" retryAllQueuesNow: "Retry running all queues" retryAllQueuesConfirmTitle: "Really retry all?" retryAllQueuesConfirmText: "This will temporarily increase the server load." +enableChartsForRemoteUser: "Generate remote user data charts" +enableChartsForFederatedInstances: "Generate remote instance data charts" +showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" _achievements: earnedAt: "Unlocked at" _types: @@ -1277,6 +1279,8 @@ _role: followersMoreThanOrEq: "Has X or more followers" followingLessThanOrEq: "Follows X or fewer accounts" followingMoreThanOrEq: "Follows X or more accounts" + notesLessThanOrEq: "Post count is less than/equal to" + notesMoreThanOrEq: "Post count is greater than/equal to" and: "AND-Condition" or: "OR-Condition" not: "NOT-Condition" @@ -1875,3 +1879,18 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "Descending Filesizes" orderByCreatedAtAsc: "Ascending Dates" +_webhookSettings: + createWebhook: "Create Webhook" + name: "Name" + secret: "Secret" + events: "Webhook Events" + active: "Enabled" + _events: + follow: "When following a user" + followed: "When being followed" + note: "When posting a note" + reply: "When receiving a reply" + renote: "When renoted" + reaction: "When receiving a reaction" + mention: "When being mentioned" + diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 2e31aa729..70327c5ea 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -594,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta" pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí" notificationType: "Tipo de notificación" edit: "Editar" -useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella" emailServer: "Servidor de correo" enableEmail: "Activar el envío de correos electrónicos" emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña" @@ -1875,3 +1874,7 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "Más grandes" orderByCreatedAtAsc: "Más antiguos" +_webhookSettings: + name: "Nombre" + active: "Activado" + diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index f9b8939e8..11573e0ce 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte" pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." notificationType: "Type de notifications" edit: "Editer" -useStarForReactionFallback: "Utiliser ★ comme alternative si l’émoji de réaction est inconnu" emailServer: "Serveur mail" enableEmail: "Activer la distribution de courriel" emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli." @@ -1468,3 +1467,7 @@ _deck: channel: "Canaux" mentions: "Mentions" direct: "Direct" +_webhookSettings: + name: "Nom" + active: "Activé" + diff --git a/locales/hr-HR.yml b/locales/hr-HR.yml index ed97d539c..cd21505a4 100644 --- a/locales/hr-HR.yml +++ b/locales/hr-HR.yml @@ -1 +1,2 @@ --- + diff --git a/locales/ht-HT.yml b/locales/ht-HT.yml index ed97d539c..cd21505a4 100644 --- a/locales/ht-HT.yml +++ b/locales/ht-HT.yml @@ -1 +1,2 @@ --- + diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 5d74cf538..e5a057477 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun" pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." notificationType: "Jenis pemberitahuan" edit: "Sunting" -useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui" emailServer: "Peladen surel" enableEmail: "Nyalakan distribusi surel" emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi" @@ -1804,3 +1803,7 @@ _deck: channel: "Kanal" mentions: "Sebutan" direct: "Langsung" +_webhookSettings: + name: "Nama" + active: "Aktif" + diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 87c9e5c6f..ddd1e5e90 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito" visibility: "Visibilità" poll: "Sondaggio" useCw: "Nascondere media" -enablePlayer: "Apri in lettore video" -disablePlayer: "Chiudi il lettore" +enablePlayer: "Visualizza" +disablePlayer: "Chiudi" expandTweet: "Espandi tweet" themeEditor: "Editor di temi" description: "Descrizione" @@ -594,7 +594,6 @@ tokenRequested: "Autorizza accesso al profilo" pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." notificationType: "Tipo di notifiche" edit: "Modifica" -useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa." emailServer: "Server email" enableEmail: "Abilita consegna email" emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password" @@ -978,6 +977,9 @@ license: "Licenza" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" myClips: "Le mie Clip" drivecleaner: "Drive cleaner" +retryAllQueuesNow: "Ritenta di consumare tutte le code" +retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?" +retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." _achievements: earnedAt: "Data di conseguimento" _types: @@ -1872,3 +1874,7 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "Dal più grande al più piccolo" orderByCreatedAtAsc: "Dal più vecchio al più recente" +_webhookSettings: + name: "Nome" + active: "Attivo" + diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2011ca363..cf4ede30b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -460,7 +460,7 @@ aboutX: "{x}について" emojiStyle: "絵文字のスタイル" native: "ネイティブ" disableDrawer: "メニューをドロワーで表示しない" -showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する" +showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" noHistory: "履歴はありません" signinHistory: "ログイン履歴" enableAdvancedMfm: "高度なMFMを有効にする" @@ -594,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許可" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。" notificationType: "通知の種類" edit: "編集" -useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う" emailServer: "メールサーバー" enableEmail: "メール配信機能を有効化する" emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" @@ -981,6 +980,9 @@ drivecleaner: "ドライブクリーナー" retryAllQueuesNow: "すべてのキューを今すぐ再試行" retryAllQueuesConfirmTitle: "今すぐ再試行しますか?" retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。" +enableChartsForRemoteUser: "リモートユーザーのチャートを生成" +enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" +showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" _achievements: earnedAt: "獲得日時" @@ -1279,6 +1281,8 @@ _role: followersMoreThanOrEq: "フォロワー数が~以上" followingLessThanOrEq: "フォロー数が~以下" followingMoreThanOrEq: "フォロー数が~以上" + notesLessThanOrEq: "投稿数が~以下" + notesMoreThanOrEq: "投稿数が~以上" and: "~かつ~" or: "~または~" not: "~ではない" @@ -1929,4 +1933,20 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "サイズが大きい順" - orderByCreatedAtAsc: "追加日が古い順" \ No newline at end of file + orderByCreatedAtAsc: "追加日が古い順" + +_webhookSettings: + createWebhook: "Webhookを作成" + name: "名前" + secret: "シークレット" + events: "Webhookを実行するタイミング" + active: "有効" + _events: + follow: "フォローしたとき" + followed: "フォローされたとき" + note: "ノートを投稿したとき" + reply: "返信されたとき" + renote: "Renoteされたとき" + reaction: "リアクションがあったとき" + mention: "メンションされたとき" + diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 82551295f..5b1b312b7 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -594,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。" notificationType: "通知の種類" edit: "編集" -useStarForReactionFallback: "リアクションがようわからん場合、★を使う" emailServer: "メールサーバー" enableEmail: "メール配信を受け取る" emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで" @@ -981,6 +980,9 @@ drivecleaner: "ドライブキレイキレイ" retryAllQueuesNow: "キューを全部もっかいやり直す" retryAllQueuesConfirmTitle: "もっかいやってみるか?" retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。" +enableChartsForRemoteUser: "リモートユーザーのチャートを作る" +enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" +showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" _achievements: earnedAt: "貰った日ぃ" _types: @@ -1277,6 +1279,8 @@ _role: followersMoreThanOrEq: "フォロワー数が~以上" followingLessThanOrEq: "フォロー数が~以下" followingMoreThanOrEq: "フォロー数が~以上" + notesLessThanOrEq: "投稿数が~以下しかない" + notesMoreThanOrEq: "投稿を~以上しとる" and: "~かつ~" or: "~または~" not: "~ではない" @@ -1875,3 +1879,18 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "サイズのでかい順" orderByCreatedAtAsc: "追加日の古い順" +_webhookSettings: + createWebhook: "Webhookをつくる" + name: "名前" + secret: "シークレット" + events: "Webhookを投げるタイミング" + active: "有効" + _events: + follow: "フォローしたとき~!" + followed: "フォローもらったとき~!" + note: "ノートを投稿したとき~!" + reply: "返信があるとき~!" + renote: "Renoteされるとき~!" + reaction: "リアクションがあるとき~!" + mention: "メンションがあるとき~!" + diff --git a/locales/jbo-EN.yml b/locales/jbo-EN.yml index ed97d539c..cd21505a4 100644 --- a/locales/jbo-EN.yml +++ b/locales/jbo-EN.yml @@ -1 +1,2 @@ --- + diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 18fd8f5a5..8b43041e4 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -103,3 +103,4 @@ _deck: _columns: notifications: "Ilɣuyen" list: "Tibdarin" + diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index ef66f3fbd..63a75302a 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -83,3 +83,4 @@ _deck: notifications: "ಅಧಿಸೂಚನೆಗಳು" tl: "ಸಮಯಸಾಲು" mentions: "ಹೆಸರಿಸಿದ" + diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index e52d619f8..31b4200c2 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -592,7 +592,6 @@ tokenRequested: "계정 접근 허용" pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다." notificationType: "알림 유형" edit: "편집" -useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용" emailServer: "메일 서버" enableEmail: "이메일 송신 기능 활성화" emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다." @@ -1849,3 +1848,7 @@ _deck: _dialog: charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}" charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" +_webhookSettings: + name: "이름" + active: "활성화" + diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 9c1a48c67..5736fa67a 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -368,3 +368,4 @@ _deck: list: "ລາຍການ" channel: "ຊ່ອງ" mentions: "ກ່າວເຖິງ" + diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 3d33b5227..efbb83c70 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -483,3 +483,6 @@ _deck: antenna: "Antennes" list: "Lijsten" mentions: "Vermeldingen" +_webhookSettings: + name: "Naam" + diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 83e189b9c..36a0a2e0e 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -1,2 +1,3 @@ --- _lang_: "Norsk Bokmål" + diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 1dc818d45..cc0bbb1fa 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -564,7 +564,6 @@ tokenRequested: "Przydziel dostęp do konta" pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień." notificationType: "Rodzaj powiadomień" edit: "Edytuj" -useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane" emailServer: "Serwer poczty e-mail" enableEmail: "Włącz dostarczanie wiadomości e-mail" emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła" @@ -1358,3 +1357,7 @@ _deck: channel: "Kanały" mentions: "Wspomnienia" direct: "Bezpośredni" +_webhookSettings: + name: "Nazwa" + active: "Właczono" + diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 40b4aee7e..870ad5015 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -555,3 +555,6 @@ _deck: list: "Listas" mentions: "Menções" direct: "Notas diretas" +_webhookSettings: + name: "Nome" + diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 10cb085f3..89f8afac9 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont" pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici." notificationType: "Tipul notificării" edit: "Editează" -useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut" emailServer: "Server email" enableEmail: "Activează distribuția de emailuri" emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola" @@ -702,3 +701,6 @@ _deck: list: "Liste" channel: "Canale" mentions: "Mențiuni" +_webhookSettings: + name: "Nume" + diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 81ea01179..6a1756f8a 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи" pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь." notificationType: "Тип уведомления" edit: "Изменить" -useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи" emailServer: "Сервер электронной почты" enableEmail: "Включить обмен электронной почтой" emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля." @@ -1837,3 +1836,7 @@ _deck: _dialog: charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" +_webhookSettings: + name: "Название" + active: "Вкл." + diff --git a/locales/si-LK.yml b/locales/si-LK.yml index ed97d539c..cd21505a4 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -1 +1,2 @@ --- + diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index d4be5540b..ff6075b70 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -586,7 +586,6 @@ tokenRequested: "Povoliť prístup k účtu" pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu." notificationType: "Typ oznámenia" edit: "Upraviť" -useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe" emailServer: "Email server" enableEmail: "Zapnúť email" emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla" @@ -1475,3 +1474,7 @@ _deck: channel: "Kanály" mentions: "Zmienky" direct: "Priame poznámky" +_webhookSettings: + name: "Názov" + active: "Zapnuté" + diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 5e66df207..6ea5f77c2 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -442,3 +442,6 @@ _deck: antenna: "Antenner" list: "Listor" mentions: "Omnämningar" +_webhookSettings: + active: "Aktiverad" + diff --git a/locales/th-TH.yml b/locales/th-TH.yml index cf33e6642..a8b478439 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น" yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ" yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่" +tokenRevoked: "โทเค็นไม่ถูกต้อง" +accountDeleted: "ลบบัญชีแล้ว" menu: "เมนู" divider: "ตัวแบ่ง" addItem: "เพิ่มรายการ" @@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ" notificationType: "ประเภทการแจ้งเตือน" edit: "แก้ไข" -useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ" emailServer: "อีเมล์เซิร์ฟเวอร์" enableEmail: "เปิดใช้งานการกระจายอีเมล" emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน" @@ -959,6 +960,18 @@ invitationRequiredToRegister: "อินสแตนซ์นี้เป็น emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" postToTheChannel: "โพสต์ลงช่อง" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" +likeOnly: "ที่ชอบเท่านั้น" +resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" +sensitiveWords: "คำที่ละเอียดอ่อน" +sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" +notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ" +license: "ใบอนุญาต" +unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?" +myClips: "คลิปของฉัน" +drivecleaner: "ทำความสะอาดไดรฟ์" +retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง" +retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?" +retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ" _achievements: earnedAt: "ได้รับเมื่อ" _types: @@ -1218,6 +1231,8 @@ _role: iconUrl: "ไอคอน URL" asBadge: "แสดงเป็นตรา" descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" + displayOrder: "ตำแหน่ง" + descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" priority: "ลำดับความสำคัญ" @@ -1243,6 +1258,7 @@ _role: rateLimitFactor: "ขีดจำกัดอัตรา" descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" canHideAds: "ซ่อนโฆษณา" + canSearchNotes: "การใช้การค้นหาโน้ต" _condition: isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไกล" @@ -1844,3 +1860,13 @@ _deck: _dialog: charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" +_disabledTimeline: + title: "ปิดใช้งานไทม์ไลน์" + description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้" +_drivecleaner: + orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย" + orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก" +_webhookSettings: + name: "ชื่อ" + active: "เปิดใช้งาน" + diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 7bd8188a4..0f53dbafc 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -60,3 +60,4 @@ _deck: _columns: notifications: "Bildirim" tl: "Zaman çizelgesi" + diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index 65ef84125..5b825d7bf 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -2,3 +2,4 @@ _lang_: "ياپونچە" search: "ئىزدەش" searchByGoogle: "ئىزدەش" + diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 56e3f024a..7b2ee6d89 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту" pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані." notificationType: "Тип сповіщення" edit: "Редагувати" -useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий" emailServer: "Email сервер" enableEmail: "Увімкнути функцію доставки пошти" emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." @@ -1639,3 +1638,7 @@ _deck: channel: "Канали" mentions: "Згадки" direct: "Особисте" +_webhookSettings: + name: "Ім'я" + active: "Увімкнено" + diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index ce36de03d..f81445473 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -585,7 +585,6 @@ tokenRequested: "Cấp quyền truy cập vào tài khoản" pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây." notificationType: "Loại thông báo" edit: "Sửa" -useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có" emailServer: "Email máy chủ" enableEmail: "Bật phân phối email" emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình" @@ -1705,3 +1704,7 @@ _deck: _dialog: charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}" +_webhookSettings: + name: "Tên" + active: "Đã bật" + diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index df0f3353a..c687c47a1 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -594,7 +594,6 @@ tokenRequested: "允许访问账户" pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" notificationType: "通知类型" edit: "编辑" -useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替" emailServer: "邮件服务器" enableEmail: "启用发送邮件功能" emailConfigInfo: "用于确认电子邮件和密码重置" @@ -1875,3 +1874,7 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "按大小降序排列" orderByCreatedAtAsc: "按添加日期降序排列" +_webhookSettings: + name: "名称" + active: "已启用" + diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 9eac2d135..e031a88f4 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -594,7 +594,6 @@ tokenRequested: "允許存取帳戶" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" notificationType: "通知形式" edit: "編輯" -useStarForReactionFallback: "以★代替未知的表情符號" emailServer: "電郵伺服器" enableEmail: "啟用發送電郵功能" emailConfigInfo: "用於確認電郵地址及密碼重置" @@ -678,8 +677,8 @@ sentReactionsCount: "反應發送次數" receivedReactionsCount: "收到反應次數" pollVotesCount: "已統計的投票數" pollVotedCount: "已投票數" -yes: "確定" -no: "取消" +yes: "是" +no: "否" driveFilesCount: "雲端硬碟檔案數量" driveUsage: "雲端硬碟使用量" noCrawle: "拒絕搜尋引擎索引" @@ -1875,3 +1874,7 @@ _disabledTimeline: _drivecleaner: orderBySizeDesc: "檔案由大到小" orderByCreatedAtAsc: "依照加入的日期順序" +_webhookSettings: + name: "名稱" + active: "已啟用" + diff --git a/package.json b/package.json index 317ee399e..e4cf9c85d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.10.2", + "version": "13.10.3", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js new file mode 100644 index 000000000..42faab746 --- /dev/null +++ b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js @@ -0,0 +1,11 @@ +export class enableChartsForRemoteUser1679639483253 { + name = 'enableChartsForRemoteUser1679639483253' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForRemoteUser" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForRemoteUser"`); + } +} diff --git a/packages/backend/migration/1679651580149-cleanup.js b/packages/backend/migration/1679651580149-cleanup.js new file mode 100644 index 000000000..1f00f3cc1 --- /dev/null +++ b/packages/backend/migration/1679651580149-cleanup.js @@ -0,0 +1,11 @@ +export class cleanup1679651580149 { + name = 'cleanup1679651580149' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js new file mode 100644 index 000000000..073333984 --- /dev/null +++ b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js @@ -0,0 +1,11 @@ +export class enableChartsForFederatedInstances1679652081809 { + name = 'enableChartsForFederatedInstances1679652081809' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForFederatedInstances" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForFederatedInstances"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5a3dcfb5e..3f640c4a6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,9 @@ "@tensorflow/tfjs-node": "4.2.0" }, "dependencies": { + "@aws-sdk/client-s3": "^3.294.0", + "@aws-sdk/lib-storage": "^3.294.0", + "@aws-sdk/node-http-handler": "^3.292.0", "@bull-board/api": "5.0.0", "@bull-board/fastify": "5.0.0", "@bull-board/ui": "5.0.0", @@ -59,7 +62,6 @@ "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", - "aws-sdk": "2.1318.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", "bull": "4.10.4", @@ -190,6 +192,7 @@ "@types/ws": "8.5.4", "@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/parser": "5.54.1", + "aws-sdk-client-mock": "^2.1.1", "cross-env": "7.0.3", "eslint": "8.35.0", "eslint-plugin-import": "2.27.5", diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index b404848d7..a62854c61 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import type { EmojisRepository, Note } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Config } from '@/config.js'; import { ReactionService } from '@/core/ReactionService.js'; @@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js'; @Injectable() export class CustomEmojiService { - private cache: Cache; + private cache: KVCache; constructor( @Inject(DI.config) @@ -34,7 +34,7 @@ export class CustomEmojiService { private globalEventService: GlobalEventService, private reactionService: ReactionService, ) { - this.cache = new Cache(1000 * 60 * 60 * 12); + this.cache = new KVCache(1000 * 60 * 60 * 12); } @bindThis diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index f1e93d6dd..c6258474e 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid'; import sharp from 'sharp'; import { sharpBmp } from 'sharp-read-bmp'; import { IsNull } from 'typeorm'; +import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -36,7 +37,6 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; -import type S3 from 'aws-sdk/clients/s3.js'; type AddFileArgs = { /** User who wish to add file */ @@ -81,6 +81,7 @@ type UploadFromUrlArgs = { export class DriveService { private registerLogger: Logger; private downloaderLogger: Logger; + private deleteLogger: Logger; constructor( @Inject(DI.config) @@ -118,6 +119,7 @@ export class DriveService { const logger = new Logger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); this.downloaderLogger = logger.createSubLogger('downloader'); + this.deleteLogger = logger.createSubLogger('delete'); } /*** @@ -368,7 +370,7 @@ export class DriveService { Body: stream, ContentType: type, CacheControl: 'max-age=31536000, immutable', - } as S3.PutObjectRequest; + } as PutObjectCommandInput; if (filename) params.ContentDisposition = contentDisposition( 'inline', @@ -378,21 +380,16 @@ export class DriveService { ); if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - const s3 = this.s3Service.getS3(meta); - - const upload = s3.upload(params, { - partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, - }); - - await upload.promise() + await this.s3Service.upload(meta, params) .then( result => { - if (result) { + if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); - } else { - this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`); + } else { // AbortMultipartUploadCommandOutput + this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`); } - }, + }) + .catch( err => { this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err); }, @@ -528,10 +525,10 @@ export class DriveService { }; const properties: { - width?: number; - height?: number; - orientation?: number; - } = {}; + width?: number; + height?: number; + orientation?: number; + } = {}; if (info.width) { properties['width'] = info.width; @@ -616,17 +613,20 @@ export class DriveService { if (user) { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { - // Publish driveFileCreated event + // Publish driveFileCreated event this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile); this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile); }); } - // 統計を更新 this.driveChart.update(file, true); - this.perUserDriveChart.update(file, true); - if (file.userHost !== null) { - this.instanceChart.updateDrive(file, true); + if (file.userHost == null) { + // ローカルユーザーのみ + this.perUserDriveChart.update(file, true); + } else { + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateDrive(file, true); + } } return file; @@ -692,7 +692,7 @@ export class DriveService { @bindThis private async deletePostProcess(file: DriveFile, isExpired = false) { - // リモートファイル期限切れ削除後は直リンクにする + // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { this.driveFilesRepository.update(file.id, { isLink: true, @@ -709,33 +709,36 @@ export class DriveService { this.driveFilesRepository.delete(file.id); } - // 統計を更新 this.driveChart.update(file, false); - this.perUserDriveChart.update(file, false); - if (file.userHost !== null) { - this.instanceChart.updateDrive(file, false); + if (file.userHost == null) { + // ローカルユーザーのみ + this.perUserDriveChart.update(file, false); + } else { + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateDrive(file, false); + } } } @bindThis public async deleteObjectStorageFile(key: string) { const meta = await this.metaService.fetch(); - - const s3 = this.s3Service.getS3(meta); - try { - await s3.deleteObject({ - Bucket: meta.objectStorageBucket!, + const param = { + Bucket: meta.objectStorageBucket, Key: key, - }).promise(); + } as DeleteObjectCommandInput; + + await this.s3Service.delete(meta, param); } catch (err: any) { - if (err.code === 'NoSuchKey') { - console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err); + if (err.name === 'NoSuchKey') { + this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); return; + } else { + throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { + cause: err, + }); } - throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { - cause: err, - }); } } diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index e83b037dd..b85791e43 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { InstancesRepository } from '@/models/index.js'; import type { Instance } from '@/models/entities/Instance.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class FederatedInstanceService { - private cache: Cache; + private cache: KVCache; constructor( @Inject(DI.instancesRepository) @@ -18,7 +18,7 @@ export class FederatedInstanceService { private utilityService: UtilityService, private idService: IdService, ) { - this.cache = new Cache(1000 * 60 * 60); + this.cache = new KVCache(1000 * 60 * 60); } @bindThis diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index ee9ae0733..ef87051a7 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import type { LocalUser } from '@/models/entities/User.js'; import type { UsersRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { bindThis } from '@/decorators.js'; @@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: Cache; + private cache: KVCache; constructor( @Inject(DI.usersRepository) @@ -19,7 +19,7 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new Cache(Infinity); + this.cache = new KVCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 2fc2a3d54..7d0805376 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -19,7 +19,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js import { checkWordMute } from '@/misc/check-word-mute.js'; import type { Channel } from '@/models/entities/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -46,7 +46,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -435,15 +435,20 @@ export class NoteCreateService implements OnApplicationShutdown { createdAt: User['createdAt']; isBot: User['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { - // 統計を更新 + const meta = await this.metaService.fetch(); + this.notesChart.update(note, true); - this.perUserNotesChart.update(user, note, true); + if (meta.enableChartsForRemoteUser || (user.host == null)) { + this.perUserNotesChart.update(user, note, true); + } // Register host if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetch(user.host).then(i => { + this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); - this.instanceChart.updateNote(i.host, note, true); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateNote(i.host, note, true); + } }); } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 571b62552..dd878f7bb 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class NoteDeleteService { @@ -39,6 +40,7 @@ export class NoteDeleteService { private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, + private metaService: MetaService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, @@ -95,14 +97,19 @@ export class NoteDeleteService { } //#endregion - // 統計を更新 + const meta = await this.metaService.fetch(); + this.notesChart.update(note, false); - this.perUserNotesChart.update(user, note, false); + if (meta.enableChartsForRemoteUser || (user.host == null)) { + this.perUserNotesChart.update(user, note, false); + } if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetch(user.host).then(i => { + this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); - this.instanceChart.updateNote(i.host, note, false); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateNote(i.host, note, false); + } }); } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 271ba7917..b3aea878d 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -21,6 +21,8 @@ import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +const FALLBACK = '❤'; + const legacies: Record = { 'like': '👍', 'love': '❤', // ここに記述する場合は異体字セレクタを入れない @@ -147,7 +149,11 @@ export class ReactionService { .where('id = :id', { id: note.id }) .execute(); - this.perUserReactionsChart.update(user, note); + const meta = await this.metaService.fetch(); + + if (meta.enableChartsForRemoteUser || (user.host == null)) { + this.perUserReactionsChart.update(user, note); + } // カスタム絵文字リアクションだったら絵文字情報も送る const decodedReaction = this.decodeReaction(reaction); @@ -251,12 +257,6 @@ export class ReactionService { //#endregion } - @bindThis - public async getFallbackReaction(): Promise { - const meta = await this.metaService.fetch(); - return meta.useStarForReactionFallback ? '⭐' : '👍'; - } - @bindThis public convertLegacyReactions(reactions: Record) { const _reactions = {} as Record; @@ -290,7 +290,7 @@ export class ReactionService { @bindThis public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise { - if (reaction == null) return await this.getFallbackReaction(); + if (reaction == null) return FALLBACK; reacterHost = this.utilityService.toPunyNullable(reacterHost); @@ -318,7 +318,7 @@ export class ReactionService { if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; } - return await this.getFallbackReaction(); + return FALLBACK; } @bindThis diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 86f983cc7..4537f1b81 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -3,7 +3,7 @@ import { IsNull } from 'typeorm'; import type { LocalUser, User } from '@/models/entities/User.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { Relay } from '@/models/entities/Relay.js'; import { QueueService } from '@/core/QueueService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; @@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const; @Injectable() export class RelayService { - private relaysCache: Cache; + private relaysCache: KVCache; constructor( @Inject(DI.usersRepository) @@ -30,7 +30,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new Cache(1000 * 60 * 10); + this.relaysCache = new KVCache(1000 * 60 * 10); } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 4775196c6..7b63e43cb 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { In } from 'typeorm'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown { - private rolesCache: Cache; - private roleAssignmentByUserIdCache: Cache; + private rolesCache: KVCache; + private roleAssignmentByUserIdCache: KVCache; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; @@ -84,8 +84,8 @@ export class RoleService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.rolesCache = new Cache(Infinity); - this.roleAssignmentByUserIdCache = new Cache(Infinity); + this.rolesCache = new KVCache(Infinity); + this.roleAssignmentByUserIdCache = new KVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); } @@ -192,6 +192,12 @@ export class RoleService implements OnApplicationShutdown { case 'followingMoreThanOrEq': { return user.followingCount >= value.value; } + case 'notesLessThanOrEq': { + return user.notesCount <= value.value; + } + case 'notesMoreThanOrEq': { + return user.notesCount >= value.value; + } default: return false; } diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index cc8f95081..629278d91 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -1,11 +1,16 @@ import { URL } from 'node:url'; +import * as http from 'node:http'; +import * as https from 'node:https'; import { Inject, Injectable } from '@nestjs/common'; -import S3 from 'aws-sdk/clients/s3.js'; +import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { Upload } from '@aws-sdk/lib-storage'; +import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Meta } from '@/models/entities/Meta.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3'; @Injectable() export class S3Service { @@ -18,25 +23,47 @@ export class S3Service { } @bindThis - public getS3(meta: Meta) { + public getS3Client(meta: Meta): S3Client { const u = meta.objectStorageEndpoint - ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` - : `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; + ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}` + : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent - return new S3({ - endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0 - ? meta.objectStorageEndpoint - : undefined, - accessKeyId: meta.objectStorageAccessKey!, - secretAccessKey: meta.objectStorageSecretKey!, + const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy); + const handlerOption: NodeHttpHandlerOptions = {}; + if (meta.objectStorageUseSSL) { + handlerOption.httpsAgent = agent as https.Agent; + } else { + handlerOption.httpAgent = agent as http.Agent; + } + + return new S3Client({ + endpoint: meta.objectStorageEndpoint ? u : undefined, + credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? { + accessKeyId: meta.objectStorageAccessKey, + secretAccessKey: meta.objectStorageSecretKey, + } : undefined, region: meta.objectStorageRegion ?? undefined, - sslEnabled: meta.objectStorageUseSSL, - s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted - ? false - : meta.objectStorageS3ForcePathStyle, - httpOptions: { - agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), - }, + tls: meta.objectStorageUseSSL, + forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted + requestHandler: new NodeHttpHandler(handlerOption), }); } + + @bindThis + public async upload(meta: Meta, input: PutObjectCommandInput) { + const client = this.getS3Client(meta); + return new Upload({ + client, + params: input, + partSize: (client.config.endpoint && (await client.config.endpoint()).hostname === 'storage.googleapis.com') + ? 500 * 1024 * 1024 + : 8 * 1024 * 1024, + }).done(); + } + + @bindThis + public delete(meta: Meta, input: DeleteObjectCommandInput) { + const client = this.getS3Client(meta); + return client.send(new DeleteObjectCommand(input)); + } } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 92408da34..33b51537a 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @Injectable() @@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown { private logger: Logger; // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ - private blockingsByUserIdCache: Cache; + private blockingsByUserIdCache: KVCache; constructor( @Inject(DI.redisSubscriber) @@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown { ) { this.logger = this.loggerService.getLogger('user-block'); - this.blockingsByUserIdCache = new Cache(Infinity); + this.blockingsByUserIdCache = new KVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index fc383d1c0..631eb4406 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import type { UsersRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { - public userByIdCache: Cache; - public localUserByNativeTokenCache: Cache; - public localUserByIdCache: Cache; - public uriPersonCache: Cache; + public userByIdCache: KVCache; + public localUserByNativeTokenCache: KVCache; + public localUserByIdCache: KVCache; + public uriPersonCache: KVCache; constructor( @Inject(DI.redisSubscriber) @@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new Cache(Infinity); - this.localUserByNativeTokenCache = new Cache(Infinity); - this.localUserByIdCache = new Cache(Infinity); - this.uriPersonCache = new Cache(Infinity); + this.userByIdCache = new KVCache(Infinity); + this.localUserByNativeTokenCache = new KVCache(Infinity); + this.localUserByIdCache = new KVCache(Infinity); + this.uriPersonCache = new KVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 1c8550435..b51b553c7 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { MetaService } from '@/core/MetaService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -57,6 +58,7 @@ export class UserFollowingService { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, + private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, @@ -200,14 +202,18 @@ export class UserFollowingService { //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(i => { + this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - this.instanceChart.updateFollowing(i.host, true); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, true); + } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(i => { + this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - this.instanceChart.updateFollowers(i.host, true); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, true); + } }); } //#endregion @@ -320,14 +326,18 @@ export class UserFollowingService { //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(i => { + this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - this.instanceChart.updateFollowing(i.host, false); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, false); + } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(i => { + this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - this.instanceChart.updateFollowers(i.host, false); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, false); + } }); } //#endregion diff --git a/packages/backend/src/core/UserKeypairStoreService.ts b/packages/backend/src/core/UserKeypairStoreService.ts index 1d3cc87c8..61c9293f8 100644 --- a/packages/backend/src/core/UserKeypairStoreService.ts +++ b/packages/backend/src/core/UserKeypairStoreService.ts @@ -1,20 +1,20 @@ import { Inject, Injectable } from '@nestjs/common'; import type { User } from '@/models/entities/User.js'; import type { UserKeypairsRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class UserKeypairStoreService { - private cache: Cache; + private cache: KVCache; constructor( @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, ) { - this.cache = new Cache(Infinity); + this.cache = new KVCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index d0a4ad7a7..c3b387561 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -3,7 +3,7 @@ import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; @@ -31,8 +31,8 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService { - private publicKeyCache: Cache; - private publicKeyByUserIdCache: Cache; + private publicKeyCache: KVCache; + private publicKeyByUserIdCache: KVCache; constructor( @Inject(DI.config) @@ -50,8 +50,8 @@ export class ApDbResolverService { private userCacheService: UserCacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new Cache(Infinity); - this.publicKeyByUserIdCache = new Cache(Infinity); + this.publicKeyCache = new KVCache(Infinity); + this.publicKeyByUserIdCache = new KVCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index d06958da0..41f7eafa4 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -30,6 +30,7 @@ import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -50,6 +51,7 @@ export class ApPersonService implements OnModuleInit { private userEntityService: UserEntityService; private idService: IdService; private globalEventService: GlobalEventService; + private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; private userCacheService: UserCacheService; @@ -92,6 +94,7 @@ export class ApPersonService implements OnModuleInit { //private userEntityService: UserEntityService, //private idService: IdService, //private globalEventService: GlobalEventService, + //private metaService: MetaService, //private federatedInstanceService: FederatedInstanceService, //private fetchInstanceMetadataService: FetchInstanceMetadataService, //private userCacheService: UserCacheService, @@ -112,6 +115,7 @@ export class ApPersonService implements OnModuleInit { this.userEntityService = this.moduleRef.get('UserEntityService'); this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); + this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.userCacheService = this.moduleRef.get('UserCacheService'); @@ -327,10 +331,12 @@ export class ApPersonService implements OnModuleInit { } // Register host - this.federatedInstanceService.fetch(host).then(i => { + this.federatedInstanceService.fetch(host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); - this.instanceChart.newUser(i.host); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.newUser(i.host); + } }); this.usersChart.update(user!, true); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 068ffad09..b693883e0 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; @@ -52,7 +52,7 @@ export class UserEntityService implements OnModuleInit { private customEmojiService: CustomEmojiService; private antennaService: AntennaService; private roleService: RoleService; - private userInstanceCache: Cache; + private userInstanceCache: KVCache; constructor( private moduleRef: ModuleRef, @@ -121,7 +121,7 @@ export class UserEntityService implements OnModuleInit { //private antennaService: AntennaService, //private roleService: RoleService, ) { - this.userInstanceCache = new Cache(1000 * 60 * 60 * 3); + this.userInstanceCache = new KVCache(1000 * 60 * 60 * 3); } onModuleInit() { diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 43a71a2b5..b249cf448 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -2,11 +2,11 @@ import { bindThis } from '@/decorators.js'; // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -export class Cache { +export class KVCache { public cache: Map; private lifetime: number; - constructor(lifetime: Cache['lifetime']) { + constructor(lifetime: KVCache['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; } @@ -87,3 +87,88 @@ export class Cache { return value; } } + +export class Cache { + private cachedAt: number | null = null; + private value: T | undefined; + private lifetime: number; + + constructor(lifetime: Cache['lifetime']) { + this.lifetime = lifetime; + } + + @bindThis + public set(value: T): void { + this.cachedAt = Date.now(); + this.value = value; + } + + @bindThis + public get(): T | undefined { + if (this.cachedAt == null) return undefined; + if ((Date.now() - this.cachedAt) > this.lifetime) { + this.value = undefined; + this.cachedAt = null; + return undefined; + } + return this.value; + } + + @bindThis + public delete() { + this.value = undefined; + this.cachedAt = null; + } + + /** + * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します + * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + */ + @bindThis + public async fetch(fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + const cachedValue = this.get(); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + this.set(value); + return value; + } + + /** + * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します + * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + */ + @bindThis + public async fetchMaybe(fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + const cachedValue = this.get(); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + if (value !== undefined) { + this.set(value); + } + return value; + } +} diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 57338ecbd..2e4f90b57 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -42,11 +42,6 @@ export class Meta { }) public disableRegistration: boolean; - @Column('boolean', { - default: false, - }) - public useStarForReactionFallback: boolean; - @Column('varchar', { length: 1024, array: true, default: '{}', }) @@ -396,6 +391,16 @@ export class Meta { }) public enableActiveEmailValidation: boolean; + @Column('boolean', { + default: true, + }) + public enableChartsForRemoteUser: boolean; + + @Column('boolean', { + default: true, + }) + public enableChartsForFederatedInstances: boolean; + @Column('jsonb', { default: { }, }) diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index 85ff26674..eca9bcf27 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -54,6 +54,16 @@ type CondFormulaValueFollowingMoreThanOrEq = { value: number; }; +type CondFormulaValueNotesLessThanOrEq = { + type: 'notesLessThanOrEq'; + value: number; +}; + +type CondFormulaValueNotesMoreThanOrEq = { + type: 'notesMoreThanOrEq'; + value: number; +}; + export type RoleCondFormulaValue = CondFormulaValueAnd | CondFormulaValueOr | @@ -65,7 +75,9 @@ export type RoleCondFormulaValue = CondFormulaValueFollowersLessThanOrEq | CondFormulaValueFollowersMoreThanOrEq | CondFormulaValueFollowingLessThanOrEq | - CondFormulaValueFollowingMoreThanOrEq; + CondFormulaValueFollowingMoreThanOrEq | + CondFormulaValueNotesLessThanOrEq | + CondFormulaValueNotesMoreThanOrEq; @Entity() export class Role { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 43a92bb26..f637bf881 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; @@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js'; @Injectable() export class DeliverProcessorService { private logger: Logger; - private suspendedHostsCache: Cache; + private suspendedHostsCache: KVCache; private latest: string | null; constructor( @@ -46,7 +46,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new Cache(1000 * 60 * 60); + this.suspendedHostsCache = new KVCache(1000 * 60 * 60); } @bindThis @@ -88,10 +88,12 @@ export class DeliverProcessorService { } this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - - this.instanceChart.requestSent(i.host, true); this.apRequestChart.deliverSucc(); this.federationChart.deliverd(i.host, true); + + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestSent(i.host, true); + } }); return 'Success'; @@ -107,9 +109,12 @@ export class DeliverProcessorService { }); } - this.instanceChart.requestSent(i.host, false); this.apRequestChart.deliverFail(); this.federationChart.deliverd(i.host, false); + + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestSent(i.host, false); + } }); if (res instanceof StatusError) { diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 41fe06b7c..ed7f38d01 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -184,9 +184,12 @@ export class InboxProcessorService { this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - this.instanceChart.requestReceived(i.host); this.apRequestChart.inbox(); this.federationChart.inbox(i.host); + + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestReceived(i.host); + } }); // アクティビティを処理 diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 364b46696..86019d416 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -118,7 +118,7 @@ export class NodeinfoServerService { }; }; - const cache = new Cache>>(1000 * 60 * 10); + const cache = new KVCache>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { const base = await cache.fetch(null, () => nodeinfo2()); diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 87438c348..a1895e370 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; @@ -18,7 +18,7 @@ export class AuthenticationError extends Error { @Injectable() export class AuthenticateService { - private appCache: Cache; + private appCache: KVCache; constructor( @Inject(DI.usersRepository) @@ -32,7 +32,7 @@ export class AuthenticateService { private userCacheService: UserCacheService, ) { - this.appCache = new Cache(Infinity); + this.appCache = new KVCache(Infinity); } @bindThis diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index ce7e0d569..fc318a621 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -239,6 +239,14 @@ export const meta = { type: 'boolean', optional: true, nullable: false, }, + enableChartsForRemoteUser: { + type: 'boolean', + optional: false, nullable: false, + }, + enableChartsForFederatedInstances: { + type: 'boolean', + optional: false, nullable: false, + }, policies: { type: 'object', optional: false, nullable: false, @@ -299,7 +307,6 @@ export default class extends Endpoint { enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, cacheRemoteFiles: instance.cacheRemoteFiles, - useStarForReactionFallback: instance.useStarForReactionFallback, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, @@ -337,6 +344,8 @@ export default class extends Endpoint { deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + enableChartsForRemoteUser: instance.enableChartsForRemoteUser, + enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, policies: { ...DEFAULT_POLICIES, ...instance.policies }, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 2f23aca24..11de29bf8 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -17,7 +17,6 @@ export const paramDef = { type: 'object', properties: { disableRegistration: { type: 'boolean', nullable: true }, - useStarForReactionFallback: { type: 'boolean', nullable: true }, pinnedUsers: { type: 'array', nullable: true, items: { type: 'string', } }, @@ -93,6 +92,8 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, + enableChartsForRemoteUser: { type: 'boolean' }, + enableChartsForFederatedInstances: { type: 'boolean' }, }, required: [], } as const; @@ -114,10 +115,6 @@ export default class extends Endpoint { set.disableRegistration = ps.disableRegistration; } - if (typeof ps.useStarForReactionFallback === 'boolean') { - set.useStarForReactionFallback = ps.useStarForReactionFallback; - } - if (Array.isArray(ps.pinnedUsers)) { set.pinnedUsers = ps.pinnedUsers.filter(Boolean); } @@ -382,6 +379,14 @@ export default class extends Endpoint { set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } + if (ps.enableChartsForRemoteUser !== undefined) { + set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; + } + + if (ps.enableChartsForFederatedInstances !== undefined) { + set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances; + } + await this.metaService.update(set); this.moderationLogService.insertModerationLog(me, 'updateMeta'); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index d147ddb7f..b7ce3363a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -79,7 +79,7 @@ export default class extends Endpoint { private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords.length === 0) { + if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) { throw new Error('invalid param'); } diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 0549800a6..406566557 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -1,55 +1,56 @@ process.env.NODE_ENV = 'test'; -import { jest } from '@jest/globals'; import { Test } from '@nestjs/testing'; +import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { DriveService } from '@/core/DriveService.js'; import { CoreModule } from '@/core/CoreModule.js'; -import { S3Service } from '@/core/S3Service'; -import type { Meta } from '@/models'; -import type { DeleteObjectOutput } from 'aws-sdk/clients/s3'; -import type { AWSError } from 'aws-sdk/lib/error'; -import type { PromiseResult, Request } from 'aws-sdk/lib/request'; import type { TestingModule } from '@nestjs/testing'; describe('DriveService', () => { let app: TestingModule; let driveService: DriveService; + const s3Mock = mockClient(S3Client); - beforeEach(async () => { + beforeAll(async () => { app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], - providers: [DriveService, S3Service], + providers: [DriveService], }).compile(); app.enableShutdownHooks(); driveService = app.get(DriveService); + }); - const s3Service = app.get(S3Service); - const s3 = s3Service.getS3({} as Meta); + beforeEach(async () => { + s3Mock.reset(); + }); - // new S3() surprisingly does not return an instance of class S3. - // Let's use getPrototypeOf here to get a real prototype, since spying on S3.prototype doesn't work. - // TODO: Use `aws-sdk-client-mock` package when upgrading to AWS SDK v3. - jest.spyOn(Object.getPrototypeOf(s3), 'deleteObject').mockImplementation(() => { - // Roughly mock AWS request object - return { - async promise(): Promise> { - const err = new Error('mock') as AWSError; - err.code = 'NoSuchKey'; - throw err; - }, - } as Request; - }); + afterAll(async () => { + await app.close(); }); describe('Object storage', () => { + test('delete a file', async () => { + s3Mock.on(DeleteObjectCommand) + .resolves({} as DeleteObjectCommandOutput); + + await driveService.deleteObjectStorageFile('peace of the world'); + }); + + test('delete a file then unexpected error', async () => { + s3Mock.on(DeleteObjectCommand) + .rejects(new InvalidObjectState({ $metadata: {}, message: '' })); + + await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrowError(Error); + }); + test('delete a file with no valid key', async () => { - try { - await driveService.deleteObjectStorageFile('lol no way'); - } catch (err: any) { - console.log(err.cause); - throw err; - } + // Some S3 implementations returns 404 Not Found on deleting with a non-existent key + s3Mock.on(DeleteObjectCommand) + .rejects(new NoSuchKey({ $metadata: {}, message: 'allowed error.' })); + + await driveService.deleteObjectStorageFile('lol no way'); }); }); }); diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 6a20a1e08..38db081ac 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -74,19 +74,19 @@ describe('ReactionService', () => { }); test('fallback - undefined', async () => { - assert.strictEqual(await reactionService.toDbReaction(undefined), '👍'); + assert.strictEqual(await reactionService.toDbReaction(undefined), '❤'); }); test('fallback - null', async () => { - assert.strictEqual(await reactionService.toDbReaction(null), '👍'); + assert.strictEqual(await reactionService.toDbReaction(null), '❤'); }); test('fallback - empty', async () => { - assert.strictEqual(await reactionService.toDbReaction(''), '👍'); + assert.strictEqual(await reactionService.toDbReaction(''), '❤'); }); test('fallback - unknown', async () => { - assert.strictEqual(await reactionService.toDbReaction('unknown'), '👍'); + assert.strictEqual(await reactionService.toDbReaction('unknown'), '❤'); }); }); }); diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts new file mode 100644 index 000000000..1dfa22afd --- /dev/null +++ b/packages/backend/test/unit/S3Service.ts @@ -0,0 +1,77 @@ +process.env.NODE_ENV = 'test'; + +import { Test } from '@nestjs/testing'; +import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { S3Service } from '@/core/S3Service'; +import { Meta } from '@/models'; +import type { TestingModule } from '@nestjs/testing'; + +describe('S3Service', () => { + let app: TestingModule; + let s3Service: S3Service; + const s3Mock = mockClient(S3Client); + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + providers: [S3Service], + }).compile(); + app.enableShutdownHooks(); + s3Service = app.get(S3Service); + }); + + beforeEach(async () => { + s3Mock.reset(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('upload', () => { + test('upload a file', async () => { + s3Mock.on(PutObjectCommand).resolves({}); + + await s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x', + }); + }); + + test('upload a large file', async () => { + s3Mock.on(CreateMultipartUploadCommand).resolves({ UploadId: '1' }); + s3Mock.on(UploadPartCommand).resolves({ ETag: '1' }); + s3Mock.on(CompleteMultipartUploadCommand).resolves({ Bucket: 'fake', Key: 'fake' }); + + await s3Service.upload({} as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ + }); + }); + + test('upload a file error', async () => { + s3Mock.on(PutObjectCommand).rejects({ name: 'Fake Error' }); + + await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x', + })).rejects.toThrowError(Error); + }); + + test('upload a large file error', async () => { + s3Mock.on(UploadPartCommand).rejects(); + + await expect(s3Service.upload({} as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ + })).rejects.toThrowError(Error); + }); + }); +}); diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts new file mode 100644 index 000000000..c95da64bb --- /dev/null +++ b/packages/frontend/src/cache.ts @@ -0,0 +1,6 @@ +import * as misskey from 'misskey-js'; +import { Cache } from '@/scripts/cache'; + +export const clipsCache = new Cache(Infinity); +export const rolesCache = new Cache(Infinity); +export const userListsCache = new Cache(Infinity); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index af81051a5..72c6e55df 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -109,6 +109,9 @@ + @@ -151,7 +154,7 @@ import { reactionPicker } from '@/scripts/reaction-picker'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { $i } from '@/account'; import { i18n } from '@/i18n'; -import { getNoteMenu } from '@/scripts/get-note-menu'; +import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; @@ -192,6 +195,7 @@ const menuButton = shallowRef(); const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); +const clipButton = shallowRef(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); @@ -392,6 +396,10 @@ function menu(viaKeyboard = false): void { }).then(focus); } +async function clip() { + os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClipPage }), clipButton.value).then(focus); +} + function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index ea72e1b51..715fd3a9a 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -114,6 +114,9 @@ + @@ -156,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { $i } from '@/account'; import { i18n } from '@/i18n'; -import { getNoteMenu } from '@/scripts/get-note-menu'; +import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; @@ -196,6 +199,7 @@ const menuButton = shallowRef(); const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); +const clipButton = shallowRef(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); @@ -384,6 +388,10 @@ function menu(viaKeyboard = false): void { }).then(focus); } +async function clip() { + os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus); +} + function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 7fb830d53..814ab53d2 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -1,25 +1,26 @@