Merge remote-tracking branch 'misskey-dev/develop' into rainbow

# Conflicts:
#	package.json
#	packages/backend/src/server/api/endpoints/admin/emoji/add.ts
#	packages/frontend/src/components/MkMenu.vue
This commit is contained in:
mattyatea 2023-09-23 21:00:15 +09:00
commit d56d84c664
56 changed files with 759 additions and 59 deletions

View file

@ -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になりました

View file

@ -1140,6 +1140,7 @@ _plugin:
install: "ثبّت إضافات"
installWarn: "رجاءً لا تثبت إضافات غير موثوقة."
manage: "إدارة الإضافات"
viewSource: "اظهر المصدر"
_preferencesBackups:
createdAt: "تم إنشاؤه: {date} {time}"
updatedAt: "آخر تحديث: {date} {time}"

View file

@ -889,6 +889,7 @@ _plugin:
install: "প্লাগইন ইন্সটল করুন"
installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।"
manage: "প্লাগইন ম্যানেজ করুন"
viewSource: "উৎস দেখুন"
_registry:
scope: "স্কোপ"
key: "কী"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -272,6 +272,7 @@ startMessaging: "Commencer à discuter"
nUsersRead: "Lu par {n} personnes"
agreeTo: "Jaccepte {0}"
agree: "Accepter"
agreeBelow: "Jaccepte 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 lauthentification à 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 lerreur"
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 dexpiration"
@ -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 licô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: "Jadore 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: "Cest lheure daller 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 licône"
displayOrder: "Classement"
priority: "Priorité"
_priority:
low: "Basse"
@ -1144,6 +1202,7 @@ _plugin:
install: "Installation de plugin"
installWarn: "Ninstallez 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"

View file

@ -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"

15
locales/index.d.ts vendored
View file

@ -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;

View file

@ -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"

View file

@ -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: "リモートサーバーを再開"

View file

@ -1510,6 +1510,7 @@ _plugin:
install: "プラグインのインストール"
installWarn: "信頼できへんプラグインはインストールせんとってな"
manage: "プラグインの管理"
viewSource: "ソースを表示"
_preferencesBackups:
list: "作ったバックアップ"
saveNew: "新しく保存"

View file

@ -1512,6 +1512,7 @@ _plugin:
install: "플러그인 설치"
installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다."
manage: "플러그인 관리"
viewSource: "소스 보기"
_preferencesBackups:
list: "생성한 백업"
saveNew: "새 백업 만들기"

View file

@ -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ą"

View file

@ -1427,6 +1427,7 @@ _plugin:
install: "Установка расширений"
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями"
viewSource: "Просмотр исходника"
_preferencesBackups:
list: "Существующие резервные копии"
saveNew: "Создать резервную копию"

View file

@ -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ú"

View file

@ -1509,6 +1509,7 @@ _plugin:
install: "ติดตั้งปลั๊กอิน"
installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ"
manage: "จัดการปลั๊กอิน"
viewSource: "ดูต้นฉบับ"
_preferencesBackups:
list: "สร้างการสำรองข้อมูล"
saveNew: "บันทึกใหม่"

View file

@ -1180,6 +1180,7 @@ _plugin:
install: "Встановити плагін"
installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте."
manage: "Керування плагінами"
viewSource: "Переглянути вихідний код"
_preferencesBackups:
list: "Створені бекапи"
saveNew: "Зберегти як новий"

View file

@ -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"

View file

@ -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: "星期一"

View file

@ -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: "回覆"

View file

@ -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",

View file

@ -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<MiAnnouncement>): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
public async create(values: Partial<MiAnnouncement>, 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 {

View file

@ -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

View file

@ -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<string, any>) {
public async log<T extends typeof moderationLogTypes[number]>(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) ?? {},
});
}
}

View file

@ -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

View file

@ -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<void> {
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise<void> {
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<void> {
public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise<void> {
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<MiRole>, moderator?: MiUser): Promise<void> {
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);

View file

@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
userId: ps.userId,
});
}, me);
return packed;
});

View file

@ -99,9 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
this.moderationLogService.log(me, 'addCustomEmoji', {
emojiId: emoji.id,
});

View file

@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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,
});
}
}
});
}
}

View file

@ -30,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
this.queueService.destroy();
this.moderationLogService.insertModerationLog(me, 'clearQueue');
this.moderationLogService.log(me, 'clearQueue');
});
}
}

View file

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}
this.moderationLogService.insertModerationLog(me, 'promoteQueue');
this.moderationLogService.log(me, 'promoteQueue');
});
}
}

View file

