merge: upstream

This commit is contained in:
Mar0xy 2023-09-26 02:26:30 +02:00
commit 8595a325ce
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
145 changed files with 3208 additions and 1285 deletions

View file

@ -9,7 +9,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.0.0 uses: actions/checkout@v4.1.0
- run: corepack enable - run: corepack enable

View file

@ -10,7 +10,7 @@ jobs:
check_copyright_year: check_copyright_year:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
- run: | - run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!" echo "Please change copyright year!"

View file

@ -22,7 +22,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.0.0 uses: actions/checkout@v4.1.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.0.0 uses: docker/setup-buildx-action@v3.0.0

View file

@ -19,7 +19,7 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.0.0 uses: actions/checkout@v4.1.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v3.0.0 uses: docker/setup-buildx-action@v3.0.0

View file

@ -14,7 +14,7 @@ jobs:
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
- run: | - run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb sudo dpkg -i dockle.deb

View file

@ -13,7 +13,7 @@ jobs:
pnpm_install: pnpm_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
@ -40,7 +40,7 @@ jobs:
- sw - sw
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
@ -66,7 +66,7 @@ jobs:
- backend - backend
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true

View file

@ -53,7 +53,7 @@ jobs:
# Check out merge commit # Check out merge commit
- name: Fork based /deploy checkout - name: Fork based /deploy checkout
uses: actions/checkout@v4.0.0 uses: actions/checkout@v4.1.0
with: with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'

View file

@ -29,7 +29,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm

View file

@ -16,7 +16,7 @@ jobs:
node-version: [20.5.1] node-version: [20.5.1]
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
@ -68,7 +68,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
submodules: true submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150 # https://github.com/cypress-io/cypress-docker-images/issues/150

View file

@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.0.0 uses: actions/checkout@v4.1.0
- run: corepack enable - run: corepack enable

View file

@ -19,7 +19,7 @@ jobs:
node-version: [20.5.1] node-version: [20.5.1]
steps: steps:
- uses: actions/checkout@v4.0.0 - uses: actions/checkout@v4.1.0
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm

View file

@ -12,7 +12,22 @@
--> -->
## 2023.9.0 (unreleased) ## 2023.9.1
### General
- Enhance: モデレーションログ機能の強化
### Client
- Fix: ノートのメニューにある「詳細」ボタンの表示がログイン/ログアウト状態で統一されていない問題を修正
### Server
- Fix: お知らせのページネーションが機能しない
- Fix: 「ユーザーの新規投稿」の通知設定を切り替えるとサーバー内部エラーが出る
## 2023.9.0
### Note
- meilisearchを使用する場合、v1.2以上が必要です
### General ### General
- Feat: OAuth 2.0のサポート - Feat: OAuth 2.0のサポート
@ -28,8 +43,13 @@
- Feat: 二要素認証でパスキーをサポートするようになりました - Feat: 二要素認証でパスキーをサポートするようになりました
- Feat: 指定したユーザーが投稿したときに通知できるようになりました - Feat: 指定したユーザーが投稿したときに通知できるようになりました
- Feat: プロフィールでのリンク検証 - Feat: プロフィールでのリンク検証
- Feat: モデレーションログ機能
- Feat: 通知をテストできるようになりました - Feat: 通知をテストできるようになりました
- Feat: PWAのアイコンが設定できるようになりました - Feat: PWAのアイコンが設定できるようになりました
- Enhance: サーバー名の略称が設定できるようになりました
- Enhance: アンテナの受信ソースに指定したユーザを除外するものを追加
- Enhance: 二要素認証設定時のセキュリティを強化
- パスワード入力が必要な操作を行う際、二要素認証が有効であれば確認コードの入力も必要になりました
- Enhance: manifest.jsonをオーバーライド可能に - Enhance: manifest.jsonをオーバーライド可能に
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
- Enhance: ローカリゼーションの更新 - Enhance: ローカリゼーションの更新
@ -40,10 +60,9 @@
- Feat: Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`) - Feat: Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`)
- Feat: クライアントを起動している間、デバイスの画面が自動でオフになるのを防ぐオプションを追加 - Feat: クライアントを起動している間、デバイスの画面が自動でオフになるのを防ぐオプションを追加
- Feat: 新しい実績を追加 - Feat: 新しい実績を追加
- Enhance: ノート詳細ページを改修 - Enhance: ノート詳細ページでリノート一覧、リアクション一覧タブを追加
- 読み込み時のパフォーマンスが向上しました - ノートのメニューからは当該項目は消えました
- リノート一覧、リアクション一覧がタブとして追加されました - Enhance: センシティブなメディアを目立たせる設定を追加
- ノートのメニューからは当該項目は消えました
- Enhance: プロフィールにその人が作ったPlayの一覧出せるように - Enhance: プロフィールにその人が作ったPlayの一覧出せるように
- Enhance: メニューのスイッチの動作を改善 - Enhance: メニューのスイッチの動作を改善
- Enhance: 絵文字ピッカーの検索の表示件数を100件に増加 - Enhance: 絵文字ピッカーの検索の表示件数を100件に増加
@ -62,8 +81,10 @@
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように - Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
- Enhance: Mk:apiが失敗した時にエラー型の値AiScript 0.16.0で追加)を返すように - Enhance: Mk:apiが失敗した時にエラー型の値AiScript 0.16.0で追加)を返すように
- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように試験的なためPlayなどには未実装 - Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように試験的なためPlayなどには未実装
- Enhance: ノート詳細ページ読み込み時のパフォーマンスが向上しました
- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善 - Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように - Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように
- Enhance: プラグインのソースコードを確認・コピーできるように
- Enhance: 細かなデザインの調整 - Enhance: 細かなデザインの調整
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正 - Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正 - Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
@ -73,6 +94,11 @@
- Fix: Misskeyプラグインをインストールする際のAiScriptバージョンのチェックが0.14.0以降に対応していない問題を修正 - Fix: Misskeyプラグインをインストールする際のAiScriptバージョンのチェックが0.14.0以降に対応していない問題を修正
- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正 - Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正
- Fix: 環境によってはMisskey Webが開けない問題を修正 - Fix: 環境によってはMisskey Webが開けない問題を修正
- Fix: プラグインの権限リストが見れない問題を修正
- Fix: 複数の階層があるメニューで、短くタップすると正常に動かない場合がある問題を修正
- Fix: アニメーションがオフのとき、スマホで子メニューの選択ができない問題を修正
- Fix: ドロワーメニューで、親メニュー項目をマウスでホバーすると子メニューが表示されてしまう問題を修正
- Fix: AiScriptでMk:apiが外部と通信できる問題を修正
### Server ### Server
- Change: cacheRemoteFilesの初期値はfalseになりました - Change: cacheRemoteFilesの初期値はfalseになりました
@ -83,12 +109,16 @@
- Enhance: nodeinfo 2.1対応 - Enhance: nodeinfo 2.1対応
- Enhance: 自分へのメンション一覧を取得する際のパフォーマンスを向上 - Enhance: 自分へのメンション一覧を取得する際のパフォーマンスを向上
- Enhance: Docker環境でjemallocを使用することでメモリ使用量を削減 - Enhance: Docker環境でjemallocを使用することでメモリ使用量を削減
- Enhance: ID生成方式としてaidxを追加、かつデフォルトに
- Enhance: Add address bind config option (outgoingAddress)
- Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正 - Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正
- Fix: notes/reactionsのページネーションが機能しない問題を修正
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように - Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
- Fix: 一部のfeatured noteを照会できない問題を修正 - Fix: 一部のfeatured noteを照会できない問題を修正
- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正 - Fix: muteがapiからのuser list timeline取得で機能しない問題を修正
- Fix: ジョブキュー管理画面の認証を回避できる問題を修正 - Fix: ジョブキュー管理画面の認証を回避できる問題を修正
- Fix: 一部のサーバー内部エラーがスタックトレースを返さないように修正 - Fix: 一部のサーバー内部エラーがスタックトレースを返さないように修正
- Fix: 一部のリモートユーザーをフォローすることができない問題を修正
## 13.14.2 ## 13.14.2
@ -103,7 +133,6 @@
### Server ### Server
- Fix: APIのオフセットが壊れていたせいで「もっと見る」でもっと見れない問題を修正 - Fix: APIのオフセットが壊れていたせいで「もっと見る」でもっと見れない問題を修正
- Fix: 外部サーバーの投稿がタイムラインに表示されないことがある問題を修正 - Fix: 外部サーバーの投稿がタイムラインに表示されないことがある問題を修正
- Enhance: Add address bind config option (outgoingAddress)
## 13.14.1 ## 13.14.1

View file

@ -1140,6 +1140,7 @@ _plugin:
install: "ثبّت إضافات" install: "ثبّت إضافات"
installWarn: "رجاءً لا تثبت إضافات غير موثوقة." installWarn: "رجاءً لا تثبت إضافات غير موثوقة."
manage: "إدارة الإضافات" manage: "إدارة الإضافات"
viewSource: "اظهر المصدر"
_preferencesBackups: _preferencesBackups:
createdAt: "تم إنشاؤه: {date} {time}" createdAt: "تم إنشاؤه: {date} {time}"
updatedAt: "آخر تحديث: {date} {time}" updatedAt: "آخر تحديث: {date} {time}"
@ -1555,3 +1556,6 @@ _webhookSettings:
active: "مُفعّل" active: "مُفعّل"
_events: _events:
reaction: "عند التفاعل" reaction: "عند التفاعل"
_moderationLogTypes:
suspend: "علِق"
resetPassword: "أعد تعيين كلمتك السرية"

View file

@ -889,6 +889,7 @@ _plugin:
install: "প্লাগইন ইন্সটল করুন" install: "প্লাগইন ইন্সটল করুন"
installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।" installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।"
manage: "প্লাগইন ম্যানেজ করুন" manage: "প্লাগইন ম্যানেজ করুন"
viewSource: "উৎস দেখুন"
_registry: _registry:
scope: "স্কোপ" scope: "স্কোপ"
key: "কী" key: "কী"
@ -1332,3 +1333,6 @@ _deck:
_webhookSettings: _webhookSettings:
name: "নাম" name: "নাম"
active: "চালু" active: "চালু"
_moderationLogTypes:
suspend: "স্থগিত করা"
resetPassword: "পাসওয়ার্ড রিসেট করুন"

View file

@ -479,3 +479,6 @@ _deck:
list: "Llistes" list: "Llistes"
mentions: "Mencions" mentions: "Mencions"
direct: "Publicacions directes" direct: "Publicacions directes"
_moderationLogTypes:
suspend: "Suspèn"
resetPassword: "Restableix la contrasenya"

View file

@ -1492,6 +1492,7 @@ _plugin:
install: "Instalovat plugin" install: "Instalovat plugin"
installWarn: "Neinstalujte nedůvěryhodné pluginy." installWarn: "Neinstalujte nedůvěryhodné pluginy."
manage: "Správce pluginů" manage: "Správce pluginů"
viewSource: "Zobrazit zdroj"
_preferencesBackups: _preferencesBackups:
list: "Vytvořit backup" list: "Vytvořit backup"
saveNew: "Uložit novou zálohu" saveNew: "Uložit novou zálohu"
@ -1679,7 +1680,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "Již jste zaregistrovali dvoufaktorové ověřovací zařízení." alreadyRegistered: "Již jste zaregistrovali dvoufaktorové ověřovací zařízení."
registerTOTP: "Registrovat aplikaci autentizátoru" registerTOTP: "Registrovat aplikaci autentizátoru"
passwordToTOTP: "Zadejte své heslo"
step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})." step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})."
step2: "Poté naskenujte QR kód zobrazený na této obrazovce." step2: "Poté naskenujte QR kód zobrazený na této obrazovce."
step2Click: "Kliknutím na tento QR kód můžete zaregistrovat 2FA do bezpečnostního klíče nebo aplikace autentizace telefonu." step2Click: "Kliknutím na tento QR kód můžete zaregistrovat 2FA do bezpečnostního klíče nebo aplikace autentizace telefonu."
@ -2036,3 +2036,6 @@ _webhookSettings:
renote: "Při renotaci poznámky" renote: "Při renotaci poznámky"
reaction: "Při obdržení reakce" reaction: "Při obdržení reakce"
mention: "Při zmínce" mention: "Při zmínce"
_moderationLogTypes:
suspend: "Zmrazit"
resetPassword: "Resetovat heslo"

View file

@ -75,7 +75,7 @@ import: "Import"
export: "Export" export: "Export"
files: "Dateien" files: "Dateien"
download: "Herunterladen" download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Sie wird in allen Inhalten, die sie verwenden, auch verschwinden." driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden."
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?" unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
@ -418,6 +418,7 @@ moderator: "Moderator"
moderation: "Moderation" moderation: "Moderation"
moderationNote: "Moderationsnotiz" moderationNote: "Moderationsnotiz"
addModerationNote: "Moderationsnotiz hinzufügen" addModerationNote: "Moderationsnotiz hinzufügen"
moderationLogs: "Moderationsprotokolle"
nUsersMentioned: "Von {n} Benutzern erwähnt" nUsersMentioned: "Von {n} Benutzern erwähnt"
securityKeyAndPasskey: "Hardware-Sicherheitsschlüssel und Passkeys" securityKeyAndPasskey: "Hardware-Sicherheitsschlüssel und Passkeys"
securityKey: "Hardware-Sicherheitsschlüssel" securityKey: "Hardware-Sicherheitsschlüssel"
@ -639,7 +640,7 @@ display: "Anzeigeart"
copy: "Kopieren" copy: "Kopieren"
metrics: "Metriken" metrics: "Metriken"
overview: "Übersicht" overview: "Übersicht"
logs: "Logs" logs: "Protokolle"
delayed: "Verzögert" delayed: "Verzögert"
database: "Datenbank" database: "Datenbank"
channel: "Kanäle" channel: "Kanäle"
@ -710,6 +711,7 @@ lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt
alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren" alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren"
loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen" loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen"
disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" 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." verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen."
notSet: "Nicht konfiguriert" notSet: "Nicht konfiguriert"
emailVerified: "Email-Adresse bestätigt" emailVerified: "Email-Adresse bestätigt"
@ -913,7 +915,7 @@ typeToConfirm: "Bitte gib zur Bestätigung {x} ein"
deleteAccount: "Benutzerkonto löschen" deleteAccount: "Benutzerkonto löschen"
document: "Dokumentation" document: "Dokumentation"
numberOfPageCache: "Seitencachegröße" 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?" logoutConfirm: "Wirklich abmelden?"
lastActiveDate: "Zuletzt verwendet am" lastActiveDate: "Zuletzt verwendet am"
statusbar: "Statusleiste" statusbar: "Statusleiste"
@ -1048,7 +1050,7 @@ vertical: "Vertikal"
horizontal: "Horizontal" horizontal: "Horizontal"
position: "Position" position: "Position"
serverRules: "Serverregeln" serverRules: "Serverregeln"
pleaseConfirmBelowBeforeSignup: "Lies bitte Untenstehendes vor der Registration." pleaseConfirmBelowBeforeSignup: "Lies bitte diese Informationen und stimme ihnen vor der Registration zu."
pleaseAgreeAllToContinue: "Zum Fortfahren muss allen obigen Feldern zugestimmt werden." pleaseAgreeAllToContinue: "Zum Fortfahren muss allen obigen Feldern zugestimmt werden."
continue: "Fortfahren" continue: "Fortfahren"
preservedUsernames: "Reservierte Benutzernamen" preservedUsernames: "Reservierte Benutzernamen"
@ -1112,6 +1114,12 @@ renotes: "Renotes"
loadReplies: "Antworten anzeigen" loadReplies: "Antworten anzeigen"
loadConversation: "Unterhaltung anzeigen" loadConversation: "Unterhaltung anzeigen"
pinnedList: "Angeheftete Liste" pinnedList: "Angeheftete Liste"
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: _announcement:
forExistingUsers: "Nur für existierende Nutzer" 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." 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."
@ -1145,6 +1153,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." 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}." appIconResolutionMustBe: "Die Mindestauflösung ist {resolution}."
manifestJsonOverride: "Überschreiben von manifest.json" 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: _accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFrom: "Von einem anderen Konto zu diesem migrieren"
moveFromSub: "Alias für ein anderes Konto erstellen" moveFromSub: "Alias für ein anderes Konto erstellen"
@ -1525,6 +1535,7 @@ _plugin:
install: "Plugins installieren" install: "Plugins installieren"
installWarn: "Installiere bitte nur vertrauenswürdige Plugins." installWarn: "Installiere bitte nur vertrauenswürdige Plugins."
manage: "Plugins verwalten" manage: "Plugins verwalten"
viewSource: "Quelltext anzeigen"
_preferencesBackups: _preferencesBackups:
list: "Erstellte Backups" list: "Erstellte Backups"
saveNew: "Neu erstellen" saveNew: "Neu erstellen"
@ -1712,13 +1723,12 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert."
registerTOTP: "Authentifizierungs-App registrieren" registerTOTP: "Authentifizierungs-App registrieren"
passwordToTOTP: "Bitte Passwort eingeben"
step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät."
step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät."
step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren." step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren."
step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben" step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben"
step3Title: "Authentifizierungsscode eingeben" step3Title: "Authentifizierungsscode eingeben"
step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step3: "Gib zum Abschluss den Code (Token) ein, der von deiner App angezeigt wird."
setupCompleted: "Einrichtung abgeschlossen" setupCompleted: "Einrichtung abgeschlossen"
step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen."
securityKeyNotSupported: "Dein Browser unterstützt keine Hardware-Sicherheitsschlüssel." securityKeyNotSupported: "Dein Browser unterstützt keine Hardware-Sicherheitsschlüssel."
@ -1791,6 +1801,7 @@ _antennaSources:
homeTimeline: "Notizen von Benutzern, denen gefolgt wird" homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
users: "Notizen von einem oder mehreren angegebenen Benutzern" users: "Notizen von einem oder mehreren angegebenen Benutzern"
userList: "Notizen von allen Benutzern einer Liste" userList: "Notizen von allen Benutzern einer Liste"
userBlacklist: "Alle Notizen abgesehen derer angegebener Benutzer"
_weekday: _weekday:
sunday: "Sonntag" sunday: "Sonntag"
monday: "Montag" monday: "Montag"
@ -1890,6 +1901,7 @@ _profile:
metadataContent: "Inhalt" metadataContent: "Inhalt"
changeAvatar: "Profilbild ändern" changeAvatar: "Profilbild ändern"
changeBanner: "Banner ändern" changeBanner: "Banner ändern"
verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt."
_exportOrImport: _exportOrImport:
allNotes: "Alle Notizen" allNotes: "Alle Notizen"
favoritedNotes: "Als Favorit markierte Notizen" favoritedNotes: "Als Favorit markierte Notizen"
@ -2008,6 +2020,7 @@ _notification:
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
pollEnded: "Umfrageergebnisse sind verfügbar" pollEnded: "Umfrageergebnisse sind verfügbar"
newNote: "Neue Notiz"
unreadAntennaNote: "Antenne {name}" unreadAntennaNote: "Antenne {name}"
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert" emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
achievementEarned: "Errungenschaft freigeschaltet" achievementEarned: "Errungenschaft freigeschaltet"
@ -2017,6 +2030,7 @@ _notification:
notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus" notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus"
_types: _types:
all: "Alle" all: "Alle"
note: "Neue Notizen"
follow: "Neue Follower" follow: "Neue Follower"
mention: "Erwähnungen" mention: "Erwähnungen"
reply: "Antworten" reply: "Antworten"
@ -2086,3 +2100,27 @@ _webhookSettings:
renote: "Wenn du ein Renote erhältst" renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst" reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst" mention: "Wenn du erwähnt wirst"
_moderationLogTypes:
updateRole: "Rolle aktualisiert"
assignRole: "Zu Rolle zugewiesen"
unassignRole: "Aus Rolle entfernt"
suspend: "Gesperrt"
unsuspend: "Entsperrt"
addCustomEmoji: "Benutzerdefiniertes Emoji hinzugefügt"
updateCustomEmoji: "Benutzerdefiniertes Emoji aktualisiert"
deleteCustomEmoji: "Benutzerdefiniertes Emoji gelöscht"
updateServerSettings: "Servereinstellungen aktualisiert"
updateUserNote: "Moderationsnotiz aktualisiert"
deleteDriveFile: "Datei gelöscht"
deleteNote: "Notiz gelöscht"
createGlobalAnnouncement: "Globale Ankündigung erstellt"
createUserAnnouncement: "Benutzerspezifische Ankündigung erstellt"
updateGlobalAnnouncement: "Globale Ankündigung aktualisiert"
updateUserAnnouncement: "Benutzerspezifische Ankündigung aktualisiert"
deleteGlobalAnnouncement: "Globale Ankündigung gelöscht"
deleteUserAnnouncement: "Benutzerspezifische Ankündigung gelöscht"
resetPassword: "Passwort zurückgesetzt"
suspendRemoteInstance: "Fremde Instanz gesperrt"
unsuspendRemoteInstance: "Fremde Instanz entsperrt"
markSensitiveDriveFile: "Datei als sensitiv markiert"
unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert"

