Merge pull request #10402 from misskey-dev/develop

Release: 13.10.3
This commit is contained in:
syuilo 2023-03-25 08:36:41 +09:00 committed by GitHub
commit f54a9542bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2150 additions and 431 deletions

View file

@ -12,6 +12,22 @@
--> -->
## 13.10.3 (unreleased)
### General
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
### Client
- クリップボタンをノートアクションに追加できるように
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
### Server
- リモートユーザーのチャート生成を無効にするオプションを追加
- リモートサーバーのチャート生成を無効にするオプションを追加
- ドライブのチャートはローカルユーザーのみ生成するように
- 空のアンテナが作成できるのを修正
## 13.10.2 ## 13.10.2
### Server ### Server

View file

@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات." pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
notificationType: "أنواع الإشعارات" notificationType: "أنواع الإشعارات"
edit: "التعديل" edit: "التعديل"
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
emailServer: "خادم البريد الإلكتروني" emailServer: "خادم البريد الإلكتروني"
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها." emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
email: "البريد الإلكتروني " email: "البريد الإلكتروني "
@ -1275,3 +1274,7 @@ _deck:
channel: "القنوات" channel: "القنوات"
mentions: "الإشارات" mentions: "الإشارات"
direct: "مباشرة" direct: "مباشرة"
_webhookSettings:
name: "الإسم"
active: "مفعّل"

View file

@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে" pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
notificationType: "বিজ্ঞপ্তির ধরন" notificationType: "বিজ্ঞপ্তির ধরন"
edit: "সম্পাদনা" edit: "সম্পাদনা"
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
emailServer: "ইমেইল সার্ভার" emailServer: "ইমেইল সার্ভার"
enableEmail: "ইমেইল বিতরণ চালু করুন" enableEmail: "ইমেইল বিতরণ চালু করুন"
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়" emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
@ -1354,3 +1353,7 @@ _deck:
channel: "চ্যানেলগুলি" channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ" mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি" direct: "ডাইরেক্ট নোটগুলি"
_webhookSettings:
name: "নাম"
active: "চালু"

View file

@ -460,3 +460,4 @@ _deck:
list: "Llistes" list: "Llistes"
mentions: "Mencions" mentions: "Mencions"
direct: "Publicacions directes" direct: "Publicacions directes"

View file

@ -776,3 +776,7 @@ _deck:
list: "Seznamy" list: "Seznamy"
channel: "Kanály" channel: "Kanály"
mentions: "Zmínění" mentions: "Zmínění"
_webhookSettings:
name: "Jméno"
active: "Zapnuto"

View file

@ -1,2 +1,3 @@
--- ---
_lang_: "Dansk" _lang_: "Dansk"

View file

@ -594,7 +594,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können." pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
notificationType: "Art der Benachrichtigung" notificationType: "Art der Benachrichtigung"
edit: "Bearbeiten" edit: "Bearbeiten"
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
emailServer: "Email-Server" emailServer: "Email-Server"
enableEmail: "Email-Versand aktivieren" enableEmail: "Email-Versand aktivieren"
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet" 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" followersMoreThanOrEq: "Hat X oder mehr Follower"
followingLessThanOrEq: "Folgt X oder weniger Benutzern" followingLessThanOrEq: "Folgt X oder weniger Benutzern"
followingMoreThanOrEq: "Folgt X oder mehr Benutzern" followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
and: "UND-Bedingung" and: "UND-Bedingung"
or: "ODER-Bedingung" or: "ODER-Bedingung"
not: "NICHT-Bedingung" not: "NICHT-Bedingung"
@ -1875,3 +1876,7 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen" orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings:
name: "Name"
active: "Aktiviert"

View file

@ -392,3 +392,6 @@ _deck:
antenna: "Αντένες" antenna: "Αντένες"
list: "Λίστα" list: "Λίστα"
mentions: "Επισημάνσεις" mentions: "Επισημάνσεις"
_webhookSettings:
name: "Όνομα"

View file

@ -594,7 +594,6 @@ tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here." pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
notificationType: "Notification type" notificationType: "Notification type"
edit: "Edit" edit: "Edit"
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
emailServer: "Email server" emailServer: "Email server"
enableEmail: "Enable email distribution" enableEmail: "Enable email distribution"
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password" 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" retryAllQueuesNow: "Retry running all queues"
retryAllQueuesConfirmTitle: "Really retry all?" retryAllQueuesConfirmTitle: "Really retry all?"
retryAllQueuesConfirmText: "This will temporarily increase the server load." 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: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1277,6 +1279,8 @@ _role:
followersMoreThanOrEq: "Has X or more followers" followersMoreThanOrEq: "Has X or more followers"
followingLessThanOrEq: "Follows X or fewer accounts" followingLessThanOrEq: "Follows X or fewer accounts"
followingMoreThanOrEq: "Follows X or more 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" and: "AND-Condition"
or: "OR-Condition" or: "OR-Condition"
not: "NOT-Condition" not: "NOT-Condition"
@ -1875,3 +1879,18 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "Descending Filesizes" orderBySizeDesc: "Descending Filesizes"
orderByCreatedAtAsc: "Ascending Dates" 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"

View file

@ -594,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí" pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
notificationType: "Tipo de notificación" notificationType: "Tipo de notificación"
edit: "Editar" edit: "Editar"
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
emailServer: "Servidor de correo" emailServer: "Servidor de correo"
enableEmail: "Activar el envío de correos electrónicos" 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" emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
@ -1875,3 +1874,7 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "Más grandes" orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos" orderByCreatedAtAsc: "Más antiguos"
_webhookSettings:
name: "Nombre"
active: "Activado"

View file

@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications" notificationType: "Type de notifications"
edit: "Editer" edit: "Editer"
useStarForReactionFallback: "Utiliser ★ comme alternative si lémoji de réaction est inconnu"
emailServer: "Serveur mail" emailServer: "Serveur mail"
enableEmail: "Activer la distribution de courriel" 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 doubli." emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."
@ -1468,3 +1467,7 @@ _deck:
channel: "Canaux" channel: "Canaux"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct" direct: "Direct"
_webhookSettings:
name: "Nom"
active: "Activé"

View file

@ -1 +1,2 @@
--- ---

View file

@ -1 +1,2 @@
--- ---

View file

@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun"
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
notificationType: "Jenis pemberitahuan" notificationType: "Jenis pemberitahuan"
edit: "Sunting" edit: "Sunting"
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
emailServer: "Peladen surel" emailServer: "Peladen surel"
enableEmail: "Nyalakan distribusi surel" enableEmail: "Nyalakan distribusi surel"
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi" emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
@ -1804,3 +1803,7 @@ _deck:
channel: "Kanal" channel: "Kanal"
mentions: "Sebutan" mentions: "Sebutan"
direct: "Langsung" direct: "Langsung"
_webhookSettings:
name: "Nama"
active: "Aktif"

View file

@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità" visibility: "Visibilità"
poll: "Sondaggio" poll: "Sondaggio"
useCw: "Nascondere media" useCw: "Nascondere media"
enablePlayer: "Apri in lettore video" enablePlayer: "Visualizza"
disablePlayer: "Chiudi il lettore" disablePlayer: "Chiudi"
expandTweet: "Espandi tweet" expandTweet: "Espandi tweet"
themeEditor: "Editor di temi" themeEditor: "Editor di temi"
description: "Descrizione" description: "Descrizione"
@ -594,7 +594,6 @@ tokenRequested: "Autorizza accesso al profilo"
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
notificationType: "Tipo di notifiche" notificationType: "Tipo di notifiche"
edit: "Modifica" edit: "Modifica"
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Abilita consegna email" enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password" 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?" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
myClips: "Le mie Clip" myClips: "Le mie Clip"
drivecleaner: "Drive cleaner" drivecleaner: "Drive cleaner"
retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
_types: _types:
@ -1872,3 +1874,7 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo" orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente" orderByCreatedAtAsc: "Dal più vecchio al più recente"
_webhookSettings:
name: "Nome"
active: "Attivo"

View file

