diff --git a/CHANGELOG.md b/CHANGELOG.md index dc99ee33fe..0c85e850a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Feat: 二要素認証でパスキーをサポートするようになりました - Feat: 指定したユーザーが投稿したときに通知できるようになりました - Feat: プロフィールでのリンク検証 +- Feat: モデレーションログ機能 - Feat: 通知をテストできるようになりました - Feat: PWAのアイコンが設定できるようになりました - Enhance: サーバー名の略称が設定できるようになりました @@ -79,6 +80,9 @@ - Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正 - Fix: 環境によってはMisskey Webが開けない問題を修正 - Fix: プラグインの権限リストが見れない問題を修正 +- Fix: 複数の階層があるメニューで、短くタップすると正常に動かない場合がある問題を修正 +- Fix: アニメーションがオフのとき、スマホで子メニューの選択ができない問題を修正 +- Fix: ドロワーメニューで、親メニュー項目をマウスでホバーすると子メニューが表示されてしまう問題を修正 ### Server - Change: cacheRemoteFilesの初期値はfalseになりました diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 4d5872c64d..f36641128c 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1140,6 +1140,7 @@ _plugin: install: "ثبّت إضافات" installWarn: "رجاءً لا تثبت إضافات غير موثوقة." manage: "إدارة الإضافات" + viewSource: "اظهر المصدر" _preferencesBackups: createdAt: "تم إنشاؤه: {date} {time}" updatedAt: "آخر تحديث: {date} {time}" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index f3475de225..e0e1e29433 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -889,6 +889,7 @@ _plugin: install: "প্লাগইন ইন্সটল করুন" installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।" manage: "প্লাগইন ম্যানেজ করুন" + viewSource: "উৎস দেখুন" _registry: scope: "স্কোপ" key: "কী" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 9a751abc78..36a104bdc3 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1492,6 +1492,7 @@ _plugin: install: "Instalovat plugin" installWarn: "Neinstalujte nedůvěryhodné pluginy." manage: "Správce pluginů" + viewSource: "Zobrazit zdroj" _preferencesBackups: list: "Vytvořit backup" saveNew: "Uložit novou zálohu" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 596a6e5fd8..b11cb7e5b9 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -710,6 +710,7 @@ lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren" loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen" disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" +highlightSensitiveMedia: "Sensitive Medien markieren" verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen." notSet: "Nicht konfiguriert" emailVerified: "Email-Adresse bestätigt" @@ -913,7 +914,7 @@ typeToConfirm: "Bitte gib zur Bestätigung {x} ein" deleteAccount: "Benutzerkonto löschen" document: "Dokumentation" numberOfPageCache: "Seitencachegröße" -numberOfPageCacheDescription: "Das Erhöhen dieses Caches führt zu einer angenehmerern Benutzererfahrung, erhöht aber Serverlast und Arbeitsspeicherauslastung." +numberOfPageCacheDescription: "Das Erhöhen dieses Caches führt zu einer angenehmerern Benutzererfahrung, aber erhöht Last und Arbeitsspeicherauslastung auf dem Nutzergerät." logoutConfirm: "Wirklich abmelden?" lastActiveDate: "Zuletzt verwendet am" statusbar: "Statusleiste" @@ -1116,6 +1117,8 @@ keepScreenOn: "Bildschirm angeschaltet lassen" verifiedLink: "Link-Besitz wurde verifiziert" notifyNotes: "Über neue Notizen benachrichtigen" unnotifyNotes: "Nicht über neue Notizen benachrichtigen" +authentication: "Authentifikation" +authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren" _announcement: forExistingUsers: "Nur für existierende Nutzer" forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." @@ -1149,6 +1152,8 @@ _serverSettings: appIconStyleRecommendation: "Da das Icon zu einem Kreis oder Quadrat zugeschnitten wird, wird ein Icon mit gefülltem Margin um den Inhalt herum empfohlen." appIconResolutionMustBe: "Die Mindestauflösung ist {resolution}." manifestJsonOverride: "Überschreiben von manifest.json" + shortName: "Abkürzung" + shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist." _accountMigration: moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFromSub: "Alias für ein anderes Konto erstellen" @@ -1529,6 +1534,7 @@ _plugin: install: "Plugins installieren" installWarn: "Installiere bitte nur vertrauenswürdige Plugins." manage: "Plugins verwalten" + viewSource: "Quelltext anzeigen" _preferencesBackups: list: "Erstellte Backups" saveNew: "Neu erstellen" @@ -1794,6 +1800,7 @@ _antennaSources: homeTimeline: "Notizen von Benutzern, denen gefolgt wird" users: "Notizen von einem oder mehreren angegebenen Benutzern" userList: "Notizen von allen Benutzern einer Liste" + userBlacklist: "Alle Notizen abgesehen derer angegebener Benutzer" _weekday: sunday: "Sonntag" monday: "Montag" @@ -2022,6 +2029,7 @@ _notification: notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus" _types: all: "Alle" + note: "Neue Notizen" follow: "Neue Follower" mention: "Erwähnungen" reply: "Antworten" diff --git a/locales/en-US.yml b/locales/en-US.yml index e3635fd9dd..f56a70f054 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -714,6 +714,7 @@ lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", yo alwaysMarkSensitive: "Mark as sensitive by default" loadRawImages: "Load original images instead of showing thumbnails" disableShowingAnimatedImages: "Don't play animated images" +highlightSensitiveMedia: "Highlight sensitive media" verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." notSet: "Not set" emailVerified: "Email has been verified" @@ -917,7 +918,7 @@ typeToConfirm: "Please enter {x} to confirm" deleteAccount: "Delete account" document: "Documentation" numberOfPageCache: "Number of cached pages" -numberOfPageCacheDescription: "Increasing this number will improve convenience for users but cause more server load as well as more memory to be used." +numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device." logoutConfirm: "Really log out?" lastActiveDate: "Last used at" statusbar: "Status bar" @@ -1123,6 +1124,8 @@ keepScreenOn: "Keep screen on" verifiedLink: "Link ownership has been verified" notifyNotes: "Notify about new notes" unnotifyNotes: "Stop notifying about new notes" +authentication: "Authentication" +authenticationRequiredToContinue: "Please authenticate to continue" _announcement: forExistingUsers: "Existing users only" forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." @@ -1156,6 +1159,8 @@ _serverSettings: appIconStyleRecommendation: "As the icon may be cropped to a square or circle, an icon with colored margin around the content is recommended." appIconResolutionMustBe: "The minimum resolution is {resolution}." manifestJsonOverride: "manifest.json Override" + shortName: "Short name" + shortNameDescription: "A shorthand for the instance's name that can be displayed if the full official name is long." _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1536,6 +1541,7 @@ _plugin: install: "Install plugins" installWarn: "Please do not install untrustworthy plugins." manage: "Manage plugins" + viewSource: "View source" _preferencesBackups: list: "Created backups" saveNew: "Save new backup" @@ -1803,6 +1809,7 @@ _antennaSources: homeTimeline: "Notes from followed users" users: "Notes from specific users" userList: "Notes from a specified list of users" + userBlacklist: "All notes except for those of one or more specified users" _weekday: sunday: "Sunday" monday: "Monday" @@ -2032,6 +2039,7 @@ _notification: notificationWillBeDisplayedLikeThis: "Notifications look like this" _types: all: "All" + note: "New notes" follow: "New followers" mention: "Mentions" reply: "Replies" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index bfa779d78a..c6bb2c10de 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1518,6 +1518,7 @@ _plugin: install: "Instalar plugins" installWarn: "Por favor no instale plugins que no son de confianza" manage: "Gestionar plugins" + viewSource: "Ver la fuente" _preferencesBackups: list: "Respaldos creados" saveNew: "Guardar nuevo respaldo" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 64a59522f2..71446667d6 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -272,6 +272,7 @@ startMessaging: "Commencer à discuter" nUsersRead: "Lu par {n} personnes" agreeTo: "J’accepte {0}" agree: "Accepter" +agreeBelow: "J’accepte ce qui suit" basicNotesBeforeCreateAccount: "Notes importantes" termsOfService: "Conditions d'utilisation" start: "Commencer" @@ -406,6 +407,7 @@ aboutMisskey: "À propos de Misskey" administrator: "Administrateur" token: "Jeton" 2fa: "Authentification à deux facteurs" +setupOf2fa: "Configuration de l’authentification à deux facteurs" totp: "Application d'authentification" totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification" moderator: "Modérateur·rice·s" @@ -413,6 +415,7 @@ moderation: "Modérations" moderationNote: "Note de modération" addModerationNote: "Ajouter une note de modération" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" +securityKeyAndPasskey: "Sécurité et clés de sécurité" securityKey: "Clé de sécurité" lastUsed: "Dernier utilisé" lastUsedAt: "Dernière utilisation : {t}" @@ -797,6 +800,7 @@ popularPosts: "Les plus consultées" shareWithNote: "Partager dans une note" ads: "Publicité" expiration: "Échéance" +startingperiod: "Commencer" memo: "Pense-bête" priority: "Priorité" high: "Haute" @@ -958,6 +962,7 @@ internalServerError: "Erreur interne du serveur" copyErrorInfo: "Copier les détails de l’erreur" exploreOtherServers: "Trouver une autre instance" disableFederationOk: "Désactiver" +likeOnly: "Les favoris uniquement" license: "Licence" video: "Vidéo" videos: "Vidéos" @@ -978,6 +983,7 @@ horizontal: "Latéral" serverRules: "Règles du serveur" archive: "Archive" youFollowing: "Abonné·e" +options: "Options" later: "Plus tard" goToMisskey: "Retour vers Misskey" expirationDate: "Date d’expiration" @@ -990,12 +996,24 @@ icon: "Avatar" forYou: "Pour vous" replies: "Répondre" renotes: "Renoter" +loadReplies: "Inclure les réponses" +pinnedList: "Liste épinglée" +notifyNotes: "Notifier à propos des nouvelles notes" +authentication: "Authentification" +authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" _announcement: readConfirmTitle: "Marquer comme lu ?" _initialAccountSetting: profileSetting: "Paramètres du profil" privacySetting: "Paramètres de confidentialité" + initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" + ifYouNeedLearnMore: "Si vous voulez en savoir plus comment utiliser {name}(Misskey), veuillez visiter {link}." + skipAreYouSure: "Désirez-vous ignorer la configuration du profile ?" +_serverSettings: + iconUrl: "URL de l’icône" _accountMigration: + moveFrom: "Migrer un autre compte vers le présent compte" + moveFromSub: "Créer un alias vers un autre compte" moveToLabel: "Compte vers lequel vous migrez :" startMigration: "Migrer" movedTo: "Compte vers lequel vous migrez :" @@ -1052,20 +1070,33 @@ _achievements: _login1000: flavor: "Merci d'utiliser Misskey !" _profileFilled: + title: "Bien préparé" description: "Configuration de votre profil" _markedAsCat: title: "Je suis un chat" + description: "Rendre votre compte comme un chat" flavor: "Je n'ai pas encore de nom" + _following1: + title: "Vous suivez votre premier utilisateur·rice" _following50: title: "Beaucoup d'amis" _followers10: title: "Abonnez-moi !" + _followers100: + title: "Populaire" + _followers500: + title: "Tour radio" + _followers1000: + title: "Influenceur·euse" _iLoveMisskey: title: "J’adore Misskey" description: "Publication « J’❤ #Misskey »" + flavor: "L'équipe de développement de Misskey apprécie vraiment votre aide !" _foundTreasure: title: "Chasse au trésor" description: "Vous avez trouvé le trésor caché" + _client30min: + title: "Pause bien méritée" _postedAtLateNight: flavor: "C’est l’heure d’aller au lit." _postedAt0min0sec: @@ -1074,18 +1105,45 @@ _achievements: flavor: "Tic tac, tic tac, tic tac, ding !" _viewInstanceChart: title: "Analyste" + _outputHelloWorldOnScratchpad: + title: "Bonjour tout le monde !" + _open3windows: + title: "Multi-fenêtres" + _driveFolderCircularReference: + title: "Référence circulaire" + _setNameToSyuilo: + description: "Vous avez spécifié « syuilo » comme nom" + _passedSinceAccountCreated1: + title: "Premier anniversaire" + _passedSinceAccountCreated2: + title: "Second anniversaire" + _passedSinceAccountCreated3: + title: "3ème anniversaire" _loggedInOnBirthday: title: "Joyeux Anniversaire !" + description: "Vous vous êtes connecté à la date de votre anniversaire" _loggedInOnNewYearsDay: title: "Bonne année !" _cookieClicked: flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" + _brainDiver: + flavor: "Misskey-Misskey La-Tu-Ma" _role: + new: "Nouveau rôle" + edit: "Modifier le rôle" name: "Nom du rôle" description: "Description du rôle" permission: "Rôle et autorisations" assignTarget: "Attribuer" condition: "Condition" + isPublic: "Rôle public" + options: "Options" + policies: "Stratégies" + baseRole: "Modèle de rôle" + useBaseValue: "Utiliser la valeur du modèle de rôle" + chooseRoleToAssign: "Sélectionner le rôle à assigner" + iconUrl: "URL de l’icône" + displayOrder: "Classement" priority: "Priorité" _priority: low: "Basse" @@ -1144,6 +1202,7 @@ _plugin: install: "Installation de plugin" installWarn: "N’installez que des extensions provenant de sources de confiance." manage: "Gestion des plugins" + viewSource: "Afficher la source" _preferencesBackups: list: "Sauvegardes créées" saveNew: "Nouvelle sauvegarde" @@ -1330,6 +1389,7 @@ _2fa: securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité." securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil." securityKeyName: "Nom de la clé" + removeKey: "Supprimer la clé de sécurité" removeKeyConfirm: "Voulez-vous supprimer {name} ?" renewTOTPOk: "Reconfigurer" renewTOTPCancel: "Pas maintenant" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index e39b49774d..75baebaae4 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1496,6 +1496,7 @@ _plugin: install: "Memasang plugin" installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai." manage: "Manajemen plugin" + viewSource: "Lihat sumber" _preferencesBackups: list: "Cadangan yang dibuat" saveNew: "Simpan cadangan baru" diff --git a/locales/index.d.ts b/locales/index.d.ts index 8ad3223dfe..8d63df6397 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -425,6 +425,7 @@ export interface Locale { "moderation": string; "moderationNote": string; "addModerationNote": string; + "moderationLogs": string; "nUsersMentioned": string; "securityKeyAndPasskey": string; "securityKey": string; @@ -2258,6 +2259,20 @@ export interface Locale { "mention": string; }; }; + "_moderationLogTypes": { + "assignRole": string; + "unassignRole": string; + "updateRole": string; + "suspend": string; + "unsuspend": string; + "addCustomEmoji": string; + "updateServerSettings": string; + "updateUserNote": string; + "deleteDriveFile": string; + "deleteNote": string; + "createGlobalAnnouncement": string; + "createUserAnnouncement": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 9810e6015a..a69e75a374 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1529,6 +1529,7 @@ _plugin: install: "Installa estensioni" installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili." manage: "Gestisci estensioni" + viewSource: "Visualizza sorgente" _preferencesBackups: list: "Elenco di impostazioni salvate in precedenza" saveNew: "Nuovo salvataggio" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 31b08f8e35..03655580b0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -422,6 +422,7 @@ moderator: "モデレーター" moderation: "モデレーション" moderationNote: "モデレーションノート" addModerationNote: "モデレーションノートを追加する" +moderationLogs: "モデログ" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" @@ -2170,3 +2171,20 @@ _webhookSettings: renote: "Renoteされたとき" reaction: "リアクションがあったとき" mention: "メンションされたとき" + +_moderationLogTypes: + assignRole: "ロールへアサイン" + unassignRole: "ロールのアサイン解除" + updateRole: "ロール設定更新" + suspend: "凍結" + unsuspend: "凍結解除" + addCustomEmoji: "カスタム絵文字追加" + updateServerSettings: "サーバー設定更新" + updateUserNote: "モデレーションノート更新" + deleteDriveFile: "ファイルを削除" + deleteNote: "ノートを削除" + createGlobalAnnouncement: "全体のお知らせを作成" + createUserAnnouncement: "ユーザーへお知らせを作成" + resetPassword: "パスワードをリセット" + suspendRemoteInstance: "リモートサーバーを停止" + unsuspendRemoteInstance: "リモートサーバーを再開" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index d5d414ea77..2c1f718192 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1510,6 +1510,7 @@ _plugin: install: "プラグインのインストール" installWarn: "信頼できへんプラグインはインストールせんとってな" manage: "プラグインの管理" + viewSource: "ソースを表示" _preferencesBackups: list: "作ったバックアップ" saveNew: "新しく保存" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 9e405396ba..48aaa2016b 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1512,6 +1512,7 @@ _plugin: install: "플러그인 설치" installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다." manage: "플러그인 관리" + viewSource: "소스 보기" _preferencesBackups: list: "생성한 백업" saveNew: "새 백업 만들기" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 065e228c0c..1e9d61706b 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -925,6 +925,7 @@ _plugin: install: "Zainstaluj wtyczki" installWarn: "Nie instaluj niezaufanych wtyczek." manage: "Zarządzanie wtyczkami" + viewSource: "Zobacz źródło" _preferencesBackups: list: "Utworzone kopie zapasowe" saveNew: "Zapisz nową kopię zapasową" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index edf531dfcc..0bd5db9268 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1427,6 +1427,7 @@ _plugin: install: "Установка расширений" installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." manage: "Управление расширениями" + viewSource: "Просмотр исходника" _preferencesBackups: list: "Существующие резервные копии" saveNew: "Создать резервную копию" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index a5639ce59b..8d2af55db9 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -978,6 +978,7 @@ _plugin: install: "Inštalova pluginy" installWarn: "Prosím neinštalujte nedôveryhodné pluginy." manage: "Spravovanie pluginov" + viewSource: "Ukázať zdroj" _preferencesBackups: list: "Vytvorené zálohy" saveNew: "Uložiť novú" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index f9262fea7e..cf2f3b9a23 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1509,6 +1509,7 @@ _plugin: install: "ติดตั้งปลั๊กอิน" installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ" manage: "จัดการปลั๊กอิน" + viewSource: "ดูต้นฉบับ" _preferencesBackups: list: "สร้างการสำรองข้อมูล" saveNew: "บันทึกใหม่" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 777933bf53..1516272c60 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1180,6 +1180,7 @@ _plugin: install: "Встановити плагін" installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте." manage: "Керування плагінами" + viewSource: "Переглянути вихідний код" _preferencesBackups: list: "Створені бекапи" saveNew: "Зберегти як новий" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index dec9e7f888..40688e98f3 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1343,6 +1343,7 @@ _plugin: install: "Cài đặt tiện ích" installWarn: "Vui lòng không cài đặt những tiện ích đáng ngờ." manage: "Quản lý plugin" + viewSource: "Xem mã nguồn" _preferencesBackups: list: "Tạo sao lưu" saveNew: "Lưu bản sao lưu" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 3026682890..f1512ac348 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -710,6 +710,7 @@ lockedAccountInfo: "即使启用该功能,只要您不将帖子可见范围设 alwaysMarkSensitive: "默认将媒体文件标记为敏感内容" loadRawImages: "添加附件图像的缩略图时使用原始图像质量" disableShowingAnimatedImages: "不播放动画" +highlightSensitiveMedia: "高亮显示敏感媒体" verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。" notSet: "未设置" emailVerified: "电子邮件地址已验证" @@ -1116,6 +1117,8 @@ keepScreenOn: "保持设备屏幕开启" verifiedLink: "已验证的链接" notifyNotes: "打开发帖通知" unnotifyNotes: "关闭发帖通知" +authentication: "验证" +authenticationRequiredToContinue: "要继续,请先进行验证" _announcement: forExistingUsers: "仅限现有用户" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" @@ -1529,6 +1532,7 @@ _plugin: install: "安装插件" installWarn: "请不要安装不可信的插件。" manage: "管理插件..." + viewSource: "查看源代码" _preferencesBackups: list: "已创建的备份" saveNew: "另存为" @@ -1794,6 +1798,7 @@ _antennaSources: homeTimeline: "已关注用户的帖子" users: "来自指定用户的帖子" userList: "来自指定列表中的帖子" + userBlacklist: "除掉已选择用户后所有的帖子" _weekday: sunday: "星期日" monday: "星期一" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index a61e3b242b..a031730a37 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -321,7 +321,7 @@ copyUrl: "複製URL" rename: "重新命名" avatar: "大頭貼" banner: "橫幅" -displayOfSensitiveMedia: "顯示敏感媒體" +displayOfSensitiveMedia: "敏感檔案的顯示" whenServerDisconnected: "與伺服器的連接中斷時" disconnectedFromServer: "與伺服器中斷連線" reload: "重新整理" @@ -490,7 +490,7 @@ createAccount: "建立帳戶" existingAccount: "現有帳戶" regenerate: "再次生成" fontSize: "字體大小" -mediaListWithOneImageAppearance: "只有一張圖片時的媒體列表高度" +mediaListWithOneImageAppearance: "只有一張圖片時的檔案列表高度" limitTo: "上限為 {x}" noFollowRequests: "沒有追隨您的請求" openImageInNewTab: "於新分頁中開啟圖片" @@ -707,9 +707,10 @@ driveUsage: "雲端硬碟使用量" noCrawle: "拒絕搜尋引擎索引" noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。" lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。" -alwaysMarkSensitive: "預設將多媒體標記為敏感內容" +alwaysMarkSensitive: "預設標記檔案為敏感內容" loadRawImages: "以原始圖檔顯示附件圖檔的縮圖" disableShowingAnimatedImages: "不播放動態圖檔" +highlightSensitiveMedia: "強調敏感標記" verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。" notSet: "未設定" emailVerified: "已成功驗證您的電郵" @@ -926,7 +927,7 @@ type: "類型" speed: "速度" slow: "慢" fast: "快" -sensitiveMediaDetection: "敏感性媒體的檢測" +sensitiveMediaDetection: "敏感檔案的檢測" localOnly: "僅限本地" remoteOnly: "僅限遠端" failedToUpload: "上傳失敗" @@ -935,7 +936,7 @@ cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此 cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。" beta: "測試版" enableAutoSensitive: "自動 NSFW 判定" -enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷多媒體內容是否需要標記 NSFW。即使關閉此功能,也可能會依實例規則而自動啟用。" +enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依實例規則而自動啟用。" activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。" navbar: "導覽列" shuffle: "隨機" @@ -1116,6 +1117,8 @@ keepScreenOn: "保持設備螢幕開啟" verifiedLink: "已驗證連結" notifyNotes: "開啟貼文通知" unnotifyNotes: "關閉貼文通知" +authentication: "驗證" +authenticationRequiredToContinue: "請於繼續前完成驗證" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -1149,6 +1152,8 @@ _serverSettings: appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。" appIconResolutionMustBe: "解析度必須為 {resolution}。" manifestJsonOverride: "覆寫 manifest.json" + shortName: "簡稱" + shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" @@ -1478,7 +1483,7 @@ _role: or: "~或~" not: "~否" _sensitiveMediaDetection: - description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。" + description: "您可以使用機器學習自動檢測敏感檔案以便審查。這會稍微增加伺服器負荷。" sensitivity: "檢測敏感度" sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" setSensitiveFlagAutomatically: "設定 NSFW 標籤" @@ -1529,6 +1534,7 @@ _plugin: install: "安裝外掛組件" installWarn: "請不要安裝來源不明的外掛。" manage: "管理外掛" + viewSource: "檢視原始碼" _preferencesBackups: list: "已備份的設定檔" saveNew: "另存新檔" @@ -1563,9 +1569,9 @@ _aboutMisskey: morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" patrons: "贊助者" _displayOfSensitiveMedia: - respect: "隱藏被標記為敏感的多媒體內容" - ignore: "不隱藏被標記為敏感的多媒體內容" - force: "隱藏所有多媒體內容" + respect: "隱藏敏感檔案" + ignore: "顯示敏感檔案" + force: "隱藏所有檔案" _instanceTicker: none: "隱藏" remote: "向遠端使用者顯示" @@ -1794,6 +1800,7 @@ _antennaSources: homeTimeline: "來自已追隨使用者的貼文" users: "來自特定使用者的貼文" userList: "來自特定清單中的貼文" + userBlacklist: "除指定使用者外的所有貼文" _weekday: sunday: "週日" monday: "週一" @@ -2022,6 +2029,7 @@ _notification: notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示" _types: all: "全部 " + note: "使用者的最新貼文" follow: "追隨中" mention: "提及" reply: "回覆" diff --git a/package.json b/package.json index 8fc177e0cd..8e5540f68d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.9.0-rc.1-prismisskey.2", + "version": "2023.9.0-rc.2", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 70f37516a4..31fcb139ea 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -12,6 +12,7 @@ import { bindThis } from '@/decorators.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; @Injectable() export class AnnouncementService { @@ -24,6 +25,7 @@ export class AnnouncementService { private idService: IdService, private globalEventService: GlobalEventService, + private moderationLogService: ModerationLogService, ) { } @@ -58,7 +60,7 @@ export class AnnouncementService { } @bindThis - public async create(values: Partial): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { + public async create(values: Partial, moderator: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { const announcement = await this.announcementsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), @@ -79,10 +81,21 @@ export class AnnouncementService { this.globalEventService.publishMainStream(values.userId, 'announcementCreated', { announcement: packed, }); + + this.moderationLogService.log(moderator, 'createUserAnnouncement', { + announcementId: announcement.id, + announcement: announcement, + userId: values.userId, + }); } else { this.globalEventService.publishBroadcastStream('announcementCreated', { announcement: packed, }); + + this.moderationLogService.log(moderator, 'createGlobalAnnouncement', { + announcementId: announcement.id, + announcement: announcement, + }); } return { diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index e015d3dc41..2ff062142c 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -119,6 +120,7 @@ export class DriveService { private globalEventService: GlobalEventService, private queueService: QueueService, private roleService: RoleService, + private moderationLogService: ModerationLogService, private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, @@ -648,7 +650,7 @@ export class DriveService { } @bindThis - public async deleteFile(file: MiDriveFile, isExpired = false) { + public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -671,11 +673,11 @@ export class DriveService { } } - this.deletePostProcess(file, isExpired); + this.deletePostProcess(file, isExpired, deleter); } @bindThis - public async deleteFileSync(file: MiDriveFile, isExpired = false) { + public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -702,11 +704,11 @@ export class DriveService { await Promise.all(promises); } - this.deletePostProcess(file, isExpired); + this.deletePostProcess(file, isExpired, deleter); } @bindThis - private async deletePostProcess(file: MiDriveFile, isExpired = false) { + private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) { // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { this.driveFilesRepository.update(file.id, { @@ -733,6 +735,17 @@ export class DriveService { this.instanceChart.updateDrive(file, false); } } + + if (file.userId) { + this.globalEventService.publishDriveStream(file.userId, 'fileDeleted', file.id); + } + + if (deleter && await this.roleService.isModerator(deleter) && (file.userId !== deleter.id)) { + this.moderationLogService.log(deleter, 'deleteDriveFile', { + fileId: file.id, + fileUserId: file.userId, + }); + } } @bindThis diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index b0e5b794d0..f7f9063d92 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; +import { ModerationLogPayloads, moderationLogTypes } from '@/types.js'; @Injectable() export class ModerationLogService { @@ -21,13 +22,13 @@ export class ModerationLogService { } @bindThis - public async insertModerationLog(moderator: { id: MiUser['id'] }, type: string, info?: Record) { + public async log(moderator: { id: MiUser['id'] }, type: T, info?: ModerationLogPayloads[T]) { await this.moderationLogsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), userId: moderator.id, type: type, - info: info ?? {}, + info: (info as any) ?? {}, }); } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 69fff36a02..c99f92b9cb 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -23,6 +23,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; @Injectable() export class NoteDeleteService { @@ -48,6 +49,7 @@ export class NoteDeleteService { private apDeliverManagerService: ApDeliverManagerService, private metaService: MetaService, private searchService: SearchService, + private moderationLogService: ModerationLogService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, @@ -58,7 +60,7 @@ export class NoteDeleteService { * @param user 投稿者 * @param note 投稿 */ - async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false) { + async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { const deletedAt = new Date(); const cascadingNotes = await this.findCascadingNotes(note); @@ -131,6 +133,14 @@ export class NoteDeleteService { id: note.id, userId: user.id, }); + + if (deleter && (note.userId !== deleter.id)) { + this.moderationLogService.log(deleter, 'deleteNote', { + noteId: note.id, + noteUserId: note.userId, + note: note, + }); + } } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 518f283695..39f21ecec4 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -18,6 +18,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { StreamMessages } from '@/server/api/stream/types.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @@ -98,6 +99,7 @@ export class RoleService implements OnApplicationShutdown { private userEntityService: UserEntityService, private globalEventService: GlobalEventService, private idService: IdService, + private moderationLogService: ModerationLogService, ) { //this.onMessage = this.onMessage.bind(this); @@ -374,9 +376,11 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null): Promise { + public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise { const now = new Date(); + const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); + const existing = await this.roleAssignmentsRepository.findOneBy({ roleId: roleId, userId: userId, @@ -406,10 +410,19 @@ export class RoleService implements OnApplicationShutdown { }); this.globalEventService.publishInternalEvent('userRoleAssigned', created); + + if (moderator) { + this.moderationLogService.log(moderator, 'assignRole', { + roleId: roleId, + roleName: role.name, + userId: userId, + expiresAt: expiresAt ? expiresAt.toISOString() : null, + }); + } } @bindThis - public async unassign(userId: MiUser['id'], roleId: MiRole['id']): Promise { + public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise { const now = new Date(); const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); @@ -430,6 +443,15 @@ export class RoleService implements OnApplicationShutdown { }); this.globalEventService.publishInternalEvent('userRoleUnassigned', existing); + + if (moderator) { + const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); + this.moderationLogService.log(moderator, 'unassignRole', { + roleId: roleId, + roleName: role.name, + userId: userId, + }); + } } @bindThis @@ -451,6 +473,26 @@ export class RoleService implements OnApplicationShutdown { redisPipeline.exec(); } + @bindThis + public async update(role: MiRole, params: Partial, moderator?: MiUser): Promise { + const date = new Date(); + await this.rolesRepository.update(role.id, { + updatedAt: date, + ...params, + }); + + const updated = await this.rolesRepository.findOneByOrFail({ id: role.id }); + this.globalEventService.publishInternalEvent('roleUpdated', updated); + + if (moderator) { + this.moderationLogService.log(moderator, 'updateRole', { + roleId: role.id, + before: role, + after: updated, + }); + } + } + @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index c2f69bb159..262b36b9a4 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -81,7 +81,7 @@ export default class extends Endpoint { // eslint- forExistingUsers: ps.forExistingUsers, needConfirmationToRead: ps.needConfirmationToRead, userId: ps.userId, - }); + }, me); return packed; }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 4ae1b8aa11..7b9440dcf5 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -99,9 +99,7 @@ export default class extends Endpoint { // eslint- roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], }); - - - this.moderationLogService.insertModerationLog(me, 'addEmoji', { + this.moderationLogService.log(me, 'addCustomEmoji', { emojiId: emoji.id, }); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index fbb91837f2..357bf83e87 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -9,6 +9,7 @@ import type { InstancesRepository } from '@/models/_.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -34,6 +35,7 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private federatedInstanceService: FederatedInstanceService, + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) }); @@ -42,9 +44,23 @@ export default class extends Endpoint { // eslint- throw new Error('instance not found'); } - this.federatedInstanceService.update(instance.id, { + await this.federatedInstanceService.update(instance.id, { isSuspended: ps.isSuspended, }); + + if (instance.isSuspended !== ps.isSuspended) { + if (ps.isSuspended) { + this.moderationLogService.log(me, 'suspendRemoteInstance', { + id: instance.id, + host: instance.host, + }); + } else { + this.moderationLogService.log(me, 'unsuspendRemoteInstance', { + id: instance.id, + host: instance.host, + }); + } + } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts index b61c580034..c9142e9885 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -30,7 +30,7 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { this.queueService.destroy(); - this.moderationLogService.insertModerationLog(me, 'clearQueue'); + this.moderationLogService.log(me, 'clearQueue'); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts index 8d16cddd00..0cba5b4e25 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -70,7 +70,7 @@ export default class extends Endpoint { // eslint- break; } - this.moderationLogService.insertModerationLog(me, 'promoteQueue'); + this.moderationLogService.log(me, 'promoteQueue'); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 0dd4fb4126..6ce7583276 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -46,8 +47,10 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + private moderationLogService: ModerationLogService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); if (user == null) { @@ -69,6 +72,10 @@ export default class extends Endpoint { // eslint- password: hash, }); + this.moderationLogService.log(me, 'resetPassword', { + targetId: user.id, + }); + return { password: passwd, }; diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index 9a005982d4..a0f3edd867 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -83,7 +83,7 @@ export default class extends Endpoint { // eslint- return; } - await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null); + await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts index 0a79296c05..4c27583111 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -81,7 +81,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchUser); } - await this.roleService.unassign(user.id, role.id); + await this.roleService.unassign(user.id, role.id, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 65fdb4b4b4..e4e59e487c 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -9,6 +9,7 @@ import type { RolesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], @@ -70,16 +71,16 @@ export default class extends Endpoint { // eslint- @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - private globalEventService: GlobalEventService, + private roleService: RoleService, ) { - super(meta, paramDef, async (ps) => { - const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } }); - if (!roleExist) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); + if (role == null) { throw new ApiError(meta.errors.noSuchRole); } const date = new Date(); - await this.rolesRepository.update(ps.roleId, { + await this.roleService.update(role, { updatedAt: date, name: ps.name, description: ps.description, @@ -95,9 +96,7 @@ export default class extends Endpoint { // eslint- canEditMembersByModerator: ps.canEditMembersByModerator, displayOrder: ps.displayOrder, policies: ps.policies, - }); - const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId }); - this.globalEventService.publishInternalEvent('roleUpdated', updated); + }, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index d5f97ab149..f87a5a3574 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -62,6 +62,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + type: { type: 'string', nullable: true }, + userId: { type: 'string', format: 'misskey:id', nullable: true }, }, required: [], } as const; @@ -78,6 +80,14 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); + if (ps.type != null) { + query.andWhere('report.type = :type', { type: ps.type }); + } + + if (ps.userId != null) { + query.andWhere('report.userId = :userId', { userId: ps.userId }); + } + const reports = await query.limit(ps.limit).getMany(); return await this.moderationLogEntityService.packMany(reports); diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index bcf12fa4e8..89199f8bff 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -60,7 +60,7 @@ export default class extends Endpoint { // eslint- isSuspended: true, }); - this.moderationLogService.insertModerationLog(me, 'suspend', { + this.moderationLogService.log(me, 'suspend', { targetId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 59e89e15bd..a2779148ed 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -45,7 +45,7 @@ export default class extends Endpoint { // eslint- isSuspended: false, }); - this.moderationLogService.insertModerationLog(me, 'unsuspend', { + this.moderationLogService.log(me, 'unsuspend', { targetId: user.id, }); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index eabf1f306c..ea6ebdd1fe 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -441,8 +441,16 @@ export default class extends Endpoint { // eslint- set.manifestJsonOverride = ps.manifestJsonOverride; } + const before = await this.metaService.fetch(true); + await this.metaService.update(set); - this.moderationLogService.insertModerationLog(me, 'updateMeta'); + + const after = await this.metaService.fetch(true); + + this.moderationLogService.log(me, 'updateServerSettings', { + before, + after, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index c86a43977e..2e9fd5ad29 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -32,6 +33,8 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -40,9 +43,17 @@ export default class extends Endpoint { // eslint- throw new Error('user not found'); } + const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + await this.userProfilesRepository.update({ userId: user.id }, { moderationNote: ps.text, }); + + this.moderationLogService.log(me, 'updateUserNote', { + userId: user.id, + before: currentProfile.moderationNote, + after: ps.text, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index d7fdc81cdb..f46bf49965 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -65,11 +65,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } - // Delete - await this.driveService.deleteFile(file); - - // Publish fileDeleted event - this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id); + await this.driveService.deleteFile(file, false, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 74062a58f5..55aaaf4f78 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -70,7 +70,7 @@ export default class extends Endpoint { // eslint- } // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため - await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note); + await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note, false, me); }); } } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 0a28d88d08..0a53e77b79 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,3 +26,96 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export const ffVisibility = ['public', 'followers', 'private'] as const; + +export const moderationLogTypes = [ + 'updateServerSettings', + 'suspend', + 'unsuspend', + 'updateUserNote', + 'addCustomEmoji', + 'assignRole', + 'unassignRole', + 'updateRole', + 'deleteRole', + 'clearQueue', + 'promoteQueue', + 'deleteDriveFile', + 'deleteNote', + 'createGlobalAnnouncement', + 'createUserAnnouncement', + 'resetPassword', + 'suspendRemoteInstance', + 'unsuspendRemoteInstance', +] as const; + +export type ModerationLogPayloads = { + updateServerSettings: { + before: any | null; + after: any | null; + }; + suspend: { + targetId: string; + }; + unsuspend: { + targetId: string; + }; + updateUserNote: { + userId: string; + before: string | null; + after: string | null; + }; + addCustomEmoji: { + emojiId: string; + }; + assignRole: { + userId: string; + roleId: string; + roleName: string; + expiresAt: string | null; + }; + unassignRole: { + userId: string; + roleId: string; + roleName: string; + }; + updateRole: { + roleId: string; + before: any; + after: any; + }; + deleteRole: { + roleId: string; + roleName: string; + }; + clearQueue: Record; + promoteQueue: Record; + deleteDriveFile: { + fileId: string; + fileUserId: string | null; + }; + deleteNote: { + noteId: string; + noteUserId: string; + note: any; + }; + createGlobalAnnouncement: { + announcementId: string; + announcement: any; + }; + createUserAnnouncement: { + announcementId: string; + announcement: any; + userId: string; + }; + resetPassword: { + targetId: string; + }; + suspendRemoteInstance: { + id: string; + host: string; + }; + unsuspendRemoteInstance: { + id: string; + host: string; + }; +}; diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index 721fbb7345..8f61d91ba9 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -16,6 +16,7 @@ import { genAidx } from '@/misc/id/aidx.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -29,6 +30,7 @@ describe('AnnouncementService', () => { let announcementsRepository: AnnouncementsRepository; let announcementReadsRepository: AnnouncementReadsRepository; let globalEventService: jest.Mocked; + let moderationLogService: jest.Mocked; function createUser(data: Partial = {}) { const un = secureRndstr(16); @@ -71,8 +73,11 @@ describe('AnnouncementService', () => { publishMainStream: jest.fn(), publishBroadcastStream: jest.fn(), }; - } - if (typeof token === 'function') { + } else if (token === ModerationLogService) { + return { + log: jest.fn(), + }; + } else if (typeof token === 'function') { const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; const Mock = moduleMocker.generateFromMetadata(mockMetadata); return new Mock(); @@ -87,6 +92,7 @@ describe('AnnouncementService', () => { announcementsRepository = app.get(DI.announcementsRepository); announcementReadsRepository = app.get(DI.announcementReadsRepository); globalEventService = app.get(GlobalEventService) as jest.Mocked; + moderationLogService = app.get(ModerationLogService) as jest.Mocked; }); afterEach(async () => { @@ -155,10 +161,11 @@ describe('AnnouncementService', () => { describe('create', () => { test('通常', async () => { + const me = await createUser(); const result = await announcementService.create({ title: 'Title', text: 'Text', - }); + }, me); expect(result.raw.title).toBe('Title'); expect(result.packed.title).toBe('Title'); @@ -166,15 +173,17 @@ describe('AnnouncementService', () => { expect(globalEventService.publishBroadcastStream).toHaveBeenCalled(); expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated'); expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed); + expect(moderationLogService.log).toHaveBeenCalled(); }); test('ユーザー指定', async () => { + const me = await createUser(); const user = await createUser(); const result = await announcementService.create({ title: 'Title', text: 'Text', userId: user.id, - }); + }, me); expect(result.raw.title).toBe('Title'); expect(result.packed.title).toBe('Title'); @@ -184,6 +193,7 @@ describe('AnnouncementService', () => { expect(globalEventService.publishMainStream.mock.lastCall![0]).toBe(user.id); expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated'); expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed); + expect(moderationLogService.log).toHaveBeenCalled(); }); }); diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue index cf955752f3..962dcd91eb 100644 --- a/packages/frontend/src/components/MkMenu.child.vue +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue new file mode 100644 index 0000000000..da043f1b8f --- /dev/null +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index e658477bbc..415d2f1974 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -395,6 +395,10 @@ export const routes = [{ path: '/abuses', name: 'abuses', component: page(() => import('./pages/admin/abuses.vue')), + }, { + path: '/modlog', + name: 'modlog', + component: page(() => import('./pages/admin/modlog.vue')), }, { path: '/settings', name: 'settings', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5cd679bce5..adedea8755 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2278,7 +2278,8 @@ declare namespace entities { Invite, InviteLimit, UserSorting, - OriginType + OriginType, + ModerationLog } } export { entities } @@ -2516,6 +2517,59 @@ type MessagingMessage = { groupId: UserGroup['id'] | null; }; +// @public (undocumented) +type ModerationLog = { + id: ID; + createdAt: DateString; + userId: User['id']; + user: UserDetailed | null; +} & ({ + type: 'updateServerSettings'; + info: ModerationLogPayloads['updateServerSettings']; +} | { + type: 'suspend'; + info: ModerationLogPayloads['suspend']; +} | { + type: 'unsuspend'; + info: ModerationLogPayloads['unsuspend']; +} | { + type: 'updateUserNote'; + info: ModerationLogPayloads['updateUserNote']; +} | { + type: 'addCustomEmoji'; + info: ModerationLogPayloads['addCustomEmoji']; +} | { + type: 'assignRole'; + info: ModerationLogPayloads['assignRole']; +} | { + type: 'unassignRole'; + info: ModerationLogPayloads['unassignRole']; +} | { + type: 'updateRole'; + info: ModerationLogPayloads['updateRole']; +} | { + type: 'deleteRole'; + info: ModerationLogPayloads['deleteRole']; +} | { + type: 'clearQueue'; + info: ModerationLogPayloads['clearQueue']; +} | { + type: 'promoteQueue'; + info: ModerationLogPayloads['promoteQueue']; +} | { + type: 'resetPassword'; + info: ModerationLogPayloads['resetPassword']; +} | { + type: 'suspendRemoteInstance'; + info: ModerationLogPayloads['suspendRemoteInstance']; +} | { + type: 'unsuspendRemoteInstance'; + info: ModerationLogPayloads['unsuspendRemoteInstance']; +}); + +// @public (undocumented) +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "assignRole", "unassignRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance"]; + // @public (undocumented) export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; @@ -2861,6 +2915,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/entities.ts:579:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 6cf6dc07e7..a8962ab3d3 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -44,3 +44,96 @@ export const permissions = [ 'read:flash-likes', 'write:flash-likes', ]; + +export const moderationLogTypes = [ + 'updateServerSettings', + 'suspend', + 'unsuspend', + 'updateUserNote', + 'addCustomEmoji', + 'assignRole', + 'unassignRole', + 'updateRole', + 'deleteRole', + 'clearQueue', + 'promoteQueue', + 'deleteDriveFile', + 'deleteNote', + 'createGlobalAnnouncement', + 'createUserAnnouncement', + 'resetPassword', + 'suspendRemoteInstance', + 'unsuspendRemoteInstance', +] as const; + +export type ModerationLogPayloads = { + updateServerSettings: { + before: any | null; + after: any | null; + }; + suspend: { + targetId: string; + }; + unsuspend: { + targetId: string; + }; + updateUserNote: { + userId: string; + before: string | null; + after: string | null; + }; + addCustomEmoji: { + emojiId: string; + }; + assignRole: { + userId: string; + roleId: string; + roleName: string; + expiresAt: string | null; + }; + unassignRole: { + userId: string; + roleId: string; + roleName: string; + }; + updateRole: { + roleId: string; + before: any; + after: any; + }; + deleteRole: { + roleId: string; + roleName: string; + }; + clearQueue: Record; + promoteQueue: Record; + deleteDriveFile: { + fileId: string; + fileUserId: string | null; + }; + deleteNote: { + noteId: string; + noteUserId: string; + note: any; + }; + createGlobalAnnouncement: { + announcementId: string; + announcement: any; + }; + createUserAnnouncement: { + announcementId: string; + announcement: any; + userId: string; + }; + resetPassword: { + targetId: string; + }; + suspendRemoteInstance: { + id: string; + host: string; + }; + unsuspendRemoteInstance: { + id: string; + host: string; + }; +}; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 034201f9b9..a1fc8befb4 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,3 +1,5 @@ +import { ModerationLogPayloads } from './consts.js'; + export type ID = string; export type DateString = string; @@ -566,3 +568,52 @@ export type UserSorting = | '+updatedAt' | '-updatedAt'; export type OriginType = 'combined' | 'local' | 'remote'; + +export type ModerationLog = { + id: ID; + createdAt: DateString; + userId: User['id']; + user: UserDetailed | null; +} & ({ + type: 'updateServerSettings'; + info: ModerationLogPayloads['updateServerSettings']; +} | { + type: 'suspend'; + info: ModerationLogPayloads['suspend']; +} | { + type: 'unsuspend'; + info: ModerationLogPayloads['unsuspend']; +} | { + type: 'updateUserNote'; + info: ModerationLogPayloads['updateUserNote']; +} | { + type: 'addCustomEmoji'; + info: ModerationLogPayloads['addCustomEmoji']; +} | { + type: 'assignRole'; + info: ModerationLogPayloads['assignRole']; +} | { + type: 'unassignRole'; + info: ModerationLogPayloads['unassignRole']; +} | { + type: 'updateRole'; + info: ModerationLogPayloads['updateRole']; +} | { + type: 'deleteRole'; + info: ModerationLogPayloads['deleteRole']; +} | { + type: 'clearQueue'; + info: ModerationLogPayloads['clearQueue']; +} | { + type: 'promoteQueue'; + info: ModerationLogPayloads['promoteQueue']; +} | { + type: 'resetPassword'; + info: ModerationLogPayloads['resetPassword']; +} | { + type: 'suspendRemoteInstance'; + info: ModerationLogPayloads['suspendRemoteInstance']; +} | { + type: 'unsuspendRemoteInstance'; + info: ModerationLogPayloads['unsuspendRemoteInstance']; +}); diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index ae4dd31fe0..e78501fdfd 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -17,6 +17,7 @@ export const notificationTypes = consts.notificationTypes; export const noteVisibilities = consts.noteVisibilities; export const mutedNoteReasons = consts.mutedNoteReasons; export const ffVisibility = consts.ffVisibility; +export const moderationLogTypes = consts.moderationLogTypes; // api extractor not supported yet //export * as api from './api.js';