View file

@ -397,3 +397,5 @@ _deck:
mentions: "Επισημάνσεις" mentions: "Επισημάνσεις"
_webhookSettings: _webhookSettings:
name: "Όνομα" name: "Όνομα"
_moderationLogTypes:
suspend: "Αποβολή"

View file

@ -418,6 +418,7 @@ moderator: "Moderator"
moderation: "Moderation" moderation: "Moderation"
moderationNote: "Moderation note" moderationNote: "Moderation note"
addModerationNote: "Add moderation note" addModerationNote: "Add moderation note"
moderationLogs: "Moderation logs"
nUsersMentioned: "Mentioned by {n} users" nUsersMentioned: "Mentioned by {n} users"
securityKeyAndPasskey: "Security- and passkeys" securityKeyAndPasskey: "Security- and passkeys"
securityKey: "Security key" securityKey: "Security key"
@ -710,6 +711,7 @@ lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", yo
alwaysMarkSensitive: "Mark as sensitive by default" alwaysMarkSensitive: "Mark as sensitive by default"
loadRawImages: "Load original images instead of showing thumbnails" loadRawImages: "Load original images instead of showing thumbnails"
disableShowingAnimatedImages: "Don't play animated images" 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." verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
notSet: "Not set" notSet: "Not set"
emailVerified: "Email has been verified" emailVerified: "Email has been verified"
@ -913,7 +915,7 @@ typeToConfirm: "Please enter {x} to confirm"
deleteAccount: "Delete account" deleteAccount: "Delete account"
document: "Documentation" document: "Documentation"
numberOfPageCache: "Number of cached pages" 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?" logoutConfirm: "Really log out?"
lastActiveDate: "Last used at" lastActiveDate: "Last used at"
statusbar: "Status bar" statusbar: "Status bar"
@ -1112,6 +1114,12 @@ renotes: "Renotes"
loadReplies: "Show replies" loadReplies: "Show replies"
loadConversation: "Show conversation" loadConversation: "Show conversation"
pinnedList: "Pinned list" pinnedList: "Pinned list"
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: _announcement:
forExistingUsers: "Existing users only" 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." 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."
@ -1145,6 +1153,8 @@ _serverSettings:
appIconStyleRecommendation: "As the icon may be cropped to a square or circle, an icon with colored margin around the content is recommended." 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}." appIconResolutionMustBe: "The minimum resolution is {resolution}."
manifestJsonOverride: "manifest.json Override" 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: _accountMigration:
moveFrom: "Migrate another account to this one" moveFrom: "Migrate another account to this one"
moveFromSub: "Create alias to another account" moveFromSub: "Create alias to another account"
@ -1525,6 +1535,7 @@ _plugin:
install: "Install plugins" install: "Install plugins"
installWarn: "Please do not install untrustworthy plugins." installWarn: "Please do not install untrustworthy plugins."
manage: "Manage plugins" manage: "Manage plugins"
viewSource: "View source"
_preferencesBackups: _preferencesBackups:
list: "Created backups" list: "Created backups"
saveNew: "Save new backup" saveNew: "Save new backup"
@ -1712,7 +1723,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "You have already registered a 2-factor authentication device." alreadyRegistered: "You have already registered a 2-factor authentication device."
registerTOTP: "Register authenticator app" registerTOTP: "Register authenticator app"
passwordToTOTP: "Enter your password"
step1: "First, install an authentication app (such as {a} or {b}) on your device." step1: "First, install an authentication app (such as {a} or {b}) on your device."
step2: "Then, scan the QR code displayed on this screen." step2: "Then, scan the QR code displayed on this screen."
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app." step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app."
@ -1791,6 +1801,7 @@ _antennaSources:
homeTimeline: "Notes from followed users" homeTimeline: "Notes from followed users"
users: "Notes from specific users" users: "Notes from specific users"
userList: "Notes from a specified list of users" userList: "Notes from a specified list of users"
userBlacklist: "All notes except for those of one or more specified users"
_weekday: _weekday:
sunday: "Sunday" sunday: "Sunday"
monday: "Monday" monday: "Monday"
@ -1890,6 +1901,7 @@ _profile:
metadataContent: "Content" metadataContent: "Content"
changeAvatar: "Change avatar" changeAvatar: "Change avatar"
changeBanner: "Change banner" changeBanner: "Change banner"
verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field."
_exportOrImport: _exportOrImport:
allNotes: "All notes" allNotes: "All notes"
favoritedNotes: "Favorite notes" favoritedNotes: "Favorite notes"
@ -2008,6 +2020,7 @@ _notification:
youReceivedFollowRequest: "You've received a follow request" youReceivedFollowRequest: "You've received a follow request"
yourFollowRequestAccepted: "Your follow request was accepted" yourFollowRequestAccepted: "Your follow request was accepted"
pollEnded: "Poll results have become available" pollEnded: "Poll results have become available"
newNote: "New note"
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
emptyPushNotificationMessage: "Push notifications have been updated" emptyPushNotificationMessage: "Push notifications have been updated"
achievementEarned: "Achievement unlocked" achievementEarned: "Achievement unlocked"
@ -2017,6 +2030,7 @@ _notification:
notificationWillBeDisplayedLikeThis: "Notifications look like this" notificationWillBeDisplayedLikeThis: "Notifications look like this"
_types: _types:
all: "All" all: "All"
note: "New notes"
follow: "New followers" follow: "New followers"
mention: "Mentions" mention: "Mentions"
reply: "Replies" reply: "Replies"
@ -2086,3 +2100,27 @@ _webhookSettings:
renote: "When renoted" renote: "When renoted"
reaction: "When receiving a reaction" reaction: "When receiving a reaction"
mention: "When being mentioned" mention: "When being mentioned"
_moderationLogTypes:
updateRole: "Role updated"
assignRole: "Assigned to role"
unassignRole: "Removed from role"
suspend: "Suspended"
unsuspend: "Unsuspended"
addCustomEmoji: "Custom emoji added"
updateCustomEmoji: "Custom emoji updated"
deleteCustomEmoji: "Custom emoji deleted"
updateServerSettings: "Server settings updated"
updateUserNote: "Moderation note updated"
deleteDriveFile: "File deleted"
deleteNote: "Note deleted"
createGlobalAnnouncement: "Global announcement created"
createUserAnnouncement: "User announcement created"
updateGlobalAnnouncement: "Global announcement updated"
updateUserAnnouncement: "User announcement updated"
deleteGlobalAnnouncement: "Global announcement deleted"
deleteUserAnnouncement: "User announcement deleted"
resetPassword: "Password reset"
suspendRemoteInstance: "Remote instance suspended"
unsuspendRemoteInstance: "Remote instance unsuspended"
markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive"

View file

@ -1518,6 +1518,7 @@ _plugin:
install: "Instalar plugins" install: "Instalar plugins"
installWarn: "Por favor no instale plugins que no son de confianza" installWarn: "Por favor no instale plugins que no son de confianza"
manage: "Gestionar plugins" manage: "Gestionar plugins"
viewSource: "Ver la fuente"
_preferencesBackups: _preferencesBackups:
list: "Respaldos creados" list: "Respaldos creados"
saveNew: "Guardar nuevo respaldo" saveNew: "Guardar nuevo respaldo"
@ -1705,7 +1706,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "Ya has completado la configuración." alreadyRegistered: "Ya has completado la configuración."
registerTOTP: "Registrar aplicación autenticadora" registerTOTP: "Registrar aplicación autenticadora"
passwordToTOTP: "Ingresa tu contraseña"
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra." step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra."
step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla."
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora." step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora."
@ -2079,3 +2079,6 @@ _webhookSettings:
renote: "Cuando reciba un \"re-note\"" renote: "Cuando reciba un \"re-note\""
reaction: "Cuando se recibe una reacción" reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención" mention: "Cuando hay una mención"
_moderationLogTypes:
suspend: "Suspender"
resetPassword: "Resetear contraseña"

View file

@ -272,6 +272,7 @@ startMessaging: "Commencer à discuter"
nUsersRead: "Lu par {n} personnes" nUsersRead: "Lu par {n} personnes"
agreeTo: "Jaccepte {0}" agreeTo: "Jaccepte {0}"
agree: "Accepter" agree: "Accepter"
agreeBelow: "Jaccepte ce qui suit"
basicNotesBeforeCreateAccount: "Notes importantes" basicNotesBeforeCreateAccount: "Notes importantes"
termsOfService: "Conditions d'utilisation" termsOfService: "Conditions d'utilisation"
start: "Commencer" start: "Commencer"
@ -406,6 +407,7 @@ aboutMisskey: "À propos de Misskey"
administrator: "Administrateur" administrator: "Administrateur"
token: "Jeton" token: "Jeton"
2fa: "Authentification à deux facteurs" 2fa: "Authentification à deux facteurs"
setupOf2fa: "Configuration de lauthentification à deux facteurs"
totp: "Application d'authentification" totp: "Application d'authentification"
totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification" totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification"
moderator: "Modérateur·rice·s" moderator: "Modérateur·rice·s"
@ -413,6 +415,7 @@ moderation: "Modérations"
moderationNote: "Note de modération" moderationNote: "Note de modération"
addModerationNote: "Ajouter une note de modération" addModerationNote: "Ajouter une note de modération"
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
securityKeyAndPasskey: "Sécurité et clés de sécurité"
securityKey: "Clé de sécurité" securityKey: "Clé de sécurité"
lastUsed: "Dernier utilisé" lastUsed: "Dernier utilisé"
lastUsedAt: "Dernière utilisation : {t}" lastUsedAt: "Dernière utilisation : {t}"
@ -480,6 +483,7 @@ createAccount: "Créer un compte"
existingAccount: "Compte existant" existingAccount: "Compte existant"
regenerate: "Générer à nouveau" regenerate: "Générer à nouveau"
fontSize: "Taille de la police" fontSize: "Taille de la police"
limitTo: "Limiter à {x}"
noFollowRequests: "Vous navez aucune demande dabonnement en attente" noFollowRequests: "Vous navez aucune demande dabonnement en attente"
openImageInNewTab: "Ouvrir les images dans un nouvel onglet" openImageInNewTab: "Ouvrir les images dans un nouvel onglet"
dashboard: "Tableau de bord" dashboard: "Tableau de bord"
@ -796,6 +800,7 @@ popularPosts: "Les plus consultées"
shareWithNote: "Partager dans une note" shareWithNote: "Partager dans une note"
ads: "Publicité" ads: "Publicité"
expiration: "Échéance" expiration: "Échéance"
startingperiod: "Commencer"
memo: "Pense-bête" memo: "Pense-bête"
priority: "Priorité" priority: "Priorité"
high: "Haute" high: "Haute"
@ -957,6 +962,7 @@ internalServerError: "Erreur interne du serveur"
copyErrorInfo: "Copier les détails de lerreur" copyErrorInfo: "Copier les détails de lerreur"
exploreOtherServers: "Trouver une autre instance" exploreOtherServers: "Trouver une autre instance"
disableFederationOk: "Désactiver" disableFederationOk: "Désactiver"
likeOnly: "Les favoris uniquement"
license: "Licence" license: "Licence"
video: "Vidéo" video: "Vidéo"
videos: "Vidéos" videos: "Vidéos"
@ -977,6 +983,7 @@ horizontal: "Latéral"
serverRules: "Règles du serveur" serverRules: "Règles du serveur"
archive: "Archive" archive: "Archive"
youFollowing: "Abonné·e" youFollowing: "Abonné·e"
options: "Options"
later: "Plus tard" later: "Plus tard"
goToMisskey: "Retour vers Misskey" goToMisskey: "Retour vers Misskey"
expirationDate: "Date dexpiration" expirationDate: "Date dexpiration"
@ -989,12 +996,24 @@ icon: "Avatar"
forYou: "Pour vous" forYou: "Pour vous"
replies: "Répondre" replies: "Répondre"
renotes: "Renoter" 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: _announcement:
readConfirmTitle: "Marquer comme lu ?" readConfirmTitle: "Marquer comme lu ?"
_initialAccountSetting: _initialAccountSetting:
profileSetting: "Paramètres du profil" profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité" 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: _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 :" moveToLabel: "Compte vers lequel vous migrez :"
startMigration: "Migrer" startMigration: "Migrer"
movedTo: "Compte vers lequel vous migrez :" movedTo: "Compte vers lequel vous migrez :"
@ -1051,20 +1070,33 @@ _achievements:
_login1000: _login1000:
flavor: "Merci d'utiliser Misskey !" flavor: "Merci d'utiliser Misskey !"
_profileFilled: _profileFilled:
title: "Bien préparé"
description: "Configuration de votre profil" description: "Configuration de votre profil"
_markedAsCat: _markedAsCat:
title: "Je suis un chat" title: "Je suis un chat"
description: "Rendre votre compte comme un chat"
flavor: "Je n'ai pas encore de nom" flavor: "Je n'ai pas encore de nom"
_following1:
title: "Vous suivez votre premier utilisateur·rice"
_following50: _following50:
title: "Beaucoup d'amis" title: "Beaucoup d'amis"
_followers10: _followers10:
title: "Abonnez-moi !" title: "Abonnez-moi !"
_followers100:
title: "Populaire"
_followers500:
title: "Tour radio"
_followers1000:
title: "Influenceur·euse"
_iLoveMisskey: _iLoveMisskey:
title: "Jadore Misskey" title: "Jadore Misskey"
description: "Publication « J❤ #Misskey »" description: "Publication « J❤ #Misskey »"
flavor: "L'équipe de développement de Misskey apprécie vraiment votre aide !"
_foundTreasure: _foundTreasure:
title: "Chasse au trésor" title: "Chasse au trésor"
description: "Vous avez trouvé le trésor caché" description: "Vous avez trouvé le trésor caché"
_client30min:
title: "Pause bien méritée"
_postedAtLateNight: _postedAtLateNight:
flavor: "Cest lheure daller au lit." flavor: "Cest lheure daller au lit."
_postedAt0min0sec: _postedAt0min0sec:
@ -1073,18 +1105,45 @@ _achievements:
flavor: "Tic tac, tic tac, tic tac, ding !" flavor: "Tic tac, tic tac, tic tac, ding !"
_viewInstanceChart: _viewInstanceChart:
title: "Analyste" 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: _loggedInOnBirthday:
title: "Joyeux Anniversaire !" title: "Joyeux Anniversaire !"
description: "Vous vous êtes connecté à la date de votre anniversaire"
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "Bonne année !" title: "Bonne année !"
_cookieClicked: _cookieClicked:
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
_brainDiver:
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "Nouveau rôle"
edit: "Modifier le rôle"
name: "Nom du rôle" name: "Nom du rôle"
description: "Description du rôle" description: "Description du rôle"
permission: "Rôle et autorisations" permission: "Rôle et autorisations"
assignTarget: "Attribuer" assignTarget: "Attribuer"
condition: "Condition" 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: "Priorité"
_priority: _priority:
low: "Basse" low: "Basse"
@ -1143,6 +1202,7 @@ _plugin:
install: "Installation de plugin" install: "Installation de plugin"
installWarn: "Ninstallez que des extensions provenant de sources de confiance." installWarn: "Ninstallez que des extensions provenant de sources de confiance."
manage: "Gestion des plugins" manage: "Gestion des plugins"
viewSource: "Afficher la source"
_preferencesBackups: _preferencesBackups:
list: "Sauvegardes créées" list: "Sauvegardes créées"
saveNew: "Nouvelle sauvegarde" saveNew: "Nouvelle sauvegarde"
@ -1329,6 +1389,7 @@ _2fa:
securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité." 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." 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é" securityKeyName: "Nom de la clé"
removeKey: "Supprimer la clé de sécurité"
removeKeyConfirm: "Voulez-vous supprimer {name} ?" removeKeyConfirm: "Voulez-vous supprimer {name} ?"
renewTOTPOk: "Reconfigurer" renewTOTPOk: "Reconfigurer"
renewTOTPCancel: "Pas maintenant" renewTOTPCancel: "Pas maintenant"
@ -1630,3 +1691,6 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Nom" name: "Nom"
active: "Activé" active: "Activé"
_moderationLogTypes:
suspend: "Suspendre"
resetPassword: "Réinitialiser le mot de passe"