@ -460,7 +460,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする" enableAdvancedMfm: "高度なMFMを有効にする"
@ -594,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
emailServer: "メールサーバー" emailServer: "メールサーバー"
enableEmail: "メール配信機能を有効化する" enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
@ -981,6 +980,9 @@ drivecleaner: "ドライブクリーナー"
retryAllQueuesNow: "すべてのキューを今すぐ再試行" retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?" retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。" retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"
@ -1279,6 +1281,8 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上" followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下" followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上" followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下"
notesMoreThanOrEq: "投稿数が~以上"
and: "~かつ~" and: "~かつ~"
or: "~または~" or: "~または~"
not: "~ではない" not: "~ではない"
@ -1930,3 +1934,19 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "サイズが大きい順" orderBySizeDesc: "サイズが大きい順"
orderByCreatedAtAsc: "追加日が古い順" orderByCreatedAtAsc: "追加日が古い順"
_webhookSettings:
createWebhook: "Webhookを作成"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
active: "有効"
_events:
follow: "フォローしたとき"
followed: "フォローされたとき"
note: "ノートを投稿したとき"
reply: "返信されたとき"
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

View file

@ -594,7 +594,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
emailServer: "メールサーバー" emailServer: "メールサーバー"
enableEmail: "メール配信を受け取る" enableEmail: "メール配信を受け取る"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで" emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
@ -981,6 +980,9 @@ drivecleaner: "ドライブキレイキレイ"
retryAllQueuesNow: "キューを全部もっかいやり直す" retryAllQueuesNow: "キューを全部もっかいやり直す"
retryAllQueuesConfirmTitle: "もっかいやってみるか?" retryAllQueuesConfirmTitle: "もっかいやってみるか?"
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。" retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
_achievements: _achievements:
earnedAt: "貰った日ぃ" earnedAt: "貰った日ぃ"
_types: _types:
@ -1277,6 +1279,8 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上" followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下" followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上" followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下しかない"
notesMoreThanOrEq: "投稿を~以上しとる"
and: "~かつ~" and: "~かつ~"
or: "~または~" or: "~または~"
not: "~ではない" not: "~ではない"
@ -1875,3 +1879,18 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "サイズのでかい順" orderBySizeDesc: "サイズのでかい順"
orderByCreatedAtAsc: "追加日の古い順" orderByCreatedAtAsc: "追加日の古い順"
_webhookSettings:
createWebhook: "Webhookをつくる"
name: "名前"
secret: "シークレット"
events: "Webhookを投げるタイミング"
active: "有効"
_events:
follow: "フォローしたとき~!"
followed: "フォローもらったとき~!"
note: "ノートを投稿したとき~!"
reply: "返信があるとき~!"
renote: "Renoteされるとき"
reaction: "リアクションがあるとき~!"
mention: "メンションがあるとき~!"

View file

@ -1 +1,2 @@
--- ---

View file

@ -103,3 +103,4 @@ _deck:
_columns: _columns:
notifications: "Ilɣuyen" notifications: "Ilɣuyen"
list: "Tibdarin" list: "Tibdarin"

View file

@ -83,3 +83,4 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು" notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು" tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ" mentions: "ಹೆಸರಿಸಿದ"

View file

@ -592,7 +592,6 @@ tokenRequested: "계정 접근 허용"
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다." pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
notificationType: "알림 유형" notificationType: "알림 유형"
edit: "편집" edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailServer: "메일 서버" emailServer: "메일 서버"
enableEmail: "이메일 송신 기능 활성화" enableEmail: "이메일 송신 기능 활성화"
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다." emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
@ -1849,3 +1848,7 @@ _deck:
_dialog: _dialog:
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}" charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
_webhookSettings:
name: "이름"
active: "활성화"

View file

@ -368,3 +368,4 @@ _deck:
list: "ລາຍການ" list: "ລາຍການ"
channel: "ຊ່ອງ" channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ" mentions: "ກ່າວເຖິງ"

View file

@ -483,3 +483,6 @@ _deck:
antenna: "Antennes" antenna: "Antennes"
list: "Lijsten" list: "Lijsten"
mentions: "Vermeldingen" mentions: "Vermeldingen"
_webhookSettings:
name: "Naam"

View file

@ -1,2 +1,3 @@
--- ---
_lang_: "Norsk Bokmål" _lang_: "Norsk Bokmål"

View file

@ -564,7 +564,6 @@ tokenRequested: "Przydziel dostęp do konta"
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień." pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
notificationType: "Rodzaj powiadomień" notificationType: "Rodzaj powiadomień"
edit: "Edytuj" edit: "Edytuj"
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
emailServer: "Serwer poczty e-mail" emailServer: "Serwer poczty e-mail"
enableEmail: "Włącz dostarczanie wiadomości 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" emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
@ -1358,3 +1357,7 @@ _deck:
channel: "Kanały" channel: "Kanały"
mentions: "Wspomnienia" mentions: "Wspomnienia"
direct: "Bezpośredni" direct: "Bezpośredni"
_webhookSettings:
name: "Nazwa"
active: "Właczono"

View file

@ -555,3 +555,6 @@ _deck:
list: "Listas" list: "Listas"
mentions: "Menções" mentions: "Menções"
direct: "Notas diretas" direct: "Notas diretas"
_webhookSettings:
name: "Nome"

View file

@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont"
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici." pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
notificationType: "Tipul notificării" notificationType: "Tipul notificării"
edit: "Editează" edit: "Editează"
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Activează distribuția de emailuri" enableEmail: "Activează distribuția de emailuri"
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola" emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
@ -702,3 +701,6 @@ _deck:
list: "Liste" list: "Liste"
channel: "Canale" channel: "Canale"
mentions: "Mențiuni" mentions: "Mențiuni"
_webhookSettings:
name: "Nume"

View file

@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь." pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления" notificationType: "Тип уведомления"
edit: "Изменить" edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
emailServer: "Сервер электронной почты" emailServer: "Сервер электронной почты"
enableEmail: "Включить обмен электронной почтой" enableEmail: "Включить обмен электронной почтой"
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля." emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
@ -1837,3 +1836,7 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
_webhookSettings:
name: "Название"
active: "Вкл."

View file

@ -1 +1,2 @@
--- ---

View file

@ -586,7 +586,6 @@ tokenRequested: "Povoliť prístup k účtu"
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu." pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
notificationType: "Typ oznámenia" notificationType: "Typ oznámenia"
edit: "Upraviť" edit: "Upraviť"
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
emailServer: "Email server" emailServer: "Email server"
enableEmail: "Zapnúť email" enableEmail: "Zapnúť email"
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla" emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
@ -1475,3 +1474,7 @@ _deck:
channel: "Kanály" channel: "Kanály"
mentions: "Zmienky" mentions: "Zmienky"
direct: "Priame poznámky" direct: "Priame poznámky"
_webhookSettings:
name: "Názov"
active: "Zapnuté"

View file

@ -442,3 +442,6 @@ _deck:
antenna: "Antenner" antenna: "Antenner"
list: "Listor" list: "Listor"
mentions: "Omnämningar" mentions: "Omnämningar"
_webhookSettings:
active: "Aktiverad"

View file

@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น" userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ" yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่" yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
tokenRevoked: "โทเค็นไม่ถูกต้อง"
accountDeleted: "ลบบัญชีแล้ว"
menu: "เมนู" menu: "เมนู"
divider: "ตัวแบ่ง" divider: "ตัวแบ่ง"
addItem: "เพิ่มรายการ" addItem: "เพิ่มรายการ"
@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ" pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
notificationType: "ประเภทการแจ้งเตือน" notificationType: "ประเภทการแจ้งเตือน"
edit: "แก้ไข" edit: "แก้ไข"
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
emailServer: "อีเมล์เซิร์ฟเวอร์" emailServer: "อีเมล์เซิร์ฟเวอร์"
enableEmail: "เปิดใช้งานการกระจายอีเมล" enableEmail: "เปิดใช้งานการกระจายอีเมล"
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน" emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
@ -959,6 +960,18 @@ invitationRequiredToRegister: "อินสแตนซ์นี้เป็น
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง" postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
likeOnly: "ที่ชอบเท่านั้น"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
license: "ใบอนุญาต"
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
myClips: "คลิปของฉัน"
drivecleaner: "ทำความสะอาดไดรฟ์"
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
_achievements: _achievements:
earnedAt: "ได้รับเมื่อ" earnedAt: "ได้รับเมื่อ"
_types: _types:
@ -1218,6 +1231,8 @@ _role:
iconUrl: "ไอคอน URL" iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา" asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
displayOrder: "ตำแหน่ง"
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ" priority: "ลำดับความสำคัญ"
@ -1243,6 +1258,7 @@ _role:
rateLimitFactor: "ขีดจำกัดอัตรา" rateLimitFactor: "ขีดจำกัดอัตรา"
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
canHideAds: "ซ่อนโฆษณา" canHideAds: "ซ่อนโฆษณา"
canSearchNotes: "การใช้การค้นหาโน้ต"
_condition: _condition:
isLocal: "ผู้ใช้ภายใน" isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล" isRemote: "ผู้ใช้ระยะไกล"
@ -1844,3 +1860,13 @@ _deck:
_dialog: _dialog:
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
_disabledTimeline:
title: "ปิดใช้งานไทม์ไลน์"
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
_drivecleaner:
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
_webhookSettings:
name: "ชื่อ"
active: "เปิดใช้งาน"