@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // eslint-
password: hash,
});
this.moderationLogService.log(me, 'resetPassword', {
targetId: user.id,
});
return {
password: passwd,
};

View file

@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
});
}
}

View file

@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchUser);
}
await this.roleService.unassign(user.id, role.id);
await this.roleService.unassign(user.id, role.id, me);
});
}
}

View file

@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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);
});
}
}

View file

@ -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<typeof meta, typeof paramDef> { // 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);

View file

@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSuspended: true,
});
this.moderationLogService.insertModerationLog(me, 'suspend', {
this.moderationLogService.log(me, 'suspend', {
targetId: user.id,
});

View file

@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSuspended: false,
});
this.moderationLogService.insertModerationLog(me, 'unsuspend', {
this.moderationLogService.log(me, 'unsuspend', {
targetId: user.id,
});

View file

@ -441,8 +441,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
});
});
}
}

View file

@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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,
});
});
}
}

View file

@ -65,11 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
});
}
}

View file

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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);
});
}
}

View file

@ -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<string, never>;
promoteQueue: Record<string, never>;
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;
};
};

View file

@ -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<GlobalEventService>;
let moderationLogService: jest.Mocked<ModerationLogService>;
function createUser(data: Partial<MiUser> = {}) {
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<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
@ -87,6 +92,7 @@ describe('AnnouncementService', () => {
announcementsRepository = app.get<AnnouncementsRepository>(DI.announcementsRepository);
announcementReadsRepository = app.get<AnnouncementReadsRepository>(DI.announcementReadsRepository);
globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
});
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();
});
});

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from '@/types/menu';
import { MenuItem } from '@/types/menu.js';
const props = defineProps<{
items: MenuItem[];

View file

@ -40,9 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.switchText">{{ item.text }}</span>
</button>
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item } , { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
<span style="pointer-events: none;">{{ item.text }}</span>
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
</button>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }, { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon, { [$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]"></i>

View file

@ -159,6 +159,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: currentPage?.route.name === 'abuses',
}, {
icon: 'ti ti-list-search',
text: i18n.ts.moderationLogs,
to: '/admin/modlog',
active: currentPage?.route.name === 'modlog',
}],
}, {
title: i18n.ts.settings,

View file

@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder>
<template #label>{{ i18n.ts._moderationLogTypes[log.type] }}</template>
<template #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/>
</template>
<template #suffix>
<MkTime :time="log.createdAt" mode="detail"/>
</template>
<div :class="$style.root">
<div>{{ i18n.ts.moderator }}: {{ log.userId }}</div>
<template v-if="log.type === 'suspend'">
<div>{{ i18n.ts.user }}: {{ log.info.targetId }}</div>
</template>
<template v-else-if="log.type === 'unsuspend'">
<div>{{ i18n.ts.user }}: {{ log.info.targetId }}</div>
</template>
<template v-else-if="log.type === 'assignRole'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div>{{ i18n.ts.role }}: {{ log.info.roleName }} [{{ log.info.roleId }}]</div>
</template>
<template v-else-if="log.type === 'unassignRole'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div>{{ i18n.ts.role }}: {{ log.info.roleName }} [{{ log.info.roleId }}]</div>
</template>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue';
const props = defineProps<{
log: Misskey.entities.ModerationLog;
}>();
</script>
<style lang="scss" module>
.root {
}
.avatar {
width: 18px;
height: 18px;
}
</style>

View file

@ -0,0 +1,67 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div>
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkSelect v-model="type" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.type }}</template>
<option :value="null">{{ i18n.ts.all }}</option>
<option v-for="t in Misskey.moderationLogTypes" :key="t" :value="t">{{ t }}</option>
</MkSelect>
<MkInput v-model="moderatorId" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.moderator }}(ID)</template>
</MkInput>
</div>
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
<div class="_gaps_s">
<XModLog v-for="item in items" :key="item.id" :log="item"/>
</div>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import XHeader from './_header_.vue';
import XModLog from './modlog.ModLog.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
let logs = $shallowRef<InstanceType<typeof MkPagination>>();
let type = $ref(null);
let moderatorId = $ref('');
const pagination = {
endpoint: 'admin/show-moderation-logs' as const,
limit: 30,
params: computed(() => ({
type,
userId: moderatorId === '' ? null : moderatorId,
})),
};
console.log(Misskey);
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.moderationLogs,
icon: 'ti ti-list-search',
});
</script>

View file

@ -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',

View file

@ -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)

View file

@ -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<string, never>;
promoteQueue: Record<string, never>;
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;
};
};

View file

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

View file

@ -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';