View file

@ -1496,6 +1496,7 @@ _plugin:
install: "Memasang plugin" install: "Memasang plugin"
installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai." installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai."
manage: "Manajemen plugin" manage: "Manajemen plugin"
viewSource: "Lihat sumber"
_preferencesBackups: _preferencesBackups:
list: "Cadangan yang dibuat" list: "Cadangan yang dibuat"
saveNew: "Simpan cadangan baru" saveNew: "Simpan cadangan baru"
@ -1683,7 +1684,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
registerTOTP: "Daftarkan aplikasi autentikator" registerTOTP: "Daftarkan aplikasi autentikator"
passwordToTOTP: "Masukkan kata sandimu"
step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu."
step2: "Lalu, pindai kode QR yang ada di layar." step2: "Lalu, pindai kode QR yang ada di layar."
step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel." step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel."
@ -2041,3 +2041,6 @@ _webhookSettings:
renote: "Ketika direnote" renote: "Ketika direnote"
reaction: "Ketika menerima reaksi" reaction: "Ketika menerima reaksi"
mention: "Ketika sedang disebut" mention: "Ketika sedang disebut"
_moderationLogTypes:
suspend: "Tangguhkan"
resetPassword: "Atur ulang kata sandi"

39
locales/index.d.ts vendored
View file