View file

@ -60,3 +60,4 @@ _deck:
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"

View file

@ -2,3 +2,4 @@
_lang_: "ياپونچە" _lang_: "ياپونچە"
search: "ئىزدەش" search: "ئىزدەش"
searchByGoogle: "ئىزدەش" searchByGoogle: "ئىزدەش"

View file

@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту"
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані." pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
notificationType: "Тип сповіщення" notificationType: "Тип сповіщення"
edit: "Редагувати" edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Email сервер" emailServer: "Email сервер"
enableEmail: "Увімкнути функцію доставки пошти" enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
@ -1639,3 +1638,7 @@ _deck:
channel: "Канали" channel: "Канали"
mentions: "Згадки" mentions: "Згадки"
direct: "Особисте" direct: "Особисте"
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"

View file

@ -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." pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
notificationType: "Loại thông báo" notificationType: "Loại thông báo"
edit: "Sửa" edit: "Sửa"
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
emailServer: "Email máy chủ" emailServer: "Email máy chủ"
enableEmail: "Bật phân phối email" 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" 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: _dialog:
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" 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}" 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"

View file

@ -594,7 +594,6 @@ tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型" notificationType: "通知类型"
edit: "编辑" edit: "编辑"
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
emailServer: "邮件服务器" emailServer: "邮件服务器"
enableEmail: "启用发送邮件功能" enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置" emailConfigInfo: "用于确认电子邮件和密码重置"
@ -1875,3 +1874,7 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "按大小降序排列" orderBySizeDesc: "按大小降序排列"
orderByCreatedAtAsc: "按添加日期降序排列" orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings:
name: "名称"
active: "已启用"

View file

@ -594,7 +594,6 @@ tokenRequested: "允許存取帳戶"
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
notificationType: "通知形式" notificationType: "通知形式"
edit: "編輯" edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailServer: "電郵伺服器" emailServer: "電郵伺服器"
enableEmail: "啟用發送電郵功能" enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置" emailConfigInfo: "用於確認電郵地址及密碼重置"
@ -678,8 +677,8 @@ sentReactionsCount: "反應發送次數"
receivedReactionsCount: "收到反應次數" receivedReactionsCount: "收到反應次數"
pollVotesCount: "已統計的投票數" pollVotesCount: "已統計的投票數"
pollVotedCount: "已投票數" pollVotedCount: "已投票數"
yes: "確定" yes: ""
no: "取消" no: ""
driveFilesCount: "雲端硬碟檔案數量" driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量" driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引" noCrawle: "拒絕搜尋引擎索引"
@ -1875,3 +1874,7 @@ _disabledTimeline:
_drivecleaner: _drivecleaner:
orderBySizeDesc: "檔案由大到小" orderBySizeDesc: "檔案由大到小"
orderByCreatedAtAsc: "依照加入的日期順序" orderByCreatedAtAsc: "依照加入的日期順序"
_webhookSettings:
name: "名稱"
active: "已啟用"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.10.2", "version": "13.10.3",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -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"`);
}
}

View file

@ -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`);
}
}

View file

@ -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"`);
}
}

View file

@ -37,6 +37,9 @@
"@tensorflow/tfjs-node": "4.2.0" "@tensorflow/tfjs-node": "4.2.0"
}, },
"dependencies": { "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/api": "5.0.0",
"@bull-board/fastify": "5.0.0", "@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0", "@bull-board/ui": "5.0.0",
@ -59,7 +62,6 @@
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1318.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bull": "4.10.4", "bull": "4.10.4",
@ -190,6 +192,7 @@
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1", "@typescript-eslint/parser": "5.54.1",
"aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.35.0", "eslint": "8.35.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",

View file

@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js'; import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js'; import type { EmojisRepository, Note } from '@/models/index.js';
import { bindThis } from '@/decorators.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 { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js'; import { ReactionService } from '@/core/ReactionService.js';
@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js';
@Injectable() @Injectable()
export class CustomEmojiService { export class CustomEmojiService {
private cache: Cache<Emoji | null>; private cache: KVCache<Emoji | null>;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -34,7 +34,7 @@ export class CustomEmojiService {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private reactionService: ReactionService, private reactionService: ReactionService,
) { ) {
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12); this.cache = new KVCache<Emoji | null>(1000 * 60 * 60 * 12);
} }
@bindThis @bindThis

View file

@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp'; import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -36,7 +37,6 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import type S3 from 'aws-sdk/clients/s3.js';
type AddFileArgs = { type AddFileArgs = {
/** User who wish to add file */ /** User who wish to add file */
@ -81,6 +81,7 @@ type UploadFromUrlArgs = {
export class DriveService { export class DriveService {
private registerLogger: Logger; private registerLogger: Logger;
private downloaderLogger: Logger; private downloaderLogger: Logger;
private deleteLogger: Logger;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -118,6 +119,7 @@ export class DriveService {
const logger = new Logger('drive', 'blue'); const logger = new Logger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow'); this.registerLogger = logger.createSubLogger('register', 'yellow');
this.downloaderLogger = logger.createSubLogger('downloader'); this.downloaderLogger = logger.createSubLogger('downloader');
this.deleteLogger = logger.createSubLogger('delete');
} }
/*** /***
@ -368,7 +370,7 @@ export class DriveService {
Body: stream, Body: stream,
ContentType: type, ContentType: type,
CacheControl: 'max-age=31536000, immutable', CacheControl: 'max-age=31536000, immutable',
} as S3.PutObjectRequest; } as PutObjectCommandInput;
if (filename) params.ContentDisposition = contentDisposition( if (filename) params.ContentDisposition = contentDisposition(
'inline', 'inline',
@ -378,21 +380,16 @@ export class DriveService {
); );
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
const s3 = this.s3Service.getS3(meta); await this.s3Service.upload(meta, params)
const upload = s3.upload(params, {
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
});
await upload.promise()
.then( .then(
result => { result => {
if (result) { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
} else { } else { // AbortMultipartUploadCommandOutput
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`); this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
} }
}, })
.catch(
err => { err => {
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err); this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
}, },
@ -622,12 +619,15 @@ export class DriveService {
}); });
} }
// 統計を更新
this.driveChart.update(file, true); this.driveChart.update(file, true);
if (file.userHost == null) {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, true); this.perUserDriveChart.update(file, true);
if (file.userHost !== null) { } else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, true); this.instanceChart.updateDrive(file, true);
} }
}
return file; return file;
} }
@ -709,35 +709,38 @@ export class DriveService {
this.driveFilesRepository.delete(file.id); this.driveFilesRepository.delete(file.id);
} }
// 統計を更新
this.driveChart.update(file, false); this.driveChart.update(file, false);
if (file.userHost == null) {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, false); this.perUserDriveChart.update(file, false);
if (file.userHost !== null) { } else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, false); this.instanceChart.updateDrive(file, false);
} }
} }
}
@bindThis @bindThis
public async deleteObjectStorageFile(key: string) { public async deleteObjectStorageFile(key: string) {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const s3 = this.s3Service.getS3(meta);
try { try {
await s3.deleteObject({ const param = {
Bucket: meta.objectStorageBucket!, Bucket: meta.objectStorageBucket,
Key: key, Key: key,
}).promise(); } as DeleteObjectCommandInput;
await this.s3Service.delete(meta, param);
} catch (err: any) { } catch (err: any) {
if (err.code === 'NoSuchKey') { if (err.name === 'NoSuchKey') {
console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err); this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
return; return;
} } else {
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err, cause: err,
}); });
} }
} }
}
@bindThis @bindThis
public async uploadFromUrl({ public async uploadFromUrl({

View file

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.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 { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class FederatedInstanceService { export class FederatedInstanceService {
private cache: Cache<Instance>; private cache: KVCache<Instance>;
constructor( constructor(
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
@ -18,7 +18,7 @@ export class FederatedInstanceService {
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
) { ) {
this.cache = new Cache<Instance>(1000 * 60 * 60); this.cache = new KVCache<Instance>(1000 * 60 * 60);
} }
@bindThis @bindThis

View file

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.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 { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable() @Injectable()
export class InstanceActorService { export class InstanceActorService {
private cache: Cache<LocalUser>; private cache: KVCache<LocalUser>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -19,7 +19,7 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService, private createSystemUserService: CreateSystemUserService,
) { ) {
this.cache = new Cache<LocalUser>(Infinity); this.cache = new KVCache<LocalUser>(Infinity);
} }
@bindThis @bindThis

View file

@ -19,7 +19,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js'; import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.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 type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.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 { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.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'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -435,15 +435,20 @@ export class NoteCreateService implements OnApplicationShutdown {
createdAt: User['createdAt']; createdAt: User['createdAt'];
isBot: User['isBot']; isBot: User['isBot'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
// 統計を更新 const meta = await this.metaService.fetch();
this.notesChart.update(note, true); this.notesChart.update(note, true);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, true); this.perUserNotesChart.update(user, note, true);
}
// Register host // Register host
if (this.userEntityService.isRemoteUser(user)) { 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.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true); this.instanceChart.updateNote(i.host, note, true);
}
}); });
} }

View file

@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable() @Injectable()
export class NoteDeleteService { export class NoteDeleteService {
@ -39,6 +40,7 @@ export class NoteDeleteService {
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
@ -95,14 +97,19 @@ export class NoteDeleteService {
} }
//#endregion //#endregion
// 統計を更新 const meta = await this.metaService.fetch();
this.notesChart.update(note, false); this.notesChart.update(note, false);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, false); this.perUserNotesChart.update(user, note, false);
}
if (this.userEntityService.isRemoteUser(user)) { 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.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false); this.instanceChart.updateNote(i.host, note, false);
}
}); });
} }
} }

View file

@ -21,6 +21,8 @@ import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
const FALLBACK = '❤';
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
'love': '❤', // ここに記述する場合は異体字セレクタを入れない 'love': '❤', // ここに記述する場合は異体字セレクタを入れない
@ -147,7 +149,11 @@ export class ReactionService {
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note); this.perUserReactionsChart.update(user, note);
}
// カスタム絵文字リアクションだったら絵文字情報も送る // カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction); const decodedReaction = this.decodeReaction(reaction);
@ -251,12 +257,6 @@ export class ReactionService {
//#endregion //#endregion
} }
@bindThis
public async getFallbackReaction(): Promise<string> {
const meta = await this.metaService.fetch();
return meta.useStarForReactionFallback ? '⭐' : '👍';
}
@bindThis @bindThis
public convertLegacyReactions(reactions: Record<string, number>) { public convertLegacyReactions(reactions: Record<string, number>) {
const _reactions = {} as Record<string, number>; const _reactions = {} as Record<string, number>;
@ -290,7 +290,7 @@ export class ReactionService {
@bindThis @bindThis
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
if (reaction == null) return await this.getFallbackReaction(); if (reaction == null) return FALLBACK;
reacterHost = this.utilityService.toPunyNullable(reacterHost); reacterHost = this.utilityService.toPunyNullable(reacterHost);
@ -318,7 +318,7 @@ export class ReactionService {
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
} }
return await this.getFallbackReaction(); return FALLBACK;
} }
@bindThis @bindThis

View file

@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js'; import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.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 type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable() @Injectable()
export class RelayService { export class RelayService {
private relaysCache: Cache<Relay[]>; private relaysCache: KVCache<Relay[]>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService, private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
) { ) {
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10); this.relaysCache = new KVCache<Relay[]>(1000 * 60 * 10);
} }
@bindThis @bindThis

View file

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; 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 type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown { export class RoleService implements OnApplicationShutdown {
private rolesCache: Cache<Role[]>; private rolesCache: KVCache<Role[]>;
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>; private roleAssignmentByUserIdCache: KVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {}; public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = 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.onMessage = this.onMessage.bind(this);
this.rolesCache = new Cache<Role[]>(Infinity); this.rolesCache = new KVCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity); this.roleAssignmentByUserIdCache = new KVCache<RoleAssignment[]>(Infinity);
this.redisSubscriber.on('message', this.onMessage); this.redisSubscriber.on('message', this.onMessage);
} }
@ -192,6 +192,12 @@ export class RoleService implements OnApplicationShutdown {
case 'followingMoreThanOrEq': { case 'followingMoreThanOrEq': {
return user.followingCount >= value.value; return user.followingCount >= value.value;
} }
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
default: default:
return false; return false;
} }

View file

@ -1,11 +1,16 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import * as http from 'node:http';
import * as https from 'node:https';
import { Inject, Injectable } from '@nestjs/common'; 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 { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Meta } from '@/models/entities/Meta.js'; import type { Meta } from '@/models/entities/Meta.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
@Injectable() @Injectable()
export class S3Service { export class S3Service {
@ -18,25 +23,47 @@ export class S3Service {
} }
@bindThis @bindThis
public getS3(meta: Meta) { public getS3Client(meta: Meta): S3Client {
const u = meta.objectStorageEndpoint const u = meta.objectStorageEndpoint
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
return new S3({ const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0 const handlerOption: NodeHttpHandlerOptions = {};
? meta.objectStorageEndpoint if (meta.objectStorageUseSSL) {
: undefined, handlerOption.httpsAgent = agent as https.Agent;
accessKeyId: meta.objectStorageAccessKey!, } else {
secretAccessKey: meta.objectStorageSecretKey!, 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, region: meta.objectStorageRegion ?? undefined,
sslEnabled: meta.objectStorageUseSSL, tls: meta.objectStorageUseSSL,
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
? false requestHandler: new NodeHttpHandler(handlerOption),
: meta.objectStorageS3ForcePathStyle,
httpOptions: {
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
},
}); });
} }
@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));
}
} }

View file

@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.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'; import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable() @Injectable()
@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>; private blockingsByUserIdCache: KVCache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown {
) { ) {
this.logger = this.loggerService.getLogger('user-block'); this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity); this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage); this.redisSubscriber.on('message', this.onMessage);
} }

View file

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import type { UsersRepository } from '@/models/index.js'; 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 type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class UserCacheService implements OnApplicationShutdown { export class UserCacheService implements OnApplicationShutdown {
public userByIdCache: Cache<User>; public userByIdCache: KVCache<User>;
public localUserByNativeTokenCache: Cache<LocalUser | null>; public localUserByNativeTokenCache: KVCache<LocalUser | null>;
public localUserByIdCache: Cache<LocalUser>; public localUserByIdCache: KVCache<LocalUser>;
public uriPersonCache: Cache<User | null>; public uriPersonCache: KVCache<User | null>;
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown {
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new Cache<User>(Infinity); this.userByIdCache = new KVCache<User>(Infinity);
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity); this.localUserByNativeTokenCache = new KVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new Cache<LocalUser>(Infinity); this.localUserByIdCache = new KVCache<LocalUser>(Infinity);
this.uriPersonCache = new Cache<User | null>(Infinity); this.uriPersonCache = new KVCache<User | null>(Infinity);
this.redisSubscriber.on('message', this.onMessage); this.redisSubscriber.on('message', this.onMessage);
} }

View file

@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -57,6 +58,7 @@ export class UserFollowingService {
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private metaService: MetaService,
private notificationService: NotificationService, private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService, private webhookService: WebhookService,
@ -200,14 +202,18 @@ export class UserFollowingService {
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { 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.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true); this.instanceChart.updateFollowing(i.host, true);
}
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } 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.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true); this.instanceChart.updateFollowers(i.host, true);
}
}); });
} }
//#endregion //#endregion
@ -320,14 +326,18 @@ export class UserFollowingService {
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { 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.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false); this.instanceChart.updateFollowing(i.host, false);
}
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } 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.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false); this.instanceChart.updateFollowers(i.host, false);
}
}); });
} }
//#endregion //#endregion

View file

@ -1,20 +1,20 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.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 type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class UserKeypairStoreService { export class UserKeypairStoreService {
private cache: Cache<UserKeypair>; private cache: KVCache<UserKeypair>;
constructor( constructor(
@Inject(DI.userKeypairsRepository) @Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository, private userKeypairsRepository: UserKeypairsRepository,
) { ) {
this.cache = new Cache<UserKeypair>(Infinity); this.cache = new KVCache<UserKeypair>(Infinity);
} }
@bindThis @bindThis

View file

@ -3,7 +3,7 @@ import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.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 type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { UserCacheService } from '@/core/UserCacheService.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
@ -31,8 +31,8 @@ export type UriParseResult = {
@Injectable() @Injectable()
export class ApDbResolverService { export class ApDbResolverService {
private publicKeyCache: Cache<UserPublickey | null>; private publicKeyCache: KVCache<UserPublickey | null>;
private publicKeyByUserIdCache: Cache<UserPublickey | null>; private publicKeyByUserIdCache: KVCache<UserPublickey | null>;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -50,8 +50,8 @@ export class ApDbResolverService {
private userCacheService: UserCacheService, private userCacheService: UserCacheService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
) { ) {
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity); this.publicKeyCache = new KVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); this.publicKeyByUserIdCache = new KVCache<UserPublickey | null>(Infinity);
} }
@bindThis @bindThis

View file

@ -30,6 +30,7 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -50,6 +51,7 @@ export class ApPersonService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private idService: IdService; private idService: IdService;
private globalEventService: GlobalEventService; private globalEventService: GlobalEventService;
private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService; private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService; private fetchInstanceMetadataService: FetchInstanceMetadataService;
private userCacheService: UserCacheService; private userCacheService: UserCacheService;
@ -92,6 +94,7 @@ export class ApPersonService implements OnModuleInit {
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private idService: IdService, //private idService: IdService,
//private globalEventService: GlobalEventService, //private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService, //private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService, //private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private userCacheService: UserCacheService, //private userCacheService: UserCacheService,
@ -112,6 +115,7 @@ export class ApPersonService implements OnModuleInit {
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService'); this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.userCacheService = this.moduleRef.get('UserCacheService'); this.userCacheService = this.moduleRef.get('UserCacheService');
@ -327,10 +331,12 @@ export class ApPersonService implements OnModuleInit {
} }
// Register host // Register host
this.federatedInstanceService.fetch(host).then(i => { this.federatedInstanceService.fetch(host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.instanceChart.newUser(i.host);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host);
}
}); });
this.usersChart.update(user!, true); this.usersChart.update(user!, true);

View file

@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js'; import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } 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 { 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 { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } 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 customEmojiService: CustomEmojiService;
private antennaService: AntennaService; private antennaService: AntennaService;
private roleService: RoleService; private roleService: RoleService;
private userInstanceCache: Cache<Instance | null>; private userInstanceCache: KVCache<Instance | null>;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@ -121,7 +121,7 @@ export class UserEntityService implements OnModuleInit {
//private antennaService: AntennaService, //private antennaService: AntennaService,
//private roleService: RoleService, //private roleService: RoleService,
) { ) {
this.userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3); this.userInstanceCache = new KVCache<Instance | null>(1000 * 60 * 60 * 3);
} }
onModuleInit() { onModuleInit() {

View file

@ -2,11 +2,11 @@ import { bindThis } from '@/decorators.js';
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class Cache<T> { export class KVCache<T> {
public cache: Map<string | null, { date: number; value: T; }>; public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) { constructor(lifetime: KVCache<never>['lifetime']) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;
} }
@ -87,3 +87,88 @@ export class Cache<T> {
return value; return value;
} }
} }
export class Cache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: Cache<never>['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<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
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<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
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;
}
}

View file

@ -42,11 +42,6 @@ export class Meta {
}) })
public disableRegistration: boolean; public disableRegistration: boolean;
@Column('boolean', {
default: false,
})
public useStarForReactionFallback: boolean;
@Column('varchar', { @Column('varchar', {
length: 1024, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })
@ -396,6 +391,16 @@ export class Meta {
}) })
public enableActiveEmailValidation: boolean; public enableActiveEmailValidation: boolean;
@Column('boolean', {
default: true,
})
public enableChartsForRemoteUser: boolean;
@Column('boolean', {
default: true,
})
public enableChartsForFederatedInstances: boolean;
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })

View file

@ -54,6 +54,16 @@ type CondFormulaValueFollowingMoreThanOrEq = {
value: number; value: number;
}; };
type CondFormulaValueNotesLessThanOrEq = {
type: 'notesLessThanOrEq';
value: number;
};
type CondFormulaValueNotesMoreThanOrEq = {
type: 'notesMoreThanOrEq';
value: number;
};
export type RoleCondFormulaValue = export type RoleCondFormulaValue =
CondFormulaValueAnd | CondFormulaValueAnd |
CondFormulaValueOr | CondFormulaValueOr |
@ -65,7 +75,9 @@ export type RoleCondFormulaValue =
CondFormulaValueFollowersLessThanOrEq | CondFormulaValueFollowersLessThanOrEq |
CondFormulaValueFollowersMoreThanOrEq | CondFormulaValueFollowersMoreThanOrEq |
CondFormulaValueFollowingLessThanOrEq | CondFormulaValueFollowingLessThanOrEq |
CondFormulaValueFollowingMoreThanOrEq; CondFormulaValueFollowingMoreThanOrEq |
CondFormulaValueNotesLessThanOrEq |
CondFormulaValueNotesMoreThanOrEq;
@Entity() @Entity()
export class Role { export class Role {

View file

@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js';
import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.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 type { Instance } from '@/models/entities/Instance.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js';
@Injectable() @Injectable()
export class DeliverProcessorService { export class DeliverProcessorService {
private logger: Logger; private logger: Logger;
private suspendedHostsCache: Cache<Instance[]>; private suspendedHostsCache: KVCache<Instance[]>;
private latest: string | null; private latest: string | null;
constructor( constructor(
@ -46,7 +46,7 @@ export class DeliverProcessorService {
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60); this.suspendedHostsCache = new KVCache<Instance[]>(1000 * 60 * 60);
} }
@bindThis @bindThis
@ -88,10 +88,12 @@ export class DeliverProcessorService {
} }
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
this.instanceChart.requestSent(i.host, true);
this.apRequestChart.deliverSucc(); this.apRequestChart.deliverSucc();
this.federationChart.deliverd(i.host, true); this.federationChart.deliverd(i.host, true);
if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true);
}
}); });
return 'Success'; return 'Success';
@ -107,9 +109,12 @@ export class DeliverProcessorService {
}); });
} }
this.instanceChart.requestSent(i.host, false);
this.apRequestChart.deliverFail(); this.apRequestChart.deliverFail();
this.federationChart.deliverd(i.host, false); this.federationChart.deliverd(i.host, false);
if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false);
}
}); });
if (res instanceof StatusError) { if (res instanceof StatusError) {

View file

@ -184,9 +184,12 @@ export class InboxProcessorService {
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
this.instanceChart.requestReceived(i.host);
this.apRequestChart.inbox(); this.apRequestChart.inbox();
this.federationChart.inbox(i.host); this.federationChart.inbox(i.host);
if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host);
}
}); });
// アクティビティを処理 // アクティビティを処理

View file

@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js'; import NotesChart from '@/core/chart/charts/notes.js';
@ -118,7 +118,7 @@ export class NodeinfoServerService {
}; };
}; };
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); const cache = new KVCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
fastify.get(nodeinfo2_1path, async (request, reply) => { fastify.get(nodeinfo2_1path, async (request, reply) => {
const base = await cache.fetch(null, () => nodeinfo2()); const base = await cache.fetch(null, () => nodeinfo2());

View file

@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.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 type { App } from '@/models/entities/App.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { UserCacheService } from '@/core/UserCacheService.js';
import isNativeToken from '@/misc/is-native-token.js'; import isNativeToken from '@/misc/is-native-token.js';
@ -18,7 +18,7 @@ export class AuthenticationError extends Error {
@Injectable() @Injectable()
export class AuthenticateService { export class AuthenticateService {
private appCache: Cache<App>; private appCache: KVCache<App>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -32,7 +32,7 @@ export class AuthenticateService {
private userCacheService: UserCacheService, private userCacheService: UserCacheService,
) { ) {
this.appCache = new Cache<App>(Infinity); this.appCache = new KVCache<App>(Infinity);
} }
@bindThis @bindThis

View file

@ -239,6 +239,14 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: true, nullable: false, optional: true, nullable: false,
}, },
enableChartsForRemoteUser: {
type: 'boolean',
optional: false, nullable: false,
},
enableChartsForFederatedInstances: {
type: 'boolean',
optional: false, nullable: false,
},
policies: { policies: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
@ -299,7 +307,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable: instance.deeplAuthKey != null,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
useStarForReactionFallback: instance.useStarForReactionFallback,
pinnedUsers: instance.pinnedUsers, pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,
@ -337,6 +344,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
deeplIsPro: instance.deeplIsPro, deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging, enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation, enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
policies: { ...DEFAULT_POLICIES, ...instance.policies }, policies: { ...DEFAULT_POLICIES, ...instance.policies },
}; };
}); });

View file

@ -17,7 +17,6 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
disableRegistration: { type: 'boolean', nullable: true }, disableRegistration: { type: 'boolean', nullable: true },
useStarForReactionFallback: { type: 'boolean', nullable: true },
pinnedUsers: { type: 'array', nullable: true, items: { pinnedUsers: { type: 'array', nullable: true, items: {
type: 'string', type: 'string',
} }, } },
@ -93,6 +92,8 @@ export const paramDef = {
objectStorageS3ForcePathStyle: { type: 'boolean' }, objectStorageS3ForcePathStyle: { type: 'boolean' },
enableIpLogging: { type: 'boolean' }, enableIpLogging: { type: 'boolean' },
enableActiveEmailValidation: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
}, },
required: [], required: [],
} as const; } as const;
@ -114,10 +115,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.disableRegistration = ps.disableRegistration; set.disableRegistration = ps.disableRegistration;
} }
if (typeof ps.useStarForReactionFallback === 'boolean') {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
if (Array.isArray(ps.pinnedUsers)) { if (Array.isArray(ps.pinnedUsers)) {
set.pinnedUsers = ps.pinnedUsers.filter(Boolean); set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
} }
@ -382,6 +379,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.enableActiveEmailValidation = ps.enableActiveEmailValidation; 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); await this.metaService.update(set);
this.moderationLogService.insertModerationLog(me, 'updateMeta'); this.moderationLogService.insertModerationLog(me, 'updateMeta');
}); });

View file

@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { 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'); throw new Error('invalid param');
} }

View file

@ -1,55 +1,56 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
import { Test } from '@nestjs/testing'; 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 { GlobalModule } from '@/GlobalModule.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { CoreModule } from '@/core/CoreModule.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'; import type { TestingModule } from '@nestjs/testing';
describe('DriveService', () => { describe('DriveService', () => {
let app: TestingModule; let app: TestingModule;
let driveService: DriveService; let driveService: DriveService;
const s3Mock = mockClient(S3Client);
beforeEach(async () => { beforeAll(async () => {
app = await Test.createTestingModule({ app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule], imports: [GlobalModule, CoreModule],
providers: [DriveService, S3Service], providers: [DriveService],
}).compile(); }).compile();
app.enableShutdownHooks(); app.enableShutdownHooks();
driveService = app.get<DriveService>(DriveService); driveService = app.get<DriveService>(DriveService);
const s3Service = app.get<S3Service>(S3Service);
const s3 = s3Service.getS3({} as Meta);
// 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<PromiseResult<DeleteObjectOutput, AWSError>> {
const err = new Error('mock') as AWSError;
err.code = 'NoSuchKey';
throw err;
},
} as Request<DeleteObjectOutput, AWSError>;
}); });
beforeEach(async () => {
s3Mock.reset();
});
afterAll(async () => {
await app.close();
}); });
describe('Object storage', () => { 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 () => { test('delete a file with no valid key', async () => {
try { // 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'); await driveService.deleteObjectStorageFile('lol no way');
} catch (err: any) {
console.log(err.cause);
throw err;
}
}); });
}); });
}); });

View file

@ -74,19 +74,19 @@ describe('ReactionService', () => {
}); });
test('fallback - undefined', async () => { test('fallback - undefined', async () => {
assert.strictEqual(await reactionService.toDbReaction(undefined), '👍'); assert.strictEqual(await reactionService.toDbReaction(undefined), '');
}); });
test('fallback - null', async () => { test('fallback - null', async () => {
assert.strictEqual(await reactionService.toDbReaction(null), '👍'); assert.strictEqual(await reactionService.toDbReaction(null), '');
}); });
test('fallback - empty', async () => { test('fallback - empty', async () => {
assert.strictEqual(await reactionService.toDbReaction(''), '👍'); assert.strictEqual(await reactionService.toDbReaction(''), '');
}); });
test('fallback - unknown', async () => { test('fallback - unknown', async () => {
assert.strictEqual(await reactionService.toDbReaction('unknown'), '👍'); assert.strictEqual(await reactionService.toDbReaction('unknown'), '');
}); });
}); });
}); });

View file

@ -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>(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);
});
});
});

View file

@ -0,0 +1,6 @@
import * as misskey from 'misskey-js';
import { Cache } from '@/scripts/cache';
export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity);
export const rolesCache = new Cache(Infinity);
export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity);

View file

@ -109,6 +109,9 @@
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i> <i class="ti ti-minus"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()"> <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
@ -151,7 +154,7 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; 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 { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
@ -192,6 +195,7 @@ const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
@ -392,6 +396,10 @@ function menu(viaKeyboard = false): void {
}).then(focus); }).then(focus);
} }
async function clip() {
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClipPage }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return; if (!isMyRenote) return;
os.popupMenu([{ os.popupMenu([{

View file

@ -114,6 +114,9 @@
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i> <i class="ti ti-minus"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
</button>
<button ref="menuButton" class="button _button" @mousedown="menu()"> <button ref="menuButton" class="button _button" @mousedown="menu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
@ -156,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; 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 { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
@ -196,6 +199,7 @@ const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
@ -384,6 +388,10 @@ function menu(viaKeyboard = false): void {
}).then(focus); }).then(focus);
} }
async function clip() {
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return; if (!isMyRenote) return;
os.popupMenu([{ os.popupMenu([{

View file

@ -1,25 +1,26 @@
<template> <template>
<span v-if="!link" v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<img :class="$style.inner" :src="url" decoding="async"/> <img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<template v-if="user.isCat"> <div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
<div :class="$style.earLeft"/> <div :class="$style.earLeft">
<div :class="$style.earRight"/> <div v-if="useBlurEffect" :class="$style.layer">
</template> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
</span> </div>
<MkA v-else v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" :to="userPage(user)" :target="target"> </div>
<img :class="$style.inner" :src="url" decoding="async"/> <div :class="$style.earRight">
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <div v-if="useBlurEffect" :class="$style.layer">
<template v-if="user.isCat"> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
<div :class="$style.earLeft"/> </div>
<div :class="$style.earRight"/> </div>
</template> </div>
</MkA> </component>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from 'vue'; import { watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy'; import { getStaticImageUrl } from '@/scripts/media-proxy';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
import { acct, userPage } from '@/filters/user'; import { acct, userPage } from '@/filters/user';
@ -27,6 +28,7 @@ import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
const squareAvatars = $ref(defaultStore.state.squareAvatars); const squareAvatars = $ref(defaultStore.state.squareAvatars);
const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
user: misskey.entities.User; user: misskey.entities.User;
@ -45,15 +47,20 @@ const emit = defineEmits<{
(ev: 'click', v: MouseEvent): void; (ev: 'click', v: MouseEvent): void;
}>(); }>();
const bound = $computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.user.avatarUrl) ? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl); : props.user.avatarUrl);
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent): void {
if (props.link) return;
emit('click', ev); emit('click', ev);
} }
let color = $ref(); let color = $ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => { watch(() => props.user.avatarBlurhash, () => {
color = extractAvgColorFromBlurhash(props.user.avatarBlurhash); color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
@ -120,6 +127,25 @@ watch(() => props.user.avatarBlurhash, () => {
} }
.cat { .cat {
> .ears {
contain: strict;
position: absolute;
top: -50%;
left: -50%;
width: 100%;
height: 100%;
padding: 50%;
&.mask {
-webkit-mask:
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%,
linear-gradient(#fff, #fff);
-webkit-mask-composite: destination-out, source-over;
mask:
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%,
linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
}
> .earLeft, > .earLeft,
> .earRight { > .earRight {
contain: strict; contain: strict;
@ -128,7 +154,7 @@ watch(() => props.user.avatarBlurhash, () => {
width: 50%; width: 50%;
background: currentColor; background: currentColor;
&::before { &::after {
contain: strict; contain: strict;
content: ''; content: '';
display: block; display: block;
@ -137,19 +163,70 @@ watch(() => props.user.avatarBlurhash, () => {
margin: 20%; margin: 20%;
background: #df548f; background: #df548f;
} }
> .layer {
contain: strict;
position: absolute;
top: 0;
width: 280%;
height: 280%;
> .plot {
contain: strict;
width: 100%;
height: 100%;
clip-path: path('M0 0H1V1H0z');
transform: scale(32767);
transform-origin: 0 0;
}
}
} }
> .earLeft { > .earLeft {
border-radius: 0 75% 75%;
transform: rotate(37.5deg) skew(30deg); transform: rotate(37.5deg) skew(30deg);
&, &::after {
border-radius: 0 75% 75%;
}
> .layer {
left: 0;
transform:
skew(-30deg)
rotate(-37.5deg)
translate(-2.82842712475%, /* -2 * sqrt(2) */
-38.5857864376%); /* 40 - 2 * sqrt(2) */
> .plot {
background-position: 20% 10%; /* ~= 37.5deg */
}
}
} }
> .earRight { > .earRight {
border-radius: 75% 0 75% 75%;
transform: rotate(-37.5deg) skew(-30deg); transform: rotate(-37.5deg) skew(-30deg);
&, &::after {
border-radius: 75% 0 75% 75%;
}
> .layer {
right: 0;
transform:
skew(30deg)
rotate(37.5deg)
translate(2.82842712475%, /* 2 * sqrt(2) */
-38.5857864376%); /* 40 - 2 * sqrt(2) */
> .plot {
background-position: 80% 10%; /* ~= 37.5deg */
}
}
}
} }
&:hover { &:hover {
> .ears {
> .earLeft { > .earLeft {
animation: earwiggleleft 1s infinite; animation: earwiggleleft 1s infinite;
} }
@ -158,5 +235,6 @@ watch(() => props.user.avatarBlurhash, () => {
animation: earwiggleright 1s infinite; animation: earwiggleright 1s infinite;
} }
} }
}
} }
</style> </style>