@ -421,6 +421,7 @@ export interface Locale {
"moderation": string; "moderation": string;
"moderationNote": string; "moderationNote": string;
"addModerationNote": string; "addModerationNote": string;
"moderationLogs": string;
"nUsersMentioned": string; "nUsersMentioned": string;
"securityKeyAndPasskey": string; "securityKeyAndPasskey": string;
"securityKey": string; "securityKey": string;
@ -713,6 +714,7 @@ export interface Locale {
"alwaysMarkSensitive": string; "alwaysMarkSensitive": string;
"loadRawImages": string; "loadRawImages": string;
"disableShowingAnimatedImages": string; "disableShowingAnimatedImages": string;
"highlightSensitiveMedia": string;
"verificationEmailSent": string; "verificationEmailSent": string;
"notSet": string; "notSet": string;
"emailVerified": string; "emailVerified": string;
@ -1119,6 +1121,9 @@ export interface Locale {
"verifiedLink": string; "verifiedLink": string;
"notifyNotes": string; "notifyNotes": string;
"unnotifyNotes": string; "unnotifyNotes": string;
"authentication": string;
"authenticationRequiredToContinue": string;
"dateAndTime": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@ -1155,6 +1160,8 @@ export interface Locale {
"appIconStyleRecommendation": string; "appIconStyleRecommendation": string;
"appIconResolutionMustBe": string; "appIconResolutionMustBe": string;
"manifestJsonOverride": string; "manifestJsonOverride": string;
"shortName": string;
"shortNameDescription": string;
}; };
"_accountMigration": { "_accountMigration": {
"moveFrom": string; "moveFrom": string;
@ -1629,6 +1636,7 @@ export interface Locale {
"install": string; "install": string;
"installWarn": string; "installWarn": string;
"manage": string; "manage": string;
"viewSource": string;
}; };
"_preferencesBackups": { "_preferencesBackups": {
"list": string; "list": string;
@ -1833,7 +1841,6 @@ export interface Locale {
"_2fa": { "_2fa": {
"alreadyRegistered": string; "alreadyRegistered": string;
"registerTOTP": string; "registerTOTP": string;
"passwordToTOTP": string;
"step1": string; "step1": string;
"step2": string; "step2": string;
"step2Click": string; "step2Click": string;
@ -1915,6 +1922,7 @@ export interface Locale {
"homeTimeline": string; "homeTimeline": string;
"users": string; "users": string;
"userList": string; "userList": string;
"userBlacklist": string;
}; };
"_weekday": { "_weekday": {
"sunday": string; "sunday": string;
@ -2162,6 +2170,7 @@ export interface Locale {
"notificationWillBeDisplayedLikeThis": string; "notificationWillBeDisplayedLikeThis": string;
"_types": { "_types": {
"all": string; "all": string;
"note": string;
"follow": string; "follow": string;
"mention": string; "mention": string;
"reply": string; "reply": string;
@ -2241,6 +2250,34 @@ export interface Locale {
"mention": string; "mention": string;
}; };
}; };
"_moderationLogTypes": {
"createRole": string;
"deleteRole": string;
"updateRole": string;
"assignRole": string;
"unassignRole": string;
"suspend": string;
"unsuspend": string;
"addCustomEmoji": string;
"updateCustomEmoji": string;
"deleteCustomEmoji": string;
"updateServerSettings": string;
"updateUserNote": string;
"deleteDriveFile": string;
"deleteNote": string;
"createGlobalAnnouncement": string;
"createUserAnnouncement": string;
"updateGlobalAnnouncement": string;
"updateUserAnnouncement": string;
"deleteGlobalAnnouncement": string;
"deleteUserAnnouncement": string;
"resetPassword": string;
"suspendRemoteInstance": string;
"unsuspendRemoteInstance": string;
"markSensitiveDriveFile": string;
"unmarkSensitiveDriveFile": string;
"resolveAbuseReport": string;
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View file

@ -117,7 +117,7 @@ pinnedNote: "Nota fissata"
pinned: "Fissa sul profilo" pinned: "Fissa sul profilo"
you: "Tu" you: "Tu"
clickToShow: "Clicca per visualizzare" clickToShow: "Clicca per visualizzare"
sensitive: "Contenuto sensibile" sensitive: "Esplicito"
add: "Aggiungi" add: "Aggiungi"
reaction: "Reazioni" reaction: "Reazioni"
reactions: "Reazioni" reactions: "Reazioni"
@ -125,8 +125,8 @@ reactionSetting: "Reazioni visualizzate sul pannello"
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
attachCancel: "Rimuovi allegato" attachCancel: "Rimuovi allegato"
markAsSensitive: "Segna come sensibile" markAsSensitive: "Segna come esplicito"
unmarkAsSensitive: "Segna come non sensibile" unmarkAsSensitive: "Non segnare come esplicito "
enterFileName: "Nome del file" enterFileName: "Nome del file"
mute: "Silenzia" mute: "Silenzia"
unmute: "Riattiva l'audio" unmute: "Riattiva l'audio"
@ -134,7 +134,7 @@ renoteMute: "Silenzia i Rinota"
renoteUnmute: "Non silenziare i Rinota" renoteUnmute: "Non silenziare i Rinota"
block: "Blocca" block: "Blocca"
unblock: "Sblocca" unblock: "Sblocca"
suspend: "Sospendi" suspend: "Sospensione"
unsuspend: "Revoca la sospensione" unsuspend: "Revoca la sospensione"
blockConfirm: "Vuoi davvero bloccare il profilo?" blockConfirm: "Vuoi davvero bloccare il profilo?"
unblockConfirm: "Vuoi davvero sbloccare il profilo?" unblockConfirm: "Vuoi davvero sbloccare il profilo?"
@ -148,7 +148,7 @@ editAntenna: "Modifica Antenna"
selectWidget: "Seleziona il riquadro" selectWidget: "Seleziona il riquadro"
editWidgets: "Modifica i riquadri" editWidgets: "Modifica i riquadri"
editWidgetsExit: "Conferma le modifiche" editWidgetsExit: "Conferma le modifiche"
customEmojis: "Emoji personalizzati" customEmojis: "Emoji personalizzate"
emoji: "Emoji" emoji: "Emoji"
emojis: "Emoji" emojis: "Emoji"
emojiName: "Nome dell'emoji" emojiName: "Nome dell'emoji"
@ -158,8 +158,8 @@ settingGuide: "Configurazione suggerita"
cacheRemoteFiles: "Memorizza i file remoti nella cache" cacheRemoteFiles: "Memorizza i file remoti nella cache"
cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime." cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime."
youCanCleanRemoteFilesCache: "Puoi svuotare tutta la cache cliccando il bottone 🗑️ nella gestione file" youCanCleanRemoteFilesCache: "Puoi svuotare tutta la cache cliccando il bottone 🗑️ nella gestione file"
cacheRemoteSensitiveFiles: "Memorizza nella cache i file sensibili remoti" cacheRemoteSensitiveFiles: "Copia nella cache locale i file espliciti remoti"
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file sensibili verranno caricati direttamente dall'istanza remota senza essere salvati dal server." cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale."
flagAsBot: "Io sono un robot" flagAsBot: "Io sono un robot"
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot." flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot."
flagAsCat: "Sono un gatto" flagAsCat: "Sono un gatto"
@ -180,7 +180,7 @@ youHaveNoLists: "Non hai ancora creato nessuna lista"
followConfirm: "Vuoi seguire {name}?" followConfirm: "Vuoi seguire {name}?"
proxyAccount: "Profilo proxy" proxyAccount: "Profilo proxy"
proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti." proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti."
host: "Server remoto" host: "Host"
selectUser: "Seleziona profilo" selectUser: "Seleziona profilo"
recipient: "Destinatario" recipient: "Destinatario"
annotation: "Annotazione preventiva" annotation: "Annotazione preventiva"
@ -287,7 +287,7 @@ images: "Immagini"
image: "Immagini" image: "Immagini"
birthday: "Compleanno" birthday: "Compleanno"
yearsOld: "{age} anni" yearsOld: "{age} anni"
registeredDate: "Iscrizione a.." registeredDate: "Data iscrizione"
location: "Posizione" location: "Posizione"
theme: "Tema" theme: "Tema"
themeForLightMode: "Tema da utilizzare per il modo chiaro" themeForLightMode: "Tema da utilizzare per il modo chiaro"
@ -321,7 +321,7 @@ copyUrl: "Copia URL"
rename: "Modifica nome" rename: "Modifica nome"
avatar: "Foto del profilo" avatar: "Foto del profilo"
banner: "Intestazione" banner: "Intestazione"
displayOfSensitiveMedia: "Visibilità dei media sensibili" displayOfSensitiveMedia: "Visibilità dei media espliciti"
whenServerDisconnected: "Quando la connessione col server è persa" whenServerDisconnected: "Quando la connessione col server è persa"
disconnectedFromServer: "Il server si è disconnesso" disconnectedFromServer: "Il server si è disconnesso"
reload: "Ricarica" reload: "Ricarica"
@ -418,6 +418,7 @@ moderator: "Moderatore"
moderation: "moderazione" moderation: "moderazione"
moderationNote: "Promemoria di moderazione" moderationNote: "Promemoria di moderazione"
addModerationNote: "Aggiungi promemoria di moderazione" addModerationNote: "Aggiungi promemoria di moderazione"
moderationLogs: "Cronologia di moderazione"
nUsersMentioned: "{n} profili menzionati" nUsersMentioned: "{n} profili menzionati"
securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKeyAndPasskey: "Chiave di sicurezza e accesso"
securityKey: "Chiave di sicurezza" securityKey: "Chiave di sicurezza"
@ -496,7 +497,7 @@ noFollowRequests: "Non hai alcuna richiesta di follow"
openImageInNewTab: "Apri le immagini in un nuovo tab" openImageInNewTab: "Apri le immagini in un nuovo tab"
dashboard: "Pannello di controllo" dashboard: "Pannello di controllo"
local: "Locale" local: "Locale"
remote: "Remoto" remote: "Remota"
total: "Totale" total: "Totale"
weekOverWeekChanges: "Settimanale" weekOverWeekChanges: "Settimanale"
dayOverDayChanges: "Giornaliero" dayOverDayChanges: "Giornaliero"
@ -551,8 +552,8 @@ installedDate: "Data installazione"
lastUsedDate: "Data di ultimo uso" lastUsedDate: "Data di ultimo uso"
state: "Stato" state: "Stato"
sort: "Ordina per" sort: "Ordina per"
ascendingOrder: "Ascendente" ascendingOrder: "Aumenta"
descendingOrder: "Discendente" descendingOrder: "Diminuisce"
scratchpad: "ScratchPad" scratchpad: "ScratchPad"
scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey." scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey."
output: "Uscita" output: "Uscita"
@ -621,7 +622,7 @@ emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronic
email: "Email" email: "Email"
emailAddress: "Indirizzo di posta elettronica" emailAddress: "Indirizzo di posta elettronica"
smtpConfig: "Impostazioni del server SMTP" smtpConfig: "Impostazioni del server SMTP"
smtpHost: "Server remoto" smtpHost: "Host SMTP"
smtpPort: "Porta" smtpPort: "Porta"
smtpUser: "Nome utente" smtpUser: "Nome utente"
smtpPass: "Password" smtpPass: "Password"
@ -707,9 +708,10 @@ driveUsage: "Utilizzazione del Drive"
noCrawle: "Rifiuta l'indicizzazione dai robot." noCrawle: "Rifiuta l'indicizzazione dai robot."
noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc." noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc."
lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow." lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow."
alwaysMarkSensitive: "Segnare i media come sensibili per impostazione predefinita" alwaysMarkSensitive: "Segnare gli allegati come espliciti come opzione predefinita"
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature." loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
disableShowingAnimatedImages: "Disabilita le immagini animate" disableShowingAnimatedImages: "Disabilita le immagini animate"
highlightSensitiveMedia: "Evidenzia i media espliciti"
verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica." verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica."
notSet: "Non impostato" notSet: "Non impostato"
emailVerified: "Il tuo indirizzo email è stato verificato" emailVerified: "Il tuo indirizzo email è stato verificato"
@ -926,7 +928,7 @@ type: "Tipo"
speed: "Velocità" speed: "Velocità"
slow: "Lento" slow: "Lento"
fast: "Veloce" fast: "Veloce"
sensitiveMediaDetection: "Rilevamento dei contenuti sensibili." sensitiveMediaDetection: "Rilevamento dei contenuti espliciti"
localOnly: "Soltanto locale" localOnly: "Soltanto locale"
remoteOnly: "Solo remoto" remoteOnly: "Solo remoto"
failedToUpload: "errore di caricamento" failedToUpload: "errore di caricamento"
@ -1006,11 +1008,11 @@ cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Reazioni consentite" reactionAcceptance: "Reazioni consentite"
likeOnly: "Solo i Like" likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti" likeOnlyForRemote: "Solo Like remoti"
nonSensitiveOnly: "Solamente non sensibili" nonSensitiveOnly: "Soltanto non espliciti"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Solamente non sensibili (solo Mi piace remoti)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Soltanto non espliciti (reazioni remote)"
rolesAssignedToMe: "I miei ruoli" rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi davvero ripristinare la password?" resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
sensitiveWords: "Parole sensibili" sensitiveWords: "Parole esplicite"
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
notesSearchNotAvailable: "Non è possibile cercare tra le Note." notesSearchNotAvailable: "Non è possibile cercare tra le Note."
@ -1112,6 +1114,12 @@ renotes: "Rinota"
loadReplies: "Leggi le risposte" loadReplies: "Leggi le risposte"
loadConversation: "Leggi la conversazione" loadConversation: "Leggi la conversazione"
pinnedList: "Elenco in primo piano" pinnedList: "Elenco in primo piano"
keepScreenOn: "Mantieni lo schermo acceso"
verifiedLink: "Abbiamo confermato la validità di questo collegamento"
notifyNotes: "Notifica nuove Note"
unnotifyNotes: "Interrompi le notifiche di nuove Note"
authentication: "Autenticazione"
authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione"
_announcement: _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -1145,6 +1153,8 @@ _serverSettings:
appIconStyleRecommendation: "Poiché l'icona potrebbe essere ritagliata in un quadrato o in un cerchio, si raccomanda che abbia un margine colorato." appIconStyleRecommendation: "Poiché l'icona potrebbe essere ritagliata in un quadrato o in un cerchio, si raccomanda che abbia un margine colorato."
appIconResolutionMustBe: "La risoluzione minima è {resolution}" appIconResolutionMustBe: "La risoluzione minima è {resolution}"
manifestJsonOverride: "Sostituire il file manifest.json" manifestJsonOverride: "Sostituire il file manifest.json"
shortName: "Abbreviazione"
shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server."
_accountMigration: _accountMigration:
moveFrom: "Migra un altro profilo dentro a questo" moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto" moveFromSub: "Crea un alias verso un altro profilo remoto"
@ -1462,10 +1472,10 @@ _role:
_condition: _condition:
isLocal: "Profilo locale" isLocal: "Profilo locale"
isRemote: "Profilo remoto" isRemote: "Profilo remoto"
createdLessThan: "Creato meno di" createdLessThan: "Profilo creato da meno di N"
createdMoreThan: "Creato più di" createdMoreThan: "Profilo creato da più di N"
followersLessThanOrEq: "Ha meno di N follower" followersLessThanOrEq: "Profilo con N follower o meno"
followersMoreThanOrEq: "Ha più di N follower" followersMoreThanOrEq: "Profilo con N follower o più"
followingLessThanOrEq: "Segue N profili o meno" followingLessThanOrEq: "Segue N profili o meno"
followingMoreThanOrEq: "Segue N profili o più" followingMoreThanOrEq: "Segue N profili o più"
notesLessThanOrEq: "Conteggio Note inferiore o uguale a" notesLessThanOrEq: "Conteggio Note inferiore o uguale a"
@ -1474,9 +1484,9 @@ _role:
or: "O" or: "O"
not: "NON" not: "NON"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente." description: "Utilizzare l'apprendimento automatico (machine learning) per riconoscere media espliciti e sottoporli alla moderazione. Aumenterà lievemente il carico del server."
sensitivity: "Sensibilità di rilevamento" sensitivity: "Sensibilità del rilevamento"
sensitivityDescription: "Una minore sensibilità riduce i falsi positivi (false positività). Una maggiore sensibilità riduce le omissioni (falsi negativi)." sensitivityDescription: "Abbassando la sensibilità si riducono i falsi positivi (rilevazioni errate). Aumentando la sensibilità si riduce il numero di rilevazioni mancate. (rilevazioni ignorate)."
setSensitiveFlagAutomatically: "Impostare il flag NSFW." setSensitiveFlagAutomatically: "Impostare il flag NSFW."
setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente." setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente."
analyzeVideos: "Abilitazione dell'analisi video." analyzeVideos: "Abilitazione dell'analisi video."
@ -1525,6 +1535,7 @@ _plugin:
install: "Installa estensioni" install: "Installa estensioni"
installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili." installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili."
manage: "Gestisci estensioni" manage: "Gestisci estensioni"
viewSource: "Visualizza sorgente"
_preferencesBackups: _preferencesBackups:
list: "Elenco di impostazioni salvate in precedenza" list: "Elenco di impostazioni salvate in precedenza"
saveNew: "Nuovo salvataggio" saveNew: "Nuovo salvataggio"
@ -1559,8 +1570,8 @@ _aboutMisskey:
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
patrons: "Sostenitori" patrons: "Sostenitori"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "Nascondere i media sensibili" respect: "Nascondere i media espliciti"
ignore: "Non nascondere i media sensibili" ignore: "Non nascondere i media espliciti"
force: "Nascondi tutti i media" force: "Nascondi tutti i media"
_instanceTicker: _instanceTicker:
none: "Nascondi" none: "Nascondi"
@ -1712,7 +1723,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "La configurazione è stata già completata." alreadyRegistered: "La configurazione è stata già completata."
registerTOTP: "Registra un'app di autenticazione" registerTOTP: "Registra un'app di autenticazione"
passwordToTOTP: "Inserire la password"
step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}." step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}."
step2: "Quindi, scansionare il codice QR visualizzato con l'app." step2: "Quindi, scansionare il codice QR visualizzato con l'app."
step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo." step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo."
@ -1791,6 +1801,7 @@ _antennaSources:
homeTimeline: "Note dagli utenti che segui" homeTimeline: "Note dagli utenti che segui"
users: "Note dagli utenti selezionati" users: "Note dagli utenti selezionati"
userList: "Note dagli utenti della lista selezionata" userList: "Note dagli utenti della lista selezionata"
userBlacklist: "Tutte le Note tranne quelle di uno o più profili specificati"
_weekday: _weekday:
sunday: "Domenica" sunday: "Domenica"
monday: "Lunedì" monday: "Lunedì"
@ -1890,6 +1901,7 @@ _profile:
metadataContent: "Contenuto" metadataContent: "Contenuto"
changeAvatar: "Modifica immagine profilo" changeAvatar: "Modifica immagine profilo"
changeBanner: "Cambia intestazione" changeBanner: "Cambia intestazione"
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo."
_exportOrImport: _exportOrImport:
allNotes: "Tutte le note" allNotes: "Tutte le note"
favoritedNotes: "Note preferite" favoritedNotes: "Note preferite"
@ -2008,6 +2020,7 @@ _notification:
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata"
pollEnded: "Risultati del sondaggio." pollEnded: "Risultati del sondaggio."
newNote: "Nuove Note"
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
achievementEarned: "Obiettivo raggiunto" achievementEarned: "Obiettivo raggiunto"
@ -2017,7 +2030,8 @@ _notification:
notificationWillBeDisplayedLikeThis: "La notifica apparirà così" notificationWillBeDisplayedLikeThis: "La notifica apparirà così"
_types: _types:
all: "Tutto" all: "Tutto"
follow: "Novità follower" note: "Nuove Note"
follow: "Nuovi profili follower"
mention: "Menzioni" mention: "Menzioni"
reply: "Risposte" reply: "Risposte"
renote: "Rinota" renote: "Rinota"
@ -2086,3 +2100,27 @@ _webhookSettings:
renote: "Quando la Nota è Rinotata" renote: "Quando la Nota è Rinotata"
reaction: "Quando ricevo una reazione" reaction: "Quando ricevo una reazione"
mention: "Quando mi menzionano" mention: "Quando mi menzionano"
_moderationLogTypes:
updateRole: "Ruolo aggiornato"
assignRole: "Ruolo assegnato"
unassignRole: "Ruolo disassegnato"
suspend: "Sospensione"
unsuspend: "Sospensione rimossa"
addCustomEmoji: "Emoji personalizzata aggiunta"
updateCustomEmoji: "Emoji personalizzata aggiornata"
deleteCustomEmoji: "Emoji personalizzata eliminata"
updateServerSettings: "Impostazioni del server aggiornate"
updateUserNote: "Promemoria di moderazione aggiornato"
deleteDriveFile: "File da Drive eliminato"
deleteNote: "Nota eliminata"
createGlobalAnnouncement: "Annuncio globale creato"
createUserAnnouncement: "Annuncio ai profili iscritti creato"
updateGlobalAnnouncement: "Annuncio globale aggiornato"
updateUserAnnouncement: "Annuncio ai profili iscritti aggiornato"
deleteGlobalAnnouncement: "Annuncio globale eliminato"
deleteUserAnnouncement: "Annuncio ai profili iscritti eliminato"
resetPassword: "Password azzerata"
suspendRemoteInstance: "Istanza remota sospesa"
unsuspendRemoteInstance: "Istanza remota riattivata"
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"

View file

@ -418,6 +418,7 @@ moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
moderationNote: "モデレーションノート" moderationNote: "モデレーションノート"
addModerationNote: "モデレーションノートを追加する" addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー" securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー" securityKey: "セキュリティキー"
@ -710,6 +711,7 @@ lockedAccountInfo: "フォローを承認制にしても、ノートの公開範
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする" alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする" loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない" disableShowingAnimatedImages: "アニメーション画像を再生しない"
highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。" verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
notSet: "未設定" notSet: "未設定"
emailVerified: "メールアドレスが確認されました" emailVerified: "メールアドレスが確認されました"
@ -1116,6 +1118,9 @@ keepScreenOn: "デバイスの画面を常にオンにする"
verifiedLink: "このリンク先の所有者であることが確認されました" verifiedLink: "このリンク先の所有者であることが確認されました"
notifyNotes: "投稿を通知" notifyNotes: "投稿を通知"
unnotifyNotes: "投稿の通知を解除" unnotifyNotes: "投稿の通知を解除"
authentication: "認証"
authenticationRequiredToContinue: "続けるには認証を行ってください"
dateAndTime: "日時"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@ -1153,6 +1158,8 @@ _serverSettings:
appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるため、塗り潰された余白のある背景を持つことが推奨されます。" appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるため、塗り潰された余白のある背景を持つことが推奨されます。"
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があります。" appIconResolutionMustBe: "解像度は必ず{resolution}である必要があります。"
manifestJsonOverride: "manifest.jsonのオーバーライド" manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称"
shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。"
_accountMigration: _accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行" moveFrom: "別のアカウントからこのアカウントに移行"
@ -1546,6 +1553,7 @@ _plugin:
install: "プラグインのインストール" install: "プラグインのインストール"
installWarn: "信頼できないプラグインはインストールしないでください。" installWarn: "信頼できないプラグインはインストールしないでください。"
manage: "プラグインの管理" manage: "プラグインの管理"
viewSource: "ソースを表示"
_preferencesBackups: _preferencesBackups:
list: "作成したバックアップ" list: "作成したバックアップ"
@ -1750,7 +1758,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "既に設定は完了しています。" alreadyRegistered: "既に設定は完了しています。"
registerTOTP: "認証アプリの設定を開始" registerTOTP: "認証アプリの設定を開始"
passwordToTOTP: "パスワードを入力してください"
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
step2: "次に、表示されているQRコードをアプリでスキャンします。" step2: "次に、表示されているQRコードをアプリでスキャンします。"
step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。" step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。"
@ -1832,6 +1839,7 @@ _antennaSources:
homeTimeline: "フォローしているユーザーのノート" homeTimeline: "フォローしているユーザーのノート"
users: "指定した一人または複数のユーザーのノート" users: "指定した一人または複数のユーザーのノート"
userList: "指定したリストのユーザーのノート" userList: "指定したリストのユーザーのノート"
userBlacklist: "指定した一人または複数のユーザーを除いた全てのノート"
_weekday: _weekday:
sunday: "日曜日" sunday: "日曜日"
@ -2077,6 +2085,7 @@ _notification:
_types: _types:
all: "すべて" all: "すべて"
note: "ユーザーの新規投稿"
follow: "フォロー" follow: "フォロー"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
@ -2153,3 +2162,31 @@ _webhookSettings:
renote: "Renoteされたとき" renote: "Renoteされたとき"
reaction: "リアクションがあったとき" reaction: "リアクションがあったとき"
mention: "メンションされたとき" mention: "メンションされたとき"
_moderationLogTypes:
createRole: "ロールを作成"
deleteRole: "ロールを削除"
updateRole: "ロールを更新"
assignRole: "ロールへアサイン"
unassignRole: "ロールのアサイン解除"
suspend: "凍結"
unsuspend: "凍結解除"
addCustomEmoji: "カスタム絵文字追加"
updateCustomEmoji: "カスタム絵文字更新"
deleteCustomEmoji: "カスタム絵文字削除"
updateServerSettings: "サーバー設定更新"
updateUserNote: "モデレーションノート更新"
deleteDriveFile: "ファイルを削除"
deleteNote: "ノートを削除"
createGlobalAnnouncement: "全体のお知らせを作成"
createUserAnnouncement: "ユーザーへお知らせを作成"
updateGlobalAnnouncement: "全体のお知らせを更新"
updateUserAnnouncement: "ユーザーのお知らせを更新"
deleteGlobalAnnouncement: "全体のお知らせを削除"
deleteUserAnnouncement: "ユーザーのお知らせを削除"
resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを停止"
unsuspendRemoteInstance: "リモートサーバーを再開"
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決"

View file

@ -1510,6 +1510,7 @@ _plugin:
install: "プラグインのインストール" install: "プラグインのインストール"
installWarn: "信頼できへんプラグインはインストールせんとってな" installWarn: "信頼できへんプラグインはインストールせんとってな"
manage: "プラグインの管理" manage: "プラグインの管理"
viewSource: "ソースを表示"
_preferencesBackups: _preferencesBackups:
list: "作ったバックアップ" list: "作ったバックアップ"
saveNew: "新しく保存" saveNew: "新しく保存"
@ -1697,7 +1698,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "もう設定終わっとるわ。" alreadyRegistered: "もう設定終わっとるわ。"
registerTOTP: "認証アプリの設定はじめる" registerTOTP: "認証アプリの設定はじめる"
passwordToTOTP: "パスワードを入れてーや"
step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。"
step2: "次に、ここにあるQRコードをアプリでスキャンしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな。"
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
@ -2058,3 +2058,6 @@ _webhookSettings:
renote: "Renoteされるとき" renote: "Renoteされるとき"
reaction: "ツッコミがあるとき~!" reaction: "ツッコミがあるとき~!"
mention: "メンションがあるとき~!" mention: "メンションがあるとき~!"
_moderationLogTypes:
suspend: "凍結"
resetPassword: "パスワードをリセット"

View file

@ -1512,6 +1512,7 @@ _plugin:
install: "플러그인 설치" install: "플러그인 설치"
installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다." installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다."
manage: "플러그인 관리" manage: "플러그인 관리"
viewSource: "소스 보기"
_preferencesBackups: _preferencesBackups:
list: "생성한 백업" list: "생성한 백업"
saveNew: "새 백업 만들기" saveNew: "새 백업 만들기"
@ -1699,7 +1700,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "이미 설정이 완료되었습니다." alreadyRegistered: "이미 설정이 완료되었습니다."
registerTOTP: "인증 앱 설정 시작" registerTOTP: "인증 앱 설정 시작"
passwordToTOTP: "비밀번호를 입력하세요."
step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다."
step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다."
step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다." step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다."
@ -2073,3 +2073,6 @@ _webhookSettings:
renote: "누군가 내 글을 Renote했을 때" renote: "누군가 내 글을 Renote했을 때"
reaction: "누군가 내 노트에 리액션했을 때" reaction: "누군가 내 노트에 리액션했을 때"
mention: "누군가 나를 멘션했을 때" mention: "누군가 나를 멘션했을 때"
_moderationLogTypes:
suspend: "정지"
resetPassword: "비밀번호 재설정"

View file

@ -463,3 +463,5 @@ _deck:
mentions: "ກ່າວເຖິງ" mentions: "ກ່າວເຖິງ"
_webhookSettings: _webhookSettings:
name: "ຊື່" name: "ຊື່"
_moderationLogTypes:
suspend: "ລະງັບ"

View file

@ -494,3 +494,6 @@ _deck:
mentions: "Vermeldingen" mentions: "Vermeldingen"
_webhookSettings: _webhookSettings:
name: "Naam" name: "Naam"
_moderationLogTypes:
suspend: "Opschorten"
resetPassword: "Wachtwoord terugzetten"

View file

@ -725,3 +725,5 @@ _deck:
direct: "Direkte" direct: "Direkte"
_webhookSettings: _webhookSettings:
name: "Navn" name: "Navn"
_moderationLogTypes:
suspend: "Suspender"

View file

@ -925,6 +925,7 @@ _plugin:
install: "Zainstaluj wtyczki" install: "Zainstaluj wtyczki"
installWarn: "Nie instaluj niezaufanych wtyczek." installWarn: "Nie instaluj niezaufanych wtyczek."
manage: "Zarządzanie wtyczkami" manage: "Zarządzanie wtyczkami"
viewSource: "Zobacz źródło"
_preferencesBackups: _preferencesBackups:
list: "Utworzone kopie zapasowe" list: "Utworzone kopie zapasowe"
saveNew: "Zapisz nową kopię zapasową" saveNew: "Zapisz nową kopię zapasową"
@ -1401,3 +1402,6 @@ _webhookSettings:
renote: "Po udostępnieniu wpisu" renote: "Po udostępnieniu wpisu"
reaction: "Po otrzymaniu reakcji" reaction: "Po otrzymaniu reakcji"
mention: "Po zostaniu wspomnianym" mention: "Po zostaniu wspomnianym"
_moderationLogTypes:
suspend: "Zawieś"
resetPassword: "Zresetuj hasło"

View file

@ -411,6 +411,7 @@ aboutMisskey: "Sobre Misskey"
administrator: "Administrador" administrator: "Administrador"
token: "Símbolo" token: "Símbolo"
2fa: "Autenticação de dois fatores" 2fa: "Autenticação de dois fatores"
setupOf2fa: "Configuração de autenticação de dois fatores"
totp: "Aplicativo Autenticador" totp: "Aplicativo Autenticador"
totpDescription: "Digite a senha de uso único informado pelo aplicativo autenticador" totpDescription: "Digite a senha de uso único informado pelo aplicativo autenticador"
moderator: "Moderador" moderator: "Moderador"
@ -918,6 +919,7 @@ pleaseSelect: "Por favor, selecione."
reverse: "Inversão" reverse: "Inversão"
colored: "Colorido" colored: "Colorido"
refreshInterval: "Intervalo de atualização" refreshInterval: "Intervalo de atualização"
label: "Etiqueta"
type: "Tipo" type: "Tipo"
speed: "Velocidade" speed: "Velocidade"
slow: "Lento" slow: "Lento"
@ -1008,6 +1010,7 @@ waitingForMailAuth: "Verificação de e-mail pendente "
icon: "Avatar" icon: "Avatar"
replies: "Responder" replies: "Responder"
renotes: "Repostar" renotes: "Repostar"
keepScreenOn: "Manter a tela do dispositivo sempre ligada"
_initialAccountSetting: _initialAccountSetting:
followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo."
_serverSettings: _serverSettings:
@ -1494,3 +1497,6 @@ _webhookSettings:
follow: "Quando seguindo um usuário" follow: "Quando seguindo um usuário"
followed: "Quando sendo seguido" followed: "Quando sendo seguido"
renote: "Quando repostado" renote: "Quando repostado"
_moderationLogTypes:
suspend: "Suspender"
resetPassword: "Redefinir senha"

View file

@ -704,3 +704,6 @@ _deck:
mentions: "Mențiuni" mentions: "Mențiuni"
_webhookSettings: _webhookSettings:
name: "Nume" name: "Nume"
_moderationLogTypes:
suspend: "Suspendă"
resetPassword: "Resetează parola"

View file

@ -1427,6 +1427,7 @@ _plugin:
install: "Установка расширений" install: "Установка расширений"
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями" manage: "Управление расширениями"
viewSource: "Просмотр исходника"
_preferencesBackups: _preferencesBackups:
list: "Существующие резервные копии" list: "Существующие резервные копии"
saveNew: "Создать резервную копию" saveNew: "Создать резервную копию"
@ -1608,7 +1609,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена." alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerTOTP: "Начните настраивать приложение-аутентификатор" registerTOTP: "Начните настраивать приложение-аутентификатор"
passwordToTOTP: "Пожалуйста, введите свой пароль"
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве." step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве."
@ -1951,3 +1951,6 @@ _webhookSettings:
createWebhook: "Создать вебхук" createWebhook: "Создать вебхук"
name: "Название" name: "Название"
active: "Вкл." active: "Вкл."
_moderationLogTypes:
suspend: "Заморозить"
resetPassword: "Сброс пароля:"

View file

@ -978,6 +978,7 @@ _plugin:
install: "Inštalova pluginy" install: "Inštalova pluginy"
installWarn: "Prosím neinštalujte nedôveryhodné pluginy." installWarn: "Prosím neinštalujte nedôveryhodné pluginy."
manage: "Spravovanie pluginov" manage: "Spravovanie pluginov"
viewSource: "Ukázať zdroj"
_preferencesBackups: _preferencesBackups:
list: "Vytvorené zálohy" list: "Vytvorené zálohy"
saveNew: "Uložiť novú" saveNew: "Uložiť novú"
@ -1450,3 +1451,6 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Názov" name: "Názov"
active: "Zapnuté" active: "Zapnuté"
_moderationLogTypes:
suspend: "Zmraziť"
resetPassword: "Resetovať heslo"

View file

@ -510,7 +510,6 @@ _sfx:
chat: "Chatt" chat: "Chatt"
antenna: "Antenner" antenna: "Antenner"
_2fa: _2fa:
passwordToTOTP: "Skriv in ditt lösenord"
renewTOTPCancel: "Nej tack" renewTOTPCancel: "Nej tack"
_antennaSources: _antennaSources:
all: "Alla noter" all: "Alla noter"
@ -574,3 +573,6 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Namn" name: "Namn"
active: "Aktiverad" active: "Aktiverad"
_moderationLogTypes:
suspend: "Suspendera"
resetPassword: "Återställ Lösenord"

View file

@ -1509,6 +1509,7 @@ _plugin:
install: "ติดตั้งปลั๊กอิน" install: "ติดตั้งปลั๊กอิน"
installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ" installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ"
manage: "จัดการปลั๊กอิน" manage: "จัดการปลั๊กอิน"
viewSource: "ดูต้นฉบับ"
_preferencesBackups: _preferencesBackups:
list: "สร้างการสำรองข้อมูล" list: "สร้างการสำรองข้อมูล"
saveNew: "บันทึกใหม่" saveNew: "บันทึกใหม่"
@ -1696,7 +1697,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์"
passwordToTOTP: "กรอกรหัสผ่าน"
step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ"
step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้"
step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้" step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้"
@ -2065,3 +2065,6 @@ _webhookSettings:
renote: "รีโน้ตแล้วเมื่อ" renote: "รีโน้ตแล้วเมื่อ"
reaction: "เมื่อได้รับรีแอคชั่น" reaction: "เมื่อได้รับรีแอคชั่น"
mention: "เมื่อกำลังถูกกล่าวถึง" mention: "เมื่อกำลังถูกกล่าวถึง"
_moderationLogTypes:
suspend: "ถูกระงับ"
resetPassword: "รีเซ็ตรหัสผ่าน"

View file

@ -448,3 +448,6 @@ _deck:
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"
list: "Listeler" list: "Listeler"
mentions: "Bahsetmeler" mentions: "Bahsetmeler"
_moderationLogTypes:
suspend: "askıya al"
resetPassword: "Şifre sıfırlama"

View file

@ -1180,6 +1180,7 @@ _plugin:
install: "Встановити плагін" install: "Встановити плагін"
installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте." installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте."
manage: "Керування плагінами" manage: "Керування плагінами"
viewSource: "Переглянути вихідний код"
_preferencesBackups: _preferencesBackups:
list: "Створені бекапи" list: "Створені бекапи"
saveNew: "Зберегти як новий" saveNew: "Зберегти як новий"
@ -1619,3 +1620,6 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Ім'я" name: "Ім'я"
active: "Увімкнено" active: "Увімкнено"
_moderationLogTypes:
suspend: "Призупинити"
resetPassword: "Скинути пароль"

View file

@ -1084,3 +1084,6 @@ _webhookSettings:
_events: _events:
renote: "Qayta qayd qilinganda" renote: "Qayta qayd qilinganda"
mention: "Eslanganda" mention: "Eslanganda"
_moderationLogTypes:
suspend: "To'xtatish"
resetPassword: "Parolni tiklash"

View file

@ -1006,9 +1006,9 @@ enableChartsForRemoteUser: "Tạo biểu đồ người dùng từ xa"
video: "Video" video: "Video"
videos: "Các video" videos: "Các video"
dataSaver: "Tiết kiệm dung lượng" dataSaver: "Tiết kiệm dung lượng"
accountMigration: "Gộp chung tài khoản" accountMigration: "Chuyển tài khoản"
accountMoved: "Người dùng này đã chuyển sang một tài khoản mới:" accountMoved: "Người dùng này đã chuyển sang một tài khoản mới:"
accountMovedShort: "Tài khoản này đã được gộp" accountMovedShort: "Tài khoản này đã được chuyển"
operationForbidden: "Thao tác này không thể thực hiện" operationForbidden: "Thao tác này không thể thực hiện"
forceShowAds: "Luôn hiện quảng cáo" forceShowAds: "Luôn hiện quảng cáo"
notificationDisplay: "Thông báo" notificationDisplay: "Thông báo"
@ -1046,9 +1046,12 @@ renotes: "Đăng lại"
loadReplies: "Hiển thị các trả lời" loadReplies: "Hiển thị các trả lời"
pinnedList: "Các mục đã được ghim" pinnedList: "Các mục đã được ghim"
keepScreenOn: "Giữ màn hình luôn bật" keepScreenOn: "Giữ màn hình luôn bật"
verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này"
_announcement: _announcement:
forExistingUsers: "Chỉ những người dùng đã tồn tại" forExistingUsers: "Chỉ những người dùng đã tồn tại"
forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó."
end: "Lưu trữ thông báo" end: "Lưu trữ thông báo"
tooManyActiveAnnouncementDescription: "Có quá nhiều thông báo sẽ làm trải nghiệm của người dùng tệ đi. Vui lòng lưu trữ những thông báo đã hết hiệu lực."
readConfirmTitle: "Đánh dấu là đã đọc?" readConfirmTitle: "Đánh dấu là đã đọc?"
readConfirmText: "Điều này sẽ đánh dấu nội dung của \"{title}\" là đã đọc." readConfirmText: "Điều này sẽ đánh dấu nội dung của \"{title}\" là đã đọc."
_initialAccountSetting: _initialAccountSetting:
@ -1056,7 +1059,28 @@ _initialAccountSetting:
letsStartAccountSetup: "Để bắt đầu, hãy cùng thiết lập tài khoản nhé." letsStartAccountSetup: "Để bắt đầu, hãy cùng thiết lập tài khoản nhé."
letsFillYourProfile: "Đầu tiên, hãy thiết lập hồ sơ của bạn." letsFillYourProfile: "Đầu tiên, hãy thiết lập hồ sơ của bạn."
profileSetting: "Thiết lập hồ sơ" profileSetting: "Thiết lập hồ sơ"
privacySetting: "Cài đặt quyền riêng tư"
theseSettingsCanEditLater: "Bạn vẫn có thể thay đổi những cài đặt này."
youCanEditMoreSettingsInSettingsPageLater: "Còn rất nhiều những cài đặt khác bạn có thể thay đổi ở trang \"Cài đặt\". Hãy nhớ ghé thăm trong lần sau nhé."
followUsers: "Thử theo dõi một vài người mà bạn có thể thích để xây dựng dòng thời gian của mình."
pushNotificationDescription: "Bật thông báo đẩy sẽ cho phép bạn nhận thông báo từ {name} trực tiếp từ thiết bị của bạn."
initialAccountSettingCompleted: "Thiết lập tài khoản thành công!"
haveFun: "Hãy tận hưởng {name} nhé!"
ifYouNeedLearnMore: "Nếu bạn muốn tìm hiểu thêm về cách sử dụng {name} (Misskey), hãy vào {link}."
skipAreYouSure: "Bạn thực sự muốn bỏ qua mục thiết lập tài khoản?"
laterAreYouSure: "Bạn thực sự muốn thiết lập tài khoản vào lúc khác?"
_serverSettings:
iconUrl: "Biểu tượng URL"
appIconResolutionMustBe: "Độ phân giải tối thiểu là {resolution}."
manifestJsonOverride: "Ghi đè manifest.json"
_accountMigration: _accountMigration:
moveFrom: "Chuyển một tài khoản khác vào tài khoản này"
moveFromLabel: "Tài khoản gốc #{n}"
moveTo: "Chuyển tài khoản này vào một tài khoản khác"
moveCannotBeUndone: "Việc chuyển tài khoản không thể huỷ."
moveAccountDescription: "Điều này sẽ chuyển tài khoản này sang một tài khoản khác.\n ・Những người theo dõi sẽ tự động được chuyển sang tài khoản mới\n ・Tài khoản này sẽ tự bỏ theo dõi những người mà bạn đã theo dõi trước đây\n ・Bạn sẽ không thể đăng tút mới, v.v trên tài khoản này\n\nDù việc chuyển người theo dõi được diễn ra tự động, bạn vẫn phải tự chuẩn bị một vài bước để chuyển danh sách những người dùng bạn đang theo dõi. Để làm vậy, vui lòng thực hiện việc xuất dữ liệu những người dùng đã theo dõi mà sau này bạn sẽ dùng để nhập vào tài khoản mới ở menu Cài đặt. Hành động tương tự áp dụng với danh sách những người dùng bị chặn hoặc tắt tiếng.\n\n(Điều này áp dụng cho phiên bản Misskey v13.12.0 và sau này. Các phần mềm ActivityPub khác , ví dụ như Mastodon, sẽ có thể hoạt động khác đi.)"
startMigration: "Chuyển"
movedAndCannotBeUndone: "\nTài khoản này đã được chuyển đi.\nViệc di chuyển tài khoản không thể bị huỷ bỏ."
movedTo: "Tài khoản mới:" movedTo: "Tài khoản mới:"
_achievements: _achievements:
earnedAt: "Ngày thu nhận" earnedAt: "Ngày thu nhận"
@ -1096,6 +1120,8 @@ _achievements:
title: "Hàng tinh đăng bài" title: "Hàng tinh đăng bài"
description: "Đã đăng bài 50,000 lần rồi" description: "Đã đăng bài 50,000 lần rồi"
_notes100000: _notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "Đăng 100,000 tút"
flavor: "Liệu viết bài gì tầm này vậy? " flavor: "Liệu viết bài gì tầm này vậy? "
_login3: _login3:
title: "Sơ cấp I" title: "Sơ cấp I"
@ -1127,6 +1153,15 @@ _achievements:
_login400: _login400:
title: "Khách hàng thường xuyên cấp III" title: "Khách hàng thường xuyên cấp III"
description: "Tổng số ngày đăng nhập đạt 400 ngày" description: "Tổng số ngày đăng nhập đạt 400 ngày"
_login1000:
flavor: "Cảm ơn bạn đã sử dụng Misskey!"
_noteFavorited1:
title: "Nhà thiên văn học"
_myNoteFavorited1:
title: "Đi tìm những ngôi sao"
_profileFilled:
title: "Luôn sẵn sàng"
description: "Thiết lập tài khoản của bạn"
_markedAsCat: _markedAsCat:
title: "Tôi là một con mèo" title: "Tôi là một con mèo"
description: "Bật chế độ mèo" description: "Bật chế độ mèo"
@ -1152,8 +1187,18 @@ _achievements:
_followers10: _followers10:
title: "FOLLOW ME!!" title: "FOLLOW ME!!"
description: "Người theo dõi bạn vượt lên 10 người" description: "Người theo dõi bạn vượt lên 10 người"
_followers50:
title: "Từng chút một"
description: "Đạt được 50 lượt theo dõi"
_followers100:
title: "Người nổi tiếng"
description: "Đạt được 100 lượt theo dõi"
_followers300:
title: "Vui lòng xếp thành hàng nào"
description: "Đạt được 300 lượt theo dõi"
_followers500: _followers500:
title: "Trạm phát sóng" title: "Trạm phát sóng"
description: "Đạt được 500 lượt theo dõi"
_followers1000: _followers1000:
title: "Người có tầm ảnh hưởng" title: "Người có tầm ảnh hưởng"
description: "Người theo dõi bạn vượt lên 1000 người" description: "Người theo dõi bạn vượt lên 1000 người"
@ -1172,11 +1217,15 @@ _achievements:
description: "Tìm thấy được những kho báu cất giấu" description: "Tìm thấy được những kho báu cất giấu"
_client30min: _client30min:
title: "Giải lao xỉu" title: "Giải lao xỉu"
description: "Giữ Misskey mở trong ít nhất 30 phút"
_client60min:
description: "Giữ Misskey mở trong ít nhất 60 phút"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Xem như không có gì đâu nha" title: "Xem như không có gì đâu nha"
_postedAtLateNight: _postedAtLateNight:
title: "Loài ăn đêm" title: "Loài ăn đêm"
description: "Đăng bài trong đêm khuya " description: "Đăng bài trong đêm khuya "
flavor: "Đến giờ đi ngủ rồi."
_postedAt0min0sec: _postedAt0min0sec:
title: "Tín hiệu báo giờ" title: "Tín hiệu báo giờ"
description: "Đăng bài vào 0 phút 0 giây" description: "Đăng bài vào 0 phút 0 giây"
@ -1207,6 +1256,8 @@ _achievements:
_setNameToSyuilo: _setNameToSyuilo:
title: "Ngưỡng mộ với vị thần" title: "Ngưỡng mộ với vị thần"
description: "Đạt tên là syuilo" description: "Đạt tên là syuilo"
_passedSinceAccountCreated1:
title: "Kỷ niệm một năm"
_loggedInOnBirthday: _loggedInOnBirthday:
title: "Sinh nhật vủi vẻ" title: "Sinh nhật vủi vẻ"
description: "Đăng nhập vào ngày sinh" description: "Đăng nhập vào ngày sinh"
@ -1292,6 +1343,7 @@ _plugin:
install: "Cài đặt tiện ích" install: "Cài đặt tiện ích"
installWarn: "Vui lòng không cài đặt những tiện ích đáng ngờ." installWarn: "Vui lòng không cài đặt những tiện ích đáng ngờ."
manage: "Quản lý plugin" manage: "Quản lý plugin"
viewSource: "Xem mã nguồn"
_preferencesBackups: _preferencesBackups:
list: "Tạo sao lưu" list: "Tạo sao lưu"
saveNew: "Lưu bản sao lưu" saveNew: "Lưu bản sao lưu"
@ -1466,7 +1518,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước."
registerTOTP: "Đăng ký ứng dụng xác thực" registerTOTP: "Đăng ký ứng dụng xác thực"
passwordToTOTP: "Nhắn mật mã"
step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn."
step2: "Sau đó, quét mã QR hiển thị trên màn hình này." step2: "Sau đó, quét mã QR hiển thị trên màn hình này."
step2Click: "Quét mã QR trên ứng dụng xác thực (Authy, Google authenticator, v.v.)" step2Click: "Quét mã QR trên ứng dụng xác thực (Authy, Google authenticator, v.v.)"
@ -1809,3 +1860,6 @@ _webhookSettings:
_events: _events:
reaction: "Khi nhận được sự kiện" reaction: "Khi nhận được sự kiện"
mention: "Khi có người nhắc tới bạn" mention: "Khi có người nhắc tới bạn"
_moderationLogTypes:
suspend: "Vô hiệu hóa"
resetPassword: "Đặt lại mật khẩu"

View file

@ -418,6 +418,7 @@ moderator: "监察员"
moderation: "管理" moderation: "管理"
moderationNote: "管理笔记" moderationNote: "管理笔记"
addModerationNote: "添加管理笔记" addModerationNote: "添加管理笔记"
moderationLogs: "管理日志"
nUsersMentioned: "{n} 被提到" nUsersMentioned: "{n} 被提到"
securityKeyAndPasskey: "安全密钥或 Passkey" securityKeyAndPasskey: "安全密钥或 Passkey"
securityKey: "安全密钥" securityKey: "安全密钥"
@ -710,6 +711,7 @@ lockedAccountInfo: "即使启用该功能,只要您不将帖子可见范围设
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容" alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
loadRawImages: "添加附件图像的缩略图时使用原始图像质量" loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
disableShowingAnimatedImages: "不播放动画" disableShowingAnimatedImages: "不播放动画"
highlightSensitiveMedia: "高亮显示敏感媒体"
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。" verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
notSet: "未设置" notSet: "未设置"
emailVerified: "电子邮件地址已验证" emailVerified: "电子邮件地址已验证"
@ -1113,6 +1115,11 @@ loadReplies: "查看回复"
loadConversation: "查看对话" loadConversation: "查看对话"
pinnedList: "已置顶的列表" pinnedList: "已置顶的列表"
keepScreenOn: "保持设备屏幕开启" keepScreenOn: "保持设备屏幕开启"
verifiedLink: "已验证的链接"
notifyNotes: "打开发帖通知"
unnotifyNotes: "关闭发帖通知"
authentication: "验证"
authenticationRequiredToContinue: "要继续,请先进行验证"
_announcement: _announcement:
forExistingUsers: "仅限现有用户" forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
@ -1146,6 +1153,8 @@ _serverSettings:
appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。" appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。"
appIconResolutionMustBe: "分辨率必须为 {resolution}。" appIconResolutionMustBe: "分辨率必须为 {resolution}。"
manifestJsonOverride: "覆盖 mainfest.json" manifestJsonOverride: "覆盖 mainfest.json"
shortName: "简称"
shortNameDescription: "如果服务器的正式名称很长,可以用简称或者別名来替代。"
_accountMigration: _accountMigration:
moveFrom: "从别的账号迁移到此账户" moveFrom: "从别的账号迁移到此账户"
moveFromSub: "为另一个账户建立别名" moveFromSub: "为另一个账户建立别名"
@ -1526,6 +1535,7 @@ _plugin:
install: "安装插件" install: "安装插件"
installWarn: "请不要安装不可信的插件。" installWarn: "请不要安装不可信的插件。"
manage: "管理插件..." manage: "管理插件..."
viewSource: "查看源代码"
_preferencesBackups: _preferencesBackups:
list: "已创建的备份" list: "已创建的备份"
saveNew: "另存为" saveNew: "另存为"
@ -1713,7 +1723,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "此设备已被注册" alreadyRegistered: "此设备已被注册"
registerTOTP: "开始设置认证应用" registerTOTP: "开始设置认证应用"
passwordToTOTP: "请输入您的密码"
step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。" step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
step2: "然后,扫描屏幕上显示的二维码。" step2: "然后,扫描屏幕上显示的二维码。"
step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册"
@ -1792,6 +1801,7 @@ _antennaSources:
homeTimeline: "已关注用户的帖子" homeTimeline: "已关注用户的帖子"
users: "来自指定用户的帖子" users: "来自指定用户的帖子"
userList: "来自指定列表中的帖子" userList: "来自指定列表中的帖子"
userBlacklist: "除掉已选择用户后所有的帖子"
_weekday: _weekday:
sunday: "星期日" sunday: "星期日"
monday: "星期一" monday: "星期一"
@ -1891,6 +1901,7 @@ _profile:
metadataContent: "内容" metadataContent: "内容"
changeAvatar: "修改头像" changeAvatar: "修改头像"
changeBanner: "修改横幅" changeBanner: "修改横幅"
verifiedLinkDescription: "如果将内容设置为 URL当链接所指向的网页内包含自己的个人资料链接时可以显示一个已验证图标。"
_exportOrImport: _exportOrImport:
allNotes: "所有帖子" allNotes: "所有帖子"
favoritedNotes: "收藏的帖子" favoritedNotes: "收藏的帖子"
@ -2009,6 +2020,7 @@ _notification:
youReceivedFollowRequest: "您有新的关注请求" youReceivedFollowRequest: "您有新的关注请求"
yourFollowRequestAccepted: "您的关注请求已通过" yourFollowRequestAccepted: "您的关注请求已通过"
pollEnded: "问卷调查结果已生成。" pollEnded: "问卷调查结果已生成。"
newNote: "新的帖子"
unreadAntennaNote: "天线 {name}" unreadAntennaNote: "天线 {name}"
emptyPushNotificationMessage: "推送通知已更新" emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "获得成就" achievementEarned: "获得成就"
@ -2018,6 +2030,7 @@ _notification:
notificationWillBeDisplayedLikeThis: "通知将会这样表示" notificationWillBeDisplayedLikeThis: "通知将会这样表示"
_types: _types:
all: "全部" all: "全部"
note: "用户的新帖子"
follow: "关注中" follow: "关注中"
mention: "提及" mention: "提及"
reply: "回复" reply: "回复"
@ -2087,3 +2100,25 @@ _webhookSettings:
renote: "被转发时" renote: "被转发时"
reaction: "被回应时" reaction: "被回应时"
mention: "被提及时" mention: "被提及时"
_moderationLogTypes:
updateRole: "更新角色"
assignRole: "分配角色"
unassignRole: "取消分配角色"
suspend: "冻结"
unsuspend: "解除冻结"
addCustomEmoji: "添加自定义表情符号"
updateCustomEmoji: "更新自定义表情符号"
deleteCustomEmoji: "删除自定义表情符号"
updateServerSettings: "更新服务器设置"
updateUserNote: "更新管理笔记"
deleteDriveFile: "删除文件"
deleteNote: "删除帖子"
createGlobalAnnouncement: "创建全体通知"
createUserAnnouncement: "创建用户通知"
updateGlobalAnnouncement: "更新全体通知"
updateUserAnnouncement: "更新用户通知"
deleteGlobalAnnouncement: "删除全体通知"
deleteUserAnnouncement: "删除用户通知"
resetPassword: "重置密码"
markSensitiveDriveFile: "标记网盘文件为敏感媒体"
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"

View file

@ -1,6 +1,6 @@
--- ---
_lang_: "繁體中文" _lang_: "繁體中文(台灣)"
headlineMisskey: "貼文連繫網" headlineMisskey: "貼文連繫網"
introMisskey: "歡迎Misskey 是一個開放原始碼且去中心化的社群網路服務。\n發布「貼文」向身邊的人分享您的想法📡\n利用「反應」表達您對貼文的感覺👍\n讓我們一起探索新的世界吧🚀" introMisskey: "歡迎Misskey 是一個開放原始碼且去中心化的社群網路服務。\n發布「貼文」向身邊的人分享您的想法📡\n利用「反應」表達您對貼文的感覺👍\n讓我們一起探索新的世界吧🚀"
poweredByMisskeyDescription: "{name}是開放原始碼平臺 <b>Misskey</b> 的伺服器之一。" poweredByMisskeyDescription: "{name}是開放原始碼平臺 <b>Misskey</b> 的伺服器之一。"
monthAndDay: "{month} 月 {day} 日" monthAndDay: "{month} 月 {day} 日"
@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消" cancel: "取消"
noThankYou: "現在不要" noThankYou: "現在不要"
enterUsername: "輸入使用者名稱" enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉傳了" renotedBy: "{user} 轉"
noNotes: "無貼文" noNotes: "無貼文"
noNotifications: "沒有通知" noNotifications: "沒有通知"
instance: "伺服器" instance: "伺服器"
@ -45,7 +45,7 @@ pin: "置頂"
unpin: "取消置頂" unpin: "取消置頂"
copyContent: "複製內容" copyContent: "複製內容"
copyLink: "複製連結" copyLink: "複製連結"
copyLinkRenote: "複製轉連結" copyLinkRenote: "複製轉發的連結"
delete: "刪除" delete: "刪除"
deleteAndEdit: "刪除並編輯" deleteAndEdit: "刪除並編輯"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。" deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。"
@ -106,10 +106,10 @@ unfollow: "取消追隨"
followRequestPending: "追隨許可待批准" followRequestPending: "追隨許可待批准"
enterEmoji: "輸入表情符號" enterEmoji: "輸入表情符號"
renote: "轉發" renote: "轉發"
unrenote: "取消轉" unrenote: "取消轉"
renoted: "轉傳成功" renoted: "轉發成功。"
cantRenote: "無法轉此貼文。" cantRenote: "無法轉此貼文。"
cantReRenote: "無法轉傳之前已經轉傳過的內容。" cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用" quote: "引用"
inChannelRenote: "在頻道內轉發" inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用" inChannelQuote: "在頻道內引用"
@ -138,8 +138,8 @@ suspend: "凍結"
unsuspend: "解除凍結" unsuspend: "解除凍結"
blockConfirm: "確定要封鎖此使用者嗎?" blockConfirm: "確定要封鎖此使用者嗎?"
unblockConfirm: "確定要解除封鎖此使用者嗎?" unblockConfirm: "確定要解除封鎖此使用者嗎?"
suspendConfirm: "確定凍結此帳戶" suspendConfirm: "確定凍結此使用者"
unsuspendConfirm: "確定解凍此帳戶" unsuspendConfirm: "確定解凍此使用者"
selectList: "選擇清單" selectList: "選擇清單"
editList: "編輯清單" editList: "編輯清單"
selectChannel: "選擇頻道" selectChannel: "選擇頻道"
@ -152,12 +152,12 @@ customEmojis: "自訂表情符號"
emoji: "表情符號" emoji: "表情符號"
emojis: "表情符號" emojis: "表情符號"
emojiName: "表情符號名稱" emojiName: "表情符號名稱"
emojiUrl: "表情符號URL" emojiUrl: "表情符號 URL"
addEmoji: "新增表情符號" addEmoji: "新增表情符號"
settingGuide: "推薦設定" settingGuide: "推薦設定"
cacheRemoteFiles: "快取遠端檔案" cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "禁用此設定會停止建立遠端檔案快取,從而節省伺服器儲存空間,但會因從遠端讀取資料而增加網路數據用量。" cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但為了產生圖片的縮圖並保護使用者的隱私,建議將 default.yml 的 proxyRemoteFiles 設為 true。"
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,將快取全部刪除。" youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,將快取全部刪除。"
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案" cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。" cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
flagAsBot: "此使用者是機器人" flagAsBot: "此使用者是機器人"
@ -321,7 +321,7 @@ copyUrl: "複製URL"
rename: "重新命名" rename: "重新命名"
avatar: "大頭貼" avatar: "大頭貼"
banner: "橫幅" banner: "橫幅"
displayOfSensitiveMedia: "顯示敏感媒體" displayOfSensitiveMedia: "敏感檔案的顯示"
whenServerDisconnected: "與伺服器的連接中斷時" whenServerDisconnected: "與伺服器的連接中斷時"
disconnectedFromServer: "與伺服器中斷連線" disconnectedFromServer: "與伺服器中斷連線"
reload: "重新整理" reload: "重新整理"
@ -418,12 +418,13 @@ moderator: "審查員"
moderation: "審查" moderation: "審查"
moderationNote: "管理筆記" moderationNote: "管理筆記"
addModerationNote: "新增管理筆記" addModerationNote: "新增管理筆記"
nUsersMentioned: "被提及到 {n} 次" moderationLogs: "管理日誌"
nUsersMentioned: "被 {n} 個人提及"
securityKeyAndPasskey: "安全金鑰、Passkey" securityKeyAndPasskey: "安全金鑰、Passkey"
securityKey: "安全金鑰" securityKey: "安全金鑰"
lastUsed: "上次使用" lastUsed: "上次使用"
lastUsedAt: "上次使用:{t}" lastUsedAt: "上次使用:{t}"
unregister: "註銷帳戶" unregister: "註銷"
passwordLessLogin: "設置無密碼登入" passwordLessLogin: "設置無密碼登入"
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
resetPassword: "重設密碼" resetPassword: "重設密碼"
@ -490,7 +491,7 @@ createAccount: "建立帳戶"
existingAccount: "現有帳戶" existingAccount: "現有帳戶"
regenerate: "再次生成" regenerate: "再次生成"
fontSize: "字體大小" fontSize: "字體大小"
mediaListWithOneImageAppearance: "只有一張圖片時的媒體列表高度" mediaListWithOneImageAppearance: "只有一張圖片時的檔案列表高度"
limitTo: "上限為 {x}" limitTo: "上限為 {x}"
noFollowRequests: "沒有追隨您的請求" noFollowRequests: "沒有追隨您的請求"
openImageInNewTab: "於新分頁中開啟圖片" openImageInNewTab: "於新分頁中開啟圖片"
@ -508,8 +509,8 @@ promote: "推廣"
numberOfDays: "有效天數" numberOfDays: "有效天數"
hideThisNote: "隱藏此貼文" hideThisNote: "隱藏此貼文"
showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦" showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
objectStorage: "對象存儲" objectStorage: "物件儲存"
useObjectStorage: "使用對象存儲" useObjectStorage: "使用物件儲存"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL例如 S3https://<bucket>.s3.amazonaws.com、GCShttps://storage.googleapis.com/<bucket>)。" objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL例如 S3https://<bucket>.s3.amazonaws.com、GCShttps://storage.googleapis.com/<bucket>)。"
objectStorageBucket: "儲存空間Bucket" objectStorageBucket: "儲存空間Bucket"
@ -546,7 +547,7 @@ recentUsed: "最近使用"
install: "安裝" install: "安裝"
uninstall: "解除安裝" uninstall: "解除安裝"
installedApps: "已授權的應用程式" installedApps: "已授權的應用程式"
nothing: "無" nothing: "項目"
installedDate: "安裝時間" installedDate: "安裝時間"
lastUsedDate: "最後上線日期" lastUsedDate: "最後上線日期"
state: "狀態" state: "狀態"
@ -657,7 +658,7 @@ behavior: "行為"
sample: "範例" sample: "範例"
abuseReports: "檢舉" abuseReports: "檢舉"
reportAbuse: "檢舉" reportAbuse: "檢舉"
reportAbuseRenote: "檢舉轉貼" reportAbuseRenote: "檢舉轉"
reportAbuseOf: "檢舉{name}" reportAbuseOf: "檢舉{name}"
fillAbuseReportDescription: "請填寫檢舉的詳細理由。如有需要,請附上相關 URL。" fillAbuseReportDescription: "請填寫檢舉的詳細理由。如有需要,請附上相關 URL。"
abuseReported: "檢舉完成。感謝您的報告。" abuseReported: "檢舉完成。感謝您的報告。"
@ -707,9 +708,10 @@ driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引" noCrawle: "拒絕搜尋引擎索引"
noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。" noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。"
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。" lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
alwaysMarkSensitive: "預設將多媒體標記為敏感內容" alwaysMarkSensitive: "預設標記檔案為敏感內容"
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖" loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
disableShowingAnimatedImages: "不播放動態圖檔" disableShowingAnimatedImages: "不播放動態圖檔"
highlightSensitiveMedia: "強調敏感標記"
verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。" verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。"
notSet: "未設定" notSet: "未設定"
emailVerified: "已成功驗證您的電郵" emailVerified: "已成功驗證您的電郵"
@ -926,7 +928,7 @@ type: "類型"
speed: "速度" speed: "速度"
slow: "慢" slow: "慢"
fast: "快" fast: "快"
sensitiveMediaDetection: "敏感性媒體的檢測" sensitiveMediaDetection: "敏感檔案的檢測"
localOnly: "僅限本地" localOnly: "僅限本地"
remoteOnly: "僅限遠端" remoteOnly: "僅限遠端"
failedToUpload: "上傳失敗" failedToUpload: "上傳失敗"
@ -935,7 +937,7 @@ cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此
cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。" cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。"
beta: "測試版" beta: "測試版"
enableAutoSensitive: "自動 NSFW 判定" enableAutoSensitive: "自動 NSFW 判定"
enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷多媒體內容是否需要標記 NSFW。即使關閉此功能,也可能會依實例規則而自動啟用。" enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依實例規則而自動啟用。"
activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。" activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。"
navbar: "導覽列" navbar: "導覽列"
shuffle: "隨機" shuffle: "隨機"
@ -1112,6 +1114,13 @@ renotes: "轉發"
loadReplies: "閱覽回覆" loadReplies: "閱覽回覆"
loadConversation: "閱覽對話" loadConversation: "閱覽對話"
pinnedList: "已置頂的清單" pinnedList: "已置頂的清單"
keepScreenOn: "保持設備螢幕開啟"
verifiedLink: "已驗證連結"
notifyNotes: "開啟貼文通知"
unnotifyNotes: "關閉貼文通知"
authentication: "驗證"
authenticationRequiredToContinue: "請於繼續前完成驗證"
dateAndTime: "日期與時間"
_announcement: _announcement:
forExistingUsers: "僅限既有的使用者" forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1145,6 +1154,8 @@ _serverSettings:
appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。" appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。"
appIconResolutionMustBe: "解析度必須為 {resolution}。" appIconResolutionMustBe: "解析度必須為 {resolution}。"
manifestJsonOverride: "覆寫 manifest.json" manifestJsonOverride: "覆寫 manifest.json"
shortName: "簡稱"
shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。"
_accountMigration: _accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶" moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名" moveFromSub: "為另一個帳戶建立別名"
@ -1474,7 +1485,7 @@ _role:
or: "~或~" or: "~或~"
not: "~否" not: "~否"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。" description: "您可以使用機器學習自動檢測敏感檔案以便審查。這會稍微增加伺服器負荷。"
sensitivity: "檢測敏感度" sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 標籤" setSensitiveFlagAutomatically: "設定 NSFW 標籤"
@ -1525,6 +1536,7 @@ _plugin:
install: "安裝外掛組件" install: "安裝外掛組件"
installWarn: "請不要安裝來源不明的外掛。" installWarn: "請不要安裝來源不明的外掛。"
manage: "管理外掛" manage: "管理外掛"
viewSource: "檢視原始碼"
_preferencesBackups: _preferencesBackups:
list: "已備份的設定檔" list: "已備份的設定檔"
saveNew: "另存新檔" saveNew: "另存新檔"
@ -1559,9 +1571,9 @@ _aboutMisskey:
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
patrons: "贊助者" patrons: "贊助者"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "隱藏被標記為敏感的多媒體內容" respect: "隱藏敏感檔案"
ignore: "不隱藏被標記為敏感的多媒體內容" ignore: "顯示敏感檔案"
force: "隱藏所有多媒體內容" force: "隱藏所有檔案"
_instanceTicker: _instanceTicker:
none: "隱藏" none: "隱藏"
remote: "向遠端使用者顯示" remote: "向遠端使用者顯示"
@ -1687,7 +1699,7 @@ _ago:
future: "未來" future: "未來"
justNow: "剛剛" justNow: "剛剛"
secondsAgo: "{n} 秒前" secondsAgo: "{n} 秒前"
minutesAgo: "{n}分鐘前 " minutesAgo: "{n} 分鐘前 "
hoursAgo: "{n} 小時前" hoursAgo: "{n} 小時前"
daysAgo: "{n} 天前" daysAgo: "{n} 天前"
weeksAgo: "{n} 週前" weeksAgo: "{n} 週前"
@ -1712,7 +1724,6 @@ _timelineTutorial:
_2fa: _2fa:
alreadyRegistered: "此裝置已被註冊過了" alreadyRegistered: "此裝置已被註冊過了"
registerTOTP: "開始設定驗證應用程式" registerTOTP: "開始設定驗證應用程式"
passwordToTOTP: "請輸入密碼"
step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。" step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。"
step2: "然後,掃描螢幕上的 QR 碼。" step2: "然後,掃描螢幕上的 QR 碼。"
step2Click: "您可以點擊 QR 碼,以使用裝置上的驗證應用程式或金鑰環註冊。" step2Click: "您可以點擊 QR 碼,以使用裝置上的驗證應用程式或金鑰環註冊。"
@ -1791,6 +1802,7 @@ _antennaSources:
homeTimeline: "來自已追隨使用者的貼文" homeTimeline: "來自已追隨使用者的貼文"
users: "來自特定使用者的貼文" users: "來自特定使用者的貼文"
userList: "來自特定清單中的貼文" userList: "來自特定清單中的貼文"
userBlacklist: "除指定使用者外的所有貼文"
_weekday: _weekday:
sunday: "週日" sunday: "週日"
monday: "週一" monday: "週一"
@ -1890,6 +1902,7 @@ _profile:
metadataContent: "内容" metadataContent: "内容"
changeAvatar: "更換大頭貼" changeAvatar: "更換大頭貼"
changeBanner: "變更橫幅圖像" changeBanner: "變更橫幅圖像"
verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL欄位旁邊將出現驗證圖示。"
_exportOrImport: _exportOrImport:
allNotes: "所有貼文" allNotes: "所有貼文"
favoritedNotes: "「我的最愛」貼文" favoritedNotes: "「我的最愛」貼文"
@ -2008,6 +2021,7 @@ _notification:
youReceivedFollowRequest: "您有新的追隨請求" youReceivedFollowRequest: "您有新的追隨請求"
yourFollowRequestAccepted: "您的追隨請求已通過" yourFollowRequestAccepted: "您的追隨請求已通過"
pollEnded: "問卷調查已產生結果" pollEnded: "問卷調查已產生結果"
newNote: "新的貼文"
unreadAntennaNote: "天線 {name}" unreadAntennaNote: "天線 {name}"
emptyPushNotificationMessage: "推送通知已更新" emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "獲得成就" achievementEarned: "獲得成就"
@ -2017,6 +2031,7 @@ _notification:
notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示" notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示"
_types: _types:
all: "全部 " all: "全部 "
note: "使用者的最新貼文"
follow: "追隨中" follow: "追隨中"
mention: "提及" mention: "提及"
reply: "回覆" reply: "回覆"
@ -2086,3 +2101,29 @@ _webhookSettings:
renote: "當被轉發時" renote: "當被轉發時"
reaction: "當獲得反應時" reaction: "當獲得反應時"
mention: "當被提到時" mention: "當被提到時"
_moderationLogTypes:
createRole: "新增角色"
deleteRole: "刪除角色 "
updateRole: "更新角色設定"
assignRole: "指派角色"
unassignRole: "撤銷角色"
suspend: "凍結"
unsuspend: "解除凍結"
addCustomEmoji: "新增自訂表情符號"
updateCustomEmoji: "更新自訂表情符號"
deleteCustomEmoji: "刪除自訂表情符號"
updateServerSettings: "更新伺服器設定"
updateUserNote: "更新管理筆記"
deleteDriveFile: "刪除檔案"
deleteNote: "刪除貼文"
createGlobalAnnouncement: "建立全網通知"
createUserAnnouncement: "建立使用者通知"
updateGlobalAnnouncement: "更新全部的公告"
updateUserAnnouncement: "更新使用者的公告"
deleteGlobalAnnouncement: "刪除全部的公告"
deleteUserAnnouncement: "刪除使用者的公告"
resetPassword: "重設密碼"
suspendRemoteInstance: "封鎖遠端伺服器"
unsuspendRemoteInstance: "解除封鎖遠端伺服器"
markSensitiveDriveFile: "標記為敏感檔案"
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"

View file

@ -1,7 +1,7 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2023.9.0-beta.10", "version": "2023.9.1.beta1",
"codename": "nasubi", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/transfem-org/sharkey.git" "url": "https://github.com/transfem-org/sharkey.git"
@ -55,8 +55,8 @@
"@typescript-eslint/parser": "6.7.2", "@typescript-eslint/parser": "6.7.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.2.0", "cypress": "13.2.0",
"eslint": "8.49.0", "eslint": "8.50.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0" "@tensorflow/tfjs-core": "4.4.0"

View file

@ -0,0 +1,10 @@
export class UserBlacklistAnntena1689325027964 {
name = 'UserBlacklistAnntena1689325027964'
async up(queryRunner) {
await queryRunner.query(`ALTER TYPE "antenna_src_enum" ADD VALUE 'users_blacklist' AFTER 'list'`);
}
async down(queryRunner) {
}
}

View file

@ -0,0 +1,11 @@
export class ShortName1695440131671 {
name = 'ShortName1695440131671'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "shortName" character varying(64)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "shortName"`);
}
}

View file

@ -0,0 +1,21 @@
export class MutingNotificationTypes1695605508898 {
name = 'MutingNotificationTypes1695605508898'
async up(queryRunner) {
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test', 'pollVote', 'groupInvited')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
}
}

View file

@ -70,15 +70,15 @@
"@fastify/multipart": "7.7.3", "@fastify/multipart": "7.7.3",
"@fastify/static": "6.11.2", "@fastify/static": "6.11.2",
"@fastify/view": "8.2.0", "@fastify/view": "8.2.0",
"@nestjs/common": "10.2.5", "@nestjs/common": "10.2.6",
"@nestjs/core": "10.2.5", "@nestjs/core": "10.2.6",
"@nestjs/testing": "10.2.5", "@nestjs/testing": "10.2.6",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.1.1", "@simplewebauthn/server": "8.1.1",
"@sinonjs/fake-timers": "11.1.0", "@sinonjs/fake-timers": "11.1.0",
"@smithy/node-http-handler": "2.1.5", "@smithy/node-http-handler": "2.1.5",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.86", "@swc/core": "1.3.87",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "6.0.1", "archiver": "6.0.1",
@ -87,7 +87,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "4.11.2", "bullmq": "4.11.4",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -178,7 +178,7 @@
"@simplewebauthn/typescript-types": "8.0.0", "@simplewebauthn/typescript-types": "8.0.0",
"@swc/jest": "0.2.29", "@swc/jest": "0.2.29",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.2", "@types/archiver": "5.3.3",
"@types/bcryptjs": "2.4.4", "@types/bcryptjs": "2.4.4",
"@types/body-parser": "1.19.3", "@types/body-parser": "1.19.3",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
@ -193,9 +193,9 @@
"@types/jsrsasign": "10.5.9", "@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
"@types/node": "20.6.3", "@types/node": "20.6.4",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.10", "@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2", "@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.1", "@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0", "@types/oauth2orize-pkce": "0.1.0",
@ -221,7 +221,7 @@
"@typescript-eslint/parser": "6.7.2", "@typescript-eslint/parser": "6.7.2",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.49.0", "eslint": "8.50.0",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.28.1",
"execa": "8.0.1", "execa": "8.0.1",
"jest": "29.7.0", "jest": "29.7.0",

View file

@ -7,11 +7,12 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead } from '@/models/_.js'; import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable() @Injectable()
export class AnnouncementService { export class AnnouncementService {
@ -22,8 +23,12 @@ export class AnnouncementService {
@Inject(DI.announcementReadsRepository) @Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository, private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) { ) {
} }
@ -58,7 +63,7 @@ export class AnnouncementService {
} }
@bindThis @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({ const announcement = await this.announcementsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@ -79,10 +84,28 @@ export class AnnouncementService {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', { this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
announcement: packed, announcement: packed,
}); });
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: values.userId });
this.moderationLogService.log(moderator, 'createUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
userId: values.userId,
userUsername: user.username,
userHost: user.host,
});
}
} else { } else {
this.globalEventService.publishBroadcastStream('announcementCreated', { this.globalEventService.publishBroadcastStream('announcementCreated', {
announcement: packed, announcement: packed,
}); });
if (moderator) {
this.moderationLogService.log(moderator, 'createGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}
} }
return { return {
@ -91,6 +114,63 @@ export class AnnouncementService {
}; };
} }
@bindThis
public async update(announcement: MiAnnouncement, values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<void> {
await this.announcementsRepository.update(announcement.id, {
updatedAt: new Date(),
title: values.title,
text: values.text,
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
imageUrl: values.imageUrl || null,
display: values.display,
icon: values.icon,
forExistingUsers: values.forExistingUsers,
needConfirmationToRead: values.needConfirmationToRead,
isActive: values.isActive,
});
const after = await this.announcementsRepository.findOneByOrFail({ id: announcement.id });
if (moderator) {
if (announcement.userId) {
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
this.moderationLogService.log(moderator, 'updateUserAnnouncement', {
announcementId: announcement.id,
before: announcement,
after: after,
userId: announcement.userId,
userUsername: user.username,
userHost: user.host,
});
} else {
this.moderationLogService.log(moderator, 'updateGlobalAnnouncement', {
announcementId: announcement.id,
before: announcement,
after: after,
});
}
}
}
@bindThis
public async delete(announcement: MiAnnouncement, moderator?: MiUser): Promise<void> {
await this.announcementsRepository.delete(announcement.id);
if (moderator) {
if (announcement.userId) {
this.moderationLogService.log(moderator, 'deleteUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
} else {
this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}
}
}
@bindThis @bindThis
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> { public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
try { try {

View file

@ -119,6 +119,12 @@ export class AntennaService implements OnApplicationShutdown {
return this.utilityService.getFullApAccount(username, host).toLowerCase(); return this.utilityService.getFullApAccount(username, host).toLowerCase();
}); });
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} else if (antenna.src === 'users_blacklist') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
});
if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} }
const keywords = antenna.keywords const keywords = antenna.keywords

View file

@ -52,6 +52,7 @@ import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js'; import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js'; import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js'; import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js'; import { VideoProcessingService } from './VideoProcessingService.js';
import { WebhookService } from './WebhookService.js'; import { WebhookService } from './WebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js'; import { ProxyAccountService } from './ProxyAccountService.js';
@ -179,6 +180,7 @@ const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisti
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService }; const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
@ -309,6 +311,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserListService, UserListService,
UserMutingService, UserMutingService,
UserSuspendService, UserSuspendService,
UserAuthService,
VideoProcessingService, VideoProcessingService,
WebhookService, WebhookService,
UtilityService, UtilityService,
@ -432,6 +435,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserSuspendService, $UserSuspendService,
$UserAuthService,
$VideoProcessingService, $VideoProcessingService,
$WebhookService, $WebhookService,
$UtilityService, $UtilityService,
@ -556,6 +560,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserListService, UserListService,
UserMutingService, UserMutingService,
UserSuspendService, UserSuspendService,
UserAuthService,
VideoProcessingService, VideoProcessingService,
WebhookService, WebhookService,
UtilityService, UtilityService,
@ -678,6 +683,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserSuspendService, $UserSuspendService,
$UserAuthService,
$VideoProcessingService, $VideoProcessingService,
$WebhookService, $WebhookService,
$UtilityService, $UtilityService,

View file

@ -12,12 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import type { EmojisRepository, MiRole } from '@/models/_.js'; import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { query } from '@/misc/prelude/url.js'; import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js'; import type { Serialized } from '@/server/api/stream/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@ -36,6 +37,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
@ -66,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: boolean; isSensitive: boolean;
localOnly: boolean; localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}): Promise<MiEmoji> { }, moderator?: MiUser): Promise<MiEmoji> {
const emoji = await this.emojisRepository.insert({ const emoji = await this.emojisRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
updatedAt: new Date(), updatedAt: new Date(),
@ -89,6 +91,13 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id), emoji: await this.emojiEntityService.packDetailed(emoji.id),
}); });
if (moderator) {
this.moderationLogService.log(moderator, 'addCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
} }
return emoji; return emoji;
@ -104,7 +113,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive?: boolean; isSensitive?: boolean;
localOnly?: boolean; localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}): Promise<void> { }, moderator?: MiUser): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
@ -125,11 +134,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.localEmojisCache.refresh(); this.localEmojisCache.refresh();
const updated = await this.emojiEntityService.packDetailed(emoji.id); const packed = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) { if (emoji.name === data.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', { this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated], emojis: [packed],
}); });
} else { } else {
this.globalEventService.publishBroadcastStream('emojiDeleted', { this.globalEventService.publishBroadcastStream('emojiDeleted', {
@ -137,7 +146,16 @@ export class CustomEmojiService implements OnApplicationShutdown {
}); });
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated, emoji: packed,
});
}
if (moderator) {
const updated = await this.emojisRepository.findOneByOrFail({ id: id });
this.moderationLogService.log(moderator, 'updateCustomEmoji', {
emojiId: emoji.id,
before: emoji,
after: updated,
}); });
} }
} }
@ -231,7 +249,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async delete(id: MiEmoji['id']) { public async delete(id: MiEmoji['id'], moderator?: MiUser) {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
await this.emojisRepository.delete(emoji.id); await this.emojisRepository.delete(emoji.id);
@ -241,16 +259,30 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiDeleted', { this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)], emojis: [await this.emojiEntityService.packDetailed(emoji)],
}); });
if (moderator) {
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
} }
@bindThis @bindThis
public async deleteBulk(ids: MiEmoji['id'][]) { public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) {
const emojis = await this.emojisRepository.findBy({ const emojis = await this.emojisRepository.findBy({
id: In(ids), id: In(ids),
}); });
for (const emoji of emojis) { for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id); await this.emojisRepository.delete(emoji.id);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
} }
this.localEmojisCache.refresh(); this.localEmojisCache.refresh();

View file

@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
type AddFileArgs = { type AddFileArgs = {
/** User who wish to add file */ /** User who wish to add file */
@ -86,6 +87,9 @@ type UploadFromUrlArgs = {
@Injectable() @Injectable()
export class DriveService { export class DriveService {
public static NoSuchFolderError = class extends Error {};
public static InvalidFileNameError = class extends Error {};
public static CannotUnmarkSensitiveError = class extends Error {};
private registerLogger: Logger; private registerLogger: Logger;
private downloaderLogger: Logger; private downloaderLogger: Logger;
private deleteLogger: Logger; private deleteLogger: Logger;
@ -119,6 +123,7 @@ export class DriveService {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private roleService: RoleService, private roleService: RoleService,
private moderationLogService: ModerationLogService,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
@ -648,7 +653,63 @@ export class DriveService {
} }
@bindThis @bindThis
public async deleteFile(file: MiDriveFile, isExpired = false) { public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
throw new DriveService.InvalidFileNameError();
}
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) {
throw new DriveService.CannotUnmarkSensitiveError();
}
if (values.folderId != null) {
const folder = await this.driveFoldersRepository.findOneBy({
id: values.folderId,
userId: file.userId!,
});
if (folder == null) {
throw new DriveService.NoSuchFolderError();
}
}
await this.driveFilesRepository.update(file.id, values);
const fileObj = await this.driveFileEntityService.pack(file.id, { self: true });
// Publish fileUpdated event
if (file.userId) {
this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj);
}
if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) {
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) {
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
if (values.isSensitive) {
this.moderationLogService.log(updater, 'markSensitiveDriveFile', {
fileId: file.id,
fileUserId: file.userId,
fileUserUsername: user?.username ?? null,
fileUserHost: user?.host ?? null,
});
} else {
this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', {
fileId: file.id,
fileUserId: file.userId,
fileUserUsername: user?.username ?? null,
fileUserHost: user?.host ?? null,
});
}
}
}
return fileObj;
}
@bindThis
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
if (file.storedInternal) { if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!); this.internalStorageService.del(file.accessKey!);
@ -671,11 +732,11 @@ export class DriveService {
} }
} }
this.deletePostProcess(file, isExpired); this.deletePostProcess(file, isExpired, deleter);
} }
@bindThis @bindThis
public async deleteFileSync(file: MiDriveFile, isExpired = false) { public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
if (file.storedInternal) { if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!); this.internalStorageService.del(file.accessKey!);
@ -702,11 +763,11 @@ export class DriveService {
await Promise.all(promises); await Promise.all(promises);
} }
this.deletePostProcess(file, isExpired); this.deletePostProcess(file, isExpired, deleter);
} }
@bindThis @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) { if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, { this.driveFilesRepository.update(file.id, {
@ -733,6 +794,20 @@ export class DriveService {
this.instanceChart.updateDrive(file, false); 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)) {
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
this.moderationLogService.log(deleter, 'deleteDriveFile', {
fileId: file.id,
fileUserId: file.userId,
fileUserUsername: user?.username ?? null,
fileUserHost: user?.host ?? null,
});
}
} }
@bindThis @bindThis

View file

@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
@Injectable() @Injectable()
export class ModerationLogService { export class ModerationLogService {
@ -21,13 +22,13 @@ export class ModerationLogService {
} }
@bindThis @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({ await this.moderationLogsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
userId: moderator.id, userId: moderator.id,
type: type, 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 { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable() @Injectable()
export class NoteDeleteService { export class NoteDeleteService {
@ -48,6 +49,7 @@ export class NoteDeleteService {
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService, private metaService: MetaService,
private searchService: SearchService, private searchService: SearchService,
private moderationLogService: ModerationLogService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
@ -58,7 +60,7 @@ export class NoteDeleteService {
* @param user 稿 * @param user 稿
* @param note 稿 * @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 deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note); const cascadingNotes = await this.findCascadingNotes(note);
@ -131,6 +133,17 @@ export class NoteDeleteService {
id: note.id, id: note.id,
userId: user.id, userId: user.id,
}); });
if (deleter && (note.userId !== deleter.id)) {
const user = await this.usersRepository.findOneByOrFail({ id: note.userId });
this.moderationLogService.log(deleter, 'deleteNote', {
noteId: note.id,
noteUserId: note.userId,
noteUserUsername: user.username,
noteUserHost: user.host,
note: note,
});
}
} }
@bindThis @bindThis

View file

@ -18,6 +18,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@ -98,6 +99,7 @@ export class RoleService implements OnApplicationShutdown {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private idService: IdService, private idService: IdService,
private moderationLogService: ModerationLogService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -374,9 +376,11 @@ export class RoleService implements OnApplicationShutdown {
} }
@bindThis @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 now = new Date();
const role = await this.rolesRepository.findOneByOrFail({ id: roleId });
const existing = await this.roleAssignmentsRepository.findOneBy({ const existing = await this.roleAssignmentsRepository.findOneBy({
roleId: roleId, roleId: roleId,
userId: userId, userId: userId,
@ -406,10 +410,22 @@ export class RoleService implements OnApplicationShutdown {
}); });
this.globalEventService.publishInternalEvent('userRoleAssigned', created); this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
userUsername: user.username,
userHost: user.host,
expiresAt: expiresAt ? expiresAt.toISOString() : null,
});
}
} }
@bindThis @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 now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
@ -430,6 +446,20 @@ export class RoleService implements OnApplicationShutdown {
}); });
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing); this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
if (moderator) {
const [user, role] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: userId }),
this.rolesRepository.findOneByOrFail({ id: roleId }),
]);
this.moderationLogService.log(moderator, 'unassignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
userUsername: user.username,
userHost: user.host,
});
}
} }
@bindThis @bindThis
@ -451,6 +481,75 @@ export class RoleService implements OnApplicationShutdown {
redisPipeline.exec(); redisPipeline.exec();
} }
@bindThis
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
const date = new Date();
const created = await this.rolesRepository.insert({
id: this.idService.genId(),
createdAt: date,
updatedAt: date,
lastUsedAt: date,
name: values.name,
description: values.description,
color: values.color,
iconUrl: values.iconUrl,
target: values.target,
condFormula: values.condFormula,
isPublic: values.isPublic,
isAdministrator: values.isAdministrator,
isModerator: values.isModerator,
isExplorable: values.isExplorable,
asBadge: values.asBadge,
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('roleCreated', created);
if (moderator) {
this.moderationLogService.log(moderator, 'createRole', {
roleId: created.id,
role: created,
});
}
return created;
}
@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 async delete(role: MiRole, moderator?: MiUser): Promise<void> {
await this.rolesRepository.delete({ id: role.id });
this.globalEventService.publishInternalEvent('roleDeleted', role);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteRole', {
roleId: role.id,
role: role,
});
}
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.redisForSub.off('message', this.onMessage);

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { QueryFailedError } from 'typeorm';
import * as OTPAuth from 'otpauth';
import { DI } from '@/di-symbols.js';
import type { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type { MiLocalUser } from '@/models/User.js';
@Injectable()
export class UserAuthService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
) {
}
@bindThis
public async twoFactorAuthenticate(profile: MiUserProfile, token: string): Promise<void> {
if (profile.twoFactorBackupSecret?.includes(token)) {
await this.userProfilesRepository.update({ userId: profile.userId }, {
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token),
});
} else {
const delta = OTPAuth.TOTP.validate({
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
digits: 6,
token,
window: 5,
});
if (delta === null) {
throw new Error('authentication failed');
}
}
}
}

View file

@ -4,9 +4,10 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/_.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
@ -32,6 +33,7 @@ export class Resolver {
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
private noteReactionsRepository: NoteReactionsRepository, private noteReactionsRepository: NoteReactionsRepository,
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private metaService: MetaService, private metaService: MetaService,
@ -146,13 +148,24 @@ export class Resolver {
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction => return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }))); this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
case 'follows': case 'follows':
// rest should be <followee id> return this.followRequestsRepository.findOneBy({ id: parsed.id })
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); .then(async followRequest => {
if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID');
return Promise.all( const [follower, followee] = await Promise.all([
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), this.usersRepository.findOneBy({
) id: followRequest.followerId,
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url))); host: IsNull(),
}),
this.usersRepository.findOneBy({
id: followRequest.followeeId,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
throw new Error('resolveLocal: follower or followee does not exist');
}
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
});
default: default:
throw new Error(`resolveLocal: type ${parsed.type} unhandled`); throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
} }
@ -177,6 +190,9 @@ export class ApResolverService {
@Inject(DI.noteReactionsRepository) @Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository, private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private metaService: MetaService, private metaService: MetaService,
@ -196,6 +212,7 @@ export class ApResolverService {
this.notesRepository, this.notesRepository,
this.pollsRepository, this.pollsRepository,
this.noteReactionsRepository, this.noteReactionsRepository,
this.followRequestsRepository,
this.utilityService, this.utilityService,
this.instanceActorService, this.instanceActorService,
this.metaService, this.metaService,

View file

@ -41,8 +41,8 @@ export class MiAntenna {
}) })
public name: string; public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list'] }) @Column('enum', { enum: ['home', 'all', 'users', 'list', 'users_blacklist'] })
public src: 'home' | 'all' | 'users' | 'list'; public src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
@Column({ @Column({
...id(), ...id(),

View file

@ -20,6 +20,11 @@ export class MiMeta {
}) })
public name: string | null; public name: string | null;
@Column('varchar', {
length: 64, nullable: true,
})
public shortName: string | null;
@Column('varchar', { @Column('varchar', {
length: 1024, nullable: true, length: 1024, nullable: true,
}) })

View file

@ -47,7 +47,7 @@ export const packedAntennaSchema = {
src: { src: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['home', 'all', 'users', 'list'], enum: ['home', 'all', 'users', 'list', 'users_blacklist'],
}, },
userListId: { userListId: {
type: 'string', type: 'string',

View file

@ -20,6 +20,7 @@ import type { MiLocalUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types'; import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
@ -43,6 +44,7 @@ export class SigninApiService {
private idService: IdService, private idService: IdService,
private rateLimiterService: RateLimiterService, private rateLimiterService: RateLimiterService,
private signinService: SigninService, private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService, private webAuthnService: WebAuthnService,
) { ) {
} }
@ -125,7 +127,7 @@ export class SigninApiService {
const same = await argon2.verify(profile.password!, password); const same = await argon2.verify(profile.password!, password);
const fail = async (status?: number, failure?: { id: string }) => { const fail = async (status?: number, failure?: { id: string }) => {
// Append signin history // Append signin history
await this.signinsRepository.insert({ await this.signinsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@ -155,27 +157,15 @@ export class SigninApiService {
}); });
} }
if (profile.twoFactorBackupSecret?.includes(token)) { try {
await this.userProfilesRepository.update({ userId: profile.userId }, { await this.userAuthService.twoFactorAuthenticate(profile, token);
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token), } catch (e) {
});
return this.signinService.signin(request, reply, user);
}
const delta = OTPAuth.TOTP.validate({
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
digits: 6,
token,
window: 1,
});
if (delta === null) {
return await fail(403, { return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f', id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
}); });
} else {
return this.signinService.signin(request, reply, user);
} }
return this.signinService.signin(request, reply, user);
} else if (body.credential) { } else if (body.credential) {
if (!same && !profile.usePasswordLessLogin) { if (!same && !profile.usePasswordLessLogin) {
return await fail(403, { return await fail(403, {
@ -204,6 +194,6 @@ export class SigninApiService {
reply.code(200); reply.code(200);
return authRequest; return authRequest;
} }
// never get here // never get here
} }
} }

View file

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

View file

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AnnouncementsRepository } from '@/models/_.js'; import type { AnnouncementsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -37,13 +38,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.announcementsRepository) @Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository, private announcementsRepository: AnnouncementsRepository,
private announcementService: AnnouncementService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); const announcement = await this.announcementsRepository.findOneBy({ id: ps.id });
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await this.announcementsRepository.delete(announcement.id); await this.announcementService.delete(announcement, me);
}); });
} }
} }

View file

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AnnouncementsRepository } from '@/models/_.js'; import type { AnnouncementsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -45,13 +46,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.announcementsRepository) @Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository, private announcementsRepository: AnnouncementsRepository,
private announcementService: AnnouncementService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); const announcement = await this.announcementsRepository.findOneBy({ id: ps.id });
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await this.announcementsRepository.update(announcement.id, { await this.announcementService.update(announcement, {
updatedAt: new Date(), updatedAt: new Date(),
title: ps.title, title: ps.title,
text: ps.text, text: ps.text,
@ -62,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
forExistingUsers: ps.forExistingUsers, forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead, needConfirmationToRead: ps.needConfirmationToRead,
isActive: ps.isActive, isActive: ps.isActive,
}); }, me);
}); });
} }
} }

View file

@ -8,7 +8,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
@ -61,7 +60,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
@ -77,11 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false, isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false, localOnly: ps.localOnly ?? false,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
}); }, me);
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id,
});
return this.emojiEntityService.packDetailed(emoji); return this.emojiEntityService.packDetailed(emoji);
}); });

View file

@ -30,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.customEmojiService.deleteBulk(ps.ids); await this.customEmojiService.deleteBulk(ps.ids, me);
}); });
} }
} }

View file

@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.customEmojiService.delete(ps.id); await this.customEmojiService.delete(ps.id, me);
}); });
} }
} }

View file

@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive, isSensitive: ps.isSensitive,
localOnly: ps.localOnly, localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}); }, me);
}); });
} }
} }

View file

@ -9,6 +9,7 @@ import type { InstancesRepository } from '@/models/_.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -34,6 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService, private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) }); 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'); throw new Error('instance not found');
} }
this.federatedInstanceService.update(instance.id, { await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended, 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

@ -321,6 +321,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
maintainerEmail: instance.maintainerEmail, maintainerEmail: instance.maintainerEmail,
version: this.config.version, version: this.config.version,
name: instance.name, name: instance.name,
shortName: instance.shortName,
uri: this.config.url, uri: this.config.url,
description: instance.description, description: instance.description,
langs: instance.langs, langs: instance.langs,

View file

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

View file

@ -10,6 +10,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -47,8 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: 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 }); const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) { if (user == null) {
@ -70,6 +73,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
password: hash, password: hash,
}); });
this.moderationLogService.log(me, 'resetPassword', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
return { return {
password: passwd, password: passwd,
}; };

View file

@ -10,6 +10,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -41,6 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queueService: QueueService, private queueService: QueueService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
@ -61,6 +63,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
assigneeId: me.id, assigneeId: me.id,
forwarded: ps.forward && report.targetUserHost != null, forwarded: ps.forward && report.targetUserHost != null,
}); });
this.moderationLogService.log(me, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost != null,
});
}); });
} }
} }

View file

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

@ -5,11 +5,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['admin', 'role'], tags: ['admin', 'role'],
@ -58,37 +55,11 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,
private globalEventService: GlobalEventService,
private idService: IdService,
private roleEntityService: RoleEntityService, private roleEntityService: RoleEntityService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const date = new Date(); const created = await this.roleService.create(ps, me);
const created = await this.rolesRepository.insert({
id: this.idService.genId(),
createdAt: date,
updatedAt: date,
lastUsedAt: date,
name: ps.name,
description: ps.description,
color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target,
condFormula: ps.condFormula,
isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator,
isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('roleCreated', created);
return await this.roleEntityService.pack(created, me); return await this.roleEntityService.pack(created, me);
}); });

View file

@ -6,9 +6,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js'; import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['admin', 'role'], tags: ['admin', 'role'],
@ -41,17 +41,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.rolesRepository) @Inject(DI.rolesRepository)
private rolesRepository: RolesRepository, private rolesRepository: RolesRepository,
private globalEventService: GlobalEventService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps) => { super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (role == null) { if (role == null) {
throw new ApiError(meta.errors.noSuchRole); throw new ApiError(meta.errors.noSuchRole);
} }
await this.rolesRepository.delete({ await this.roleService.delete(role, me);
id: ps.roleId,
});
this.globalEventService.publishInternalEvent('roleDeleted', role);
}); });
} }
} }

View file

@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchUser); 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 { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['admin', 'role'], tags: ['admin', 'role'],
@ -70,17 +71,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.rolesRepository) @Inject(DI.rolesRepository)
private rolesRepository: RolesRepository, private rolesRepository: RolesRepository,
private globalEventService: GlobalEventService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps) => { super(meta, paramDef, async (ps, me) => {
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } }); const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (!roleExist) { if (role == null) {
throw new ApiError(meta.errors.noSuchRole); throw new ApiError(meta.errors.noSuchRole);
} }
const date = new Date(); await this.roleService.update(role, {
await this.rolesRepository.update(ps.roleId, {
updatedAt: date,
name: ps.name, name: ps.name,
description: ps.description, description: ps.description,
color: ps.color, color: ps.color,
@ -95,9 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder, displayOrder: ps.displayOrder,
policies: ps.policies, policies: ps.policies,
}); }, me);
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
this.globalEventService.publishInternalEvent('roleUpdated', updated);
}); });
} }
} }

View file

@ -62,6 +62,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { 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: [], required: [],
} as const; } as const;
@ -78,6 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); 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(); const reports = await query.limit(ps.limit).getMany();
return await this.moderationLogEntityService.packMany(reports); return await this.moderationLogEntityService.packMany(reports);

View file

@ -60,8 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSuspended: true, isSuspended: true,
}); });
this.moderationLogService.insertModerationLog(me, 'suspend', { this.moderationLogService.log(me, 'suspend', {
targetId: user.id, userId: user.id,
userUsername: user.username,
userHost: user.host,
}); });
(async () => { (async () => {

View file

@ -45,8 +45,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSuspended: false, isSuspended: false,
}); });
this.moderationLogService.insertModerationLog(me, 'unsuspend', { this.moderationLogService.log(me, 'unsuspend', {
targetId: user.id, userId: user.id,
userUsername: user.username,
userHost: user.host,
}); });
this.userSuspendService.doPostUnsuspend(user); this.userSuspendService.doPostUnsuspend(user);

View file

@ -44,6 +44,7 @@ export const paramDef = {
backgroundImageUrl: { type: 'string', nullable: true }, backgroundImageUrl: { type: 'string', nullable: true },
logoImageUrl: { type: 'string', nullable: true }, logoImageUrl: { type: 'string', nullable: true },
name: { type: 'string', nullable: true }, name: { type: 'string', nullable: true },
shortName: { type: 'string', nullable: true },
description: { type: 'string', nullable: true }, description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true },
@ -188,6 +189,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.name = ps.name; set.name = ps.name;
} }
if (ps.shortName !== undefined) {
set.shortName = ps.shortName;
}
if (ps.description !== undefined) { if (ps.description !== undefined) {
set.description = ps.description; set.description = ps.description;
} }
@ -436,8 +441,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.manifestJsonOverride = ps.manifestJsonOverride; set.manifestJsonOverride = ps.manifestJsonOverride;
} }
const before = await this.metaService.fetch(true);
await this.metaService.update(set); 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 type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -32,6 +33,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId }); const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -40,9 +43,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found'); throw new Error('user not found');
} }
const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update({ userId: user.id }, { await this.userProfilesRepository.update({ userId: user.id }, {
moderationNote: ps.text, moderationNote: ps.text,
}); });
this.moderationLogService.log(me, 'updateUserNote', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
before: currentProfile.moderationNote,
after: ps.text,
});
}); });
} }
} }

View file

@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
.where('announcement.isActive = :isActive', { isActive: ps.isActive }) .andWhere('announcement.isActive = :isActive', { isActive: ps.isActive })
.andWhere(new Brackets(qb => { .andWhere(new Brackets(qb => {
if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id }); if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id });
qb.orWhere('announcement.userId IS NULL'); qb.orWhere('announcement.userId IS NULL');

View file

@ -47,7 +47,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true }, userListId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: { keywords: { type: 'array', items: {
type: 'array', items: { type: 'array', items: {

View file

@ -46,7 +46,7 @@ export const paramDef = {
properties: { properties: {
antennaId: { type: 'string', format: 'misskey:id' }, antennaId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true }, userListId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: { keywords: { type: 'array', items: {
type: 'array', items: { type: 'array', items: {

View file

@ -65,11 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied); throw new ApiError(meta.errors.accessDenied);
} }
// Delete await this.driveService.deleteFile(file, false, me);
await this.driveService.deleteFile(file);
// Publish fileDeleted event
this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id);
}); });
} }
} }

View file

@ -4,12 +4,11 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js'; import type { DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -77,16 +76,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
@Inject(DI.driveFoldersRepository) private driveService: DriveService,
private driveFoldersRepository: DriveFoldersRepository,
private driveFileEntityService: DriveFileEntityService,
private roleService: RoleService, private roleService: RoleService,
private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(me.id)).alwaysMarkNsfw;
if (file == null) { if (file == null) {
throw new ApiError(meta.errors.noSuchFile); throw new ApiError(meta.errors.noSuchFile);
} }
@ -95,49 +89,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied); throw new ApiError(meta.errors.accessDenied);
} }
if (ps.name) file.name = ps.name; let packedFile;
if (!this.driveFileEntityService.validateFileName(file.name)) {
throw new ApiError(meta.errors.invalidFileName);
}
if (ps.comment !== undefined) file.comment = ps.comment; try {
packedFile = await this.driveService.updateFile(file, {
if (ps.isSensitive !== undefined && ps.isSensitive !== file.isSensitive && alwaysMarkNsfw && !ps.isSensitive) { folderId: ps.folderId,
throw new ApiError(meta.errors.restrictedByRole); name: ps.name,
} isSensitive: ps.isSensitive,
comment: ps.comment,
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; }, me);
} catch (e) {
if (ps.folderId !== undefined) { if (e instanceof DriveService.InvalidFileNameError) {
if (ps.folderId === null) { throw new ApiError(meta.errors.invalidFileName);
file.folderId = null; } else if (e instanceof DriveService.NoSuchFolderError) {
throw new ApiError(meta.errors.noSuchFolder);
} else if (e instanceof DriveService.CannotUnmarkSensitiveError) {
throw new ApiError(meta.errors.restrictedByRole);
} else { } else {
const folder = await this.driveFoldersRepository.findOneBy({ throw e;
id: ps.folderId,
userId: me.id,
});
if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
file.folderId = folder.id;
} }
} }
await this.driveFilesRepository.update(file.id, { return packedFile;
name: file.name,
comment: file.comment,
folderId: file.folderId,
isSensitive: file.isSensitive,
});
const fileObj = await this.driveFileEntityService.pack(file, { self: true });
// Publish fileUpdated event
this.globalEventService.publishDriveStream(me.id, 'fileUpdated', fileObj);
return fileObj;
}); });
} }
} }

View file

@ -75,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
summary: ps.summary, summary: ps.summary,
script: ps.script, script: ps.script,
permissions: ps.permissions, permissions: ps.permissions,
visibility: ps.visibility,
}); });
}); });
} }

View file

@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret), secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
digits: 6, digits: 6,
token, token,
window: 1, window: 5,
}); });
if (delta === null) { if (delta === null) {

View file

@ -13,6 +13,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { WebAuthnService } from '@/core/WebAuthnService.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -38,6 +39,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
token: { type: 'string', nullable: true },
name: { type: 'string', minLength: 1, maxLength: 30 }, name: { type: 'string', minLength: 1, maxLength: 30 },
credential: { type: 'object' }, credential: { type: 'object' },
}, },
@ -55,16 +57,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userSecurityKeysRepository: UserSecurityKeysRepository, private userSecurityKeysRepository: UserSecurityKeysRepository,
private webAuthnService: WebAuthnService, private webAuthnService: WebAuthnService,
private userAuthService: UserAuthService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password if (profile.twoFactorEnabled) {
const same = await argon2.verify(profile.password ?? '', ps.password); if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);;
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);
} }

View file

@ -11,6 +11,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -42,6 +43,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
token: { type: 'string', nullable: true },
}, },
required: ['password'], required: ['password'],
} as const; } as const;
@ -54,8 +56,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private webAuthnService: WebAuthnService, private webAuthnService: WebAuthnService,
private userAuthService: UserAuthService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOne({ const profile = await this.userProfilesRepository.findOne({
where: { where: {
userId: me.id, userId: me.id,
@ -68,9 +72,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
// Compare password // Compare password
const same = await argon2.verify(profile.password ?? '', ps.password); if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);;
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);
} }

View file

@ -13,6 +13,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -32,6 +33,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
token: { type: 'string', nullable: true },
}, },
required: ['password'], required: ['password'],
} as const; } as const;
@ -44,14 +46,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private userAuthService: UserAuthService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password if (profile.twoFactorEnabled) {
const same = await argon2.verify(profile.password ?? '', ps.password); if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);
} }

View file

@ -12,6 +12,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -31,6 +32,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
token: { type: 'string', nullable: true },
credentialId: { type: 'string' }, credentialId: { type: 'string' },
}, },
required: ['password', 'credentialId'], required: ['password', 'credentialId'],
@ -46,15 +48,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private userAuthService: UserAuthService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password // Compare password
const same = await argon2.verify(profile.password ?? '', ps.password); if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);
} }

View file

@ -12,6 +12,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -31,6 +32,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
token: { type: 'string', nullable: true },
}, },
required: ['password'], required: ['password'],
} as const; } as const;
@ -42,15 +44,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private userAuthService: UserAuthService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password if (profile.twoFactorEnabled) {
const same = await argon2.verify(profile.password ?? '', ps.password); if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);
} }

View file

@ -9,6 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js'; import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -21,6 +22,7 @@ export const paramDef = {
properties: { properties: {
currentPassword: { type: 'string' }, currentPassword: { type: 'string' },
newPassword: { type: 'string', minLength: 1 }, newPassword: { type: 'string', minLength: 1 },
token: { type: 'string', nullable: true },
}, },
required: ['currentPassword', 'newPassword'], required: ['currentPassword', 'newPassword'],
} as const; } as const;
@ -30,14 +32,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private userAuthService: UserAuthService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password if (profile.twoFactorEnabled) {
const same = await argon2.verify(profile.password!, ps.currentPassword); if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password!, ps.currentPassword);
if (!passwordMatched) {
throw new Error('incorrect password'); throw new Error('incorrect password');
} }

View file

@ -10,6 +10,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -21,6 +22,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
token: { type: 'string', nullable: true },
}, },
required: ['password'], required: ['password'],
} as const; } as const;
@ -34,19 +36,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private userAuthService: UserAuthService,
private deleteAccountService: DeleteAccountService, private deleteAccountService: DeleteAccountService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
if (userDetailed.isDeleted) { if (userDetailed.isDeleted) {
return; return;
} }
// Compare password const passwordMatched = await argon2.verify(profile.password!, ps.password);
const same = await argon2.verify(profile.password!, ps.password); if (!passwordMatched) {
if (!same) {
throw new Error('incorrect password'); throw new Error('incorrect password');
} }

View file

@ -15,6 +15,7 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -47,6 +48,7 @@ export const paramDef = {
properties: { properties: {
password: { type: 'string' }, password: { type: 'string' },
email: { type: 'string', nullable: true }, email: { type: 'string', nullable: true },
token: { type: 'string', nullable: true },
}, },
required: ['password'], required: ['password'],
} as const; } as const;
@ -62,15 +64,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private emailService: EmailService, private emailService: EmailService,
private userAuthService: UserAuthService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password if (profile.twoFactorEnabled) {
const same = await argon2.verify(profile.password!, ps.password); if (token == null) {
throw new Error('authentication failed');
}
if (!same) { try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password!, ps.password);;
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);
} }

Some files were not shown because too many files have changed in this diff Show more