View file

@ -10,6 +10,8 @@
<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option> <option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option> <option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option> <option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
<option value="notesLessThanOrEq">{{ i18n.ts._role._condition.notesLessThanOrEq }}</option>
<option value="notesMoreThanOrEq">{{ i18n.ts._role._condition.notesMoreThanOrEq }}</option>
<option value="and">{{ i18n.ts._role._condition.and }}</option> <option value="and">{{ i18n.ts._role._condition.and }}</option>
<option value="or">{{ i18n.ts._role._condition.or }}</option> <option value="or">{{ i18n.ts._role._condition.or }}</option>
<option value="not">{{ i18n.ts._role._condition.not }}</option> <option value="not">{{ i18n.ts._role._condition.not }}</option>
@ -42,7 +44,7 @@
<template #suffix>sec</template> <template #suffix>sec</template>
</MkInput> </MkInput>
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq'].includes(type)" v-model="v.value" type="number"> <MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
</MkInput> </MkInput>
</div> </div>
</template> </template>
@ -91,6 +93,8 @@ const type = computed({
if (t === 'followersMoreThanOrEq') v.value.value = 10; if (t === 'followersMoreThanOrEq') v.value.value = 10;
if (t === 'followingLessThanOrEq') v.value.value = 10; if (t === 'followingLessThanOrEq') v.value.value = 10;
if (t === 'followingMoreThanOrEq') v.value.value = 10; if (t === 'followingMoreThanOrEq') v.value.value = 10;
if (t === 'notesLessThanOrEq') v.value.value = 10;
if (t === 'notesMoreThanOrEq') v.value.value = 10;
v.value.type = t; v.value.type = t;
}, },
}); });

View file

@ -46,7 +46,7 @@ let sensitiveWords: string = $ref('');
async function init() { async function init() {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
sensitiveWords = meta.pinnedUsers.join('\n'); sensitiveWords = meta.sensitiveWords.join('\n');
} }
function save() { function save() {

View file

@ -26,6 +26,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { rolesCache } from '@/cache';
const router = useRouter(); const router = useRouter();
@ -61,6 +62,7 @@ if (props.id) {
} }
async function save() { async function save() {
rolesCache.delete();
if (role) { if (role) {
os.apiWithDialog('admin/roles/update', { os.apiWithDialog('admin/roles/update', {
roleId: role.id, roleId: role.id,

View file

@ -43,6 +43,14 @@
<MkSwitch v-model="emailRequiredForSignup"> <MkSwitch v-model="emailRequiredForSignup">
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template> <template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="enableChartsForRemoteUser">
<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
</MkSwitch>
<MkSwitch v-model="enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
</MkSwitch>
</div> </div>
</FormSection> </FormSection>
@ -175,6 +183,8 @@ let cacheRemoteFiles: boolean = $ref(false);
let enableRegistration: boolean = $ref(false); let enableRegistration: boolean = $ref(false);
let emailRequiredForSignup: boolean = $ref(false); let emailRequiredForSignup: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false);
let enableChartsForRemoteUser: boolean = $ref(false);
let enableChartsForFederatedInstances: boolean = $ref(false);
let swPublicKey: any = $ref(null); let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null); let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref(''); let deeplAuthKey: string = $ref('');
@ -198,6 +208,8 @@ async function init() {
enableRegistration = !meta.disableRegistration; enableRegistration = !meta.disableRegistration;
emailRequiredForSignup = meta.emailRequiredForSignup; emailRequiredForSignup = meta.emailRequiredForSignup;
enableServiceWorker = meta.enableServiceWorker; enableServiceWorker = meta.enableServiceWorker;
enableChartsForRemoteUser = meta.enableChartsForRemoteUser;
enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances;
swPublicKey = meta.swPublickey; swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey; swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey; deeplAuthKey = meta.deeplAuthKey;
@ -222,6 +234,8 @@ function save() {
disableRegistration: !enableRegistration, disableRegistration: !enableRegistration,
emailRequiredForSignup, emailRequiredForSignup,
enableServiceWorker, enableServiceWorker,
enableChartsForRemoteUser,
enableChartsForFederatedInstances,
swPublicKey, swPublicKey,
swPrivateKey, swPrivateKey,
deeplAuthKey, deeplAuthKey,

View file

@ -30,6 +30,7 @@ import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { url } from '@/config'; import { url } from '@/config';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache';
const props = defineProps<{ const props = defineProps<{
clipId: string, clipId: string,
@ -108,6 +109,8 @@ const headerActions = $computed(() => clip && isOwned ? [{
clipId: clip.id, clipId: clip.id,
...result, ...result,
}); });
clipsCache.delete();
}, },
}, ...(clip.isPublic ? [{ }, ...(clip.isPublic ? [{
icon: 'ti ti-share', icon: 'ti ti-share',
@ -133,6 +136,8 @@ const headerActions = $computed(() => clip && isOwned ? [{
await os.apiWithDialog('clips/delete', { await os.apiWithDialog('clips/delete', {
clipId: clip.id, clipId: clip.id,
}); });
clipsCache.delete();
}, },
}] : null); }] : null);

View file

@ -28,6 +28,7 @@ import MkClipPreview from '@/components/MkClipPreview.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { clipsCache } from '@/cache';
const pagination = { const pagination = {
endpoint: 'clips/list' as const, endpoint: 'clips/list' as const,
@ -65,6 +66,8 @@ async function create() {
os.apiWithDialog('clips/create', result); os.apiWithDialog('clips/create', result);
clipsCache.delete();
pagingComponent.reload(); pagingComponent.reload();
} }

View file

@ -24,6 +24,7 @@ import MkAvatars from '@/components/MkAvatars.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { userListsCache } from '@/cache';
const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
@ -38,6 +39,7 @@ async function create() {
}); });
if (canceled) return; if (canceled) return;
await os.apiWithDialog('users/lists/create', { name: name }); await os.apiWithDialog('users/lists/create', { name: name });
userListsCache.delete();
pagingComponent.reload(); pagingComponent.reload();
} }

View file

@ -37,6 +37,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { userListsCache } from '@/cache';
const props = defineProps<{ const props = defineProps<{
listId: string; listId: string;
@ -97,6 +98,8 @@ async function renameList() {
name: name, name: name,
}); });
userListsCache.delete();
list.name = name; list.name = name;
} }
@ -107,10 +110,10 @@ async function deleteList() {
}); });
if (canceled) return; if (canceled) return;
await os.api('users/lists/delete', { await os.apiWithDialog('users/lists/delete', {
listId: list.id, listId: list.id,
}); });
os.success(); userListsCache.delete();
mainRouter.push('/my/lists'); mainRouter.push('/my/lists');
} }

View file

@ -47,6 +47,7 @@
<div class="_gaps_m"> <div class="_gaps_m">
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch> <MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch> <MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
@ -143,6 +144,7 @@ async function reloadAsk() {
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<MkInput v-model="name"> <MkInput v-model="name">
<template #label>Name</template> <template #label>{{ i18n.ts._webhookSettings.name }}</template>
</MkInput> </MkInput>
<MkInput v-model="url" type="url"> <MkInput v-model="url" type="url">
@ -10,24 +10,24 @@
<MkInput v-model="secret"> <MkInput v-model="secret">
<template #prefix><i class="ti ti-lock"></i></template> <template #prefix><i class="ti ti-lock"></i></template>
<template #label>Secret</template> <template #label>{{ i18n.ts._webhookSettings.secret }}</template>
</MkInput> </MkInput>
<FormSection> <FormSection>
<template #label>Events</template> <template #label>{{ i18n.ts._webhookSettings.events }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="event_follow">Follow</MkSwitch> <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
<MkSwitch v-model="event_followed">Followed</MkSwitch> <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
<MkSwitch v-model="event_note">Note</MkSwitch> <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">Reply</MkSwitch> <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">Renote</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">Reaction</MkSwitch> <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">Mention</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>
<MkSwitch v-model="active">Active</MkSwitch> <MkSwitch v-model="active">{{ i18n.ts._webhookSettings.active }}</MkSwitch>
<div class="_buttons"> <div class="_buttons">
<MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<MkInput v-model="name"> <MkInput v-model="name">
<template #label>Name</template> <template #label>{{ i18n.ts._webhookSettings.name }}</template>
</MkInput> </MkInput>
<MkInput v-model="url" type="url"> <MkInput v-model="url" type="url">
@ -10,20 +10,20 @@
<MkInput v-model="secret"> <MkInput v-model="secret">
<template #prefix><i class="ti ti-lock"></i></template> <template #prefix><i class="ti ti-lock"></i></template>
<template #label>Secret</template> <template #label>{{ i18n.ts._webhookSettings.secret }}</template>
</MkInput> </MkInput>
<FormSection> <FormSection>
<template #label>Events</template> <template #label>{{ i18n.ts._webhookSettings.events }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="event_follow">Follow</MkSwitch> <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
<MkSwitch v-model="event_followed">Followed</MkSwitch> <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
<MkSwitch v-model="event_note">Note</MkSwitch> <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">Reply</MkSwitch> <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">Renote</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">Reaction</MkSwitch> <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">Mention</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<FormLink :to="`/settings/webhook/new`"> <FormLink :to="`/settings/webhook/new`">
Create webhook {{ i18n.ts._webhookSettings.createWebhook }}
</FormLink> </FormLink>
<FormSection> <FormSection>
@ -31,6 +31,7 @@ import MkPagination from '@/components/MkPagination.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
const pagination = { const pagination = {
endpoint: 'i/webhooks/list' as const, endpoint: 'i/webhooks/list' as const,

View file

@ -0,0 +1,80 @@
export class Cache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) {
this.lifetime = lifetime;
}
public set(value: T): void {
this.cachedAt = Date.now();
this.value = value;
}
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;
}
public delete() {
this.value = undefined;
this.cachedAt = null;
}
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
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を返すとキャッシュ無効扱いにします
*/
public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
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;
}
}

View file

@ -10,6 +10,81 @@ import { url } from '@/config';
import { noteActions } from '@/store'; import { noteActions } from '@/store';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { getUserMenu } from '@/scripts/get-user-menu'; import { getUserMenu } from '@/scripts/get-user-menu';
import { clipsCache } from '@/cache';
export async function getNoteClipMenu(props: {
note: misskey.entities.Note;
isDeleted: Ref<boolean>;
currentClipPage?: Ref<misskey.entities.Clip>;
}) {
const isRenote = (
props.note.renote != null &&
props.note.text == null &&
props.note.fileIds.length === 0 &&
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
const clips = await clipsCache.fetch(() => os.api('clips/list'));
return [...clips.map(clip => ({
text: clip.name,
action: () => {
claimAchievement('noteClipped1');
os.promiseDialog(
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
null,
async (err) => {
if (err.id === '734806c4-542c-463a-9311-15c512803965') {
const confirm = await os.confirm({
type: 'warning',
text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
});
if (!confirm.canceled) {
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
}
} else {
os.alert({
type: 'error',
text: err.message + '\n' + err.id,
});
}
},
);
},
})), null, {
icon: 'ti ti-plus',
text: i18n.ts.createNew,
action: async () => {
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: {
type: 'string',
label: i18n.ts.name,
},
description: {
type: 'string',
required: false,
multiline: true,
label: i18n.ts.description,
},
isPublic: {
type: 'boolean',
label: i18n.ts.public,
default: false,
},
});
if (canceled) return;
const clip = await os.apiWithDialog('clips/create', result);
clipsCache.delete();
claimAchievement('noteClipped1');
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
},
}];
}
export function getNoteMenu(props: { export function getNoteMenu(props: {
note: misskey.entities.Note; note: misskey.entities.Note;
@ -208,64 +283,7 @@ export function getNoteMenu(props: {
type: 'parent', type: 'parent',
icon: 'ti ti-paperclip', icon: 'ti ti-paperclip',
text: i18n.ts.clip, text: i18n.ts.clip,
children: async () => { children: () => getNoteClipMenu(props),
const clips = await os.api('clips/list');
return [{
icon: 'ti ti-plus',
text: i18n.ts.createNew,
action: async () => {
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
name: {
type: 'string',
label: i18n.ts.name,
},
description: {
type: 'string',
required: false,
multiline: true,
label: i18n.ts.description,
},
isPublic: {
type: 'boolean',
label: i18n.ts.public,
default: false,
},
});
if (canceled) return;
const clip = await os.apiWithDialog('clips/create', result);
claimAchievement('noteClipped1');
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
},
}, null, ...clips.map(clip => ({
text: clip.name,
action: () => {
claimAchievement('noteClipped1');
os.promiseDialog(
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
null,
async (err) => {
if (err.id === '734806c4-542c-463a-9311-15c512803965') {
const confirm = await os.confirm({
type: 'warning',
text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
});
if (!confirm.canceled) {
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
}
} else {
os.alert({
type: 'error',
text: err.message + '\n' + err.id,
});
}
},
);
},
}))];
},
}, },
statePromise.then(state => state.isMutedThread ? { statePromise.then(state => state.isMutedThread ? {
icon: 'ti ti-message-off', icon: 'ti ti-message-off',

View file

@ -8,6 +8,7 @@ import { userActions } from '@/store';
import { $i, iAmModerator } from '@/account'; import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { Router } from '@/nirax'; import { Router } from '@/nirax';
import { rolesCache, userListsCache } from '@/cache';
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null; const meId = $i ? $i.id : null;
@ -126,7 +127,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
icon: 'ti ti-list', icon: 'ti ti-list',
text: i18n.ts.addToList, text: i18n.ts.addToList,
children: async () => { children: async () => {
const lists = await os.api('users/lists/list'); const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
return lists.map(list => ({ return lists.map(list => ({
text: list.name, text: list.name,
@ -147,7 +148,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
icon: 'ti ti-badges', icon: 'ti ti-badges',
text: i18n.ts.roles, text: i18n.ts.roles,
children: async () => { children: async () => {
const roles = await os.api('admin/roles/list'); const roles = await rolesCache.fetch(() => os.api('admin/roles/list'));
return roles.filter(r => r.target === 'manual').map(r => ({ return roles.filter(r => r.target === 'manual').map(r => ({
text: r.name, text: r.name,

View file

@ -290,6 +290,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: false, default: false,
}, },
showClipButtonInNoteFooter: {
where: 'device',
default: false,
},
aiChanMode: { aiChanMode: {
where: 'device', where: 'device',
default: false, default: false,

File diff suppressed because it is too large Load diff