Compare commits

..

17 commits

Author SHA1 Message Date
313313fc2e
WIP: type-safe sanitization of AP objects
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-21 00:47:39 -06:00
7a7aef71cd
do not use media proxy if emoji is local
Some checks failed
Lint / pnpm_install (push) Successful in 1m33s
Publish Docker image / Build (push) Successful in 3m47s
Test (production install and build) / production (22.11.0) (push) Successful in 58s
Lint / lint (backend) (push) Successful in 1m51s
Test (backend) / unit (22.11.0) (push) Successful in 6m52s
Lint / lint (frontend) (push) Successful in 1m55s
Lint / lint (frontend-embed) (push) Successful in 1m54s
Lint / lint (frontend-shared) (push) Successful in 2m0s
Lint / lint (misskey-bubble-game) (push) Successful in 2m2s
Test (backend) / e2e (22.11.0) (push) Failing after 10m4s
Lint / lint (misskey-js) (push) Successful in 2m7s
Lint / lint (misskey-reversi) (push) Successful in 2m8s
Lint / lint (sw) (push) Successful in 1m59s
Lint / typecheck (misskey-js) (push) Successful in 1m17s
Lint / typecheck (backend) (push) Successful in 2m11s
Lint / typecheck (sw) (push) Successful in 1m19s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-20 13:23:41 -06:00
95d3fb08f4
Prefix all calls to Image and Video Processing service
Some checks failed
Test (production install and build) / production (22.11.0) (pull_request) Successful in 1m3s
Test (backend) / unit (22.11.0) (pull_request) Successful in 8m32s
Publish Docker image / Build (push) Successful in 4m37s
Lint / pnpm_install (push) Successful in 1m39s
Test (backend) / e2e (22.11.0) (pull_request) Failing after 11m28s
Test (production install and build) / production (22.11.0) (push) Successful in 1m1s
Lint / lint (backend) (pull_request) Successful in 2m8s
Test (backend) / unit (22.11.0) (push) Successful in 7m57s
Lint / lint (frontend) (pull_request) Successful in 2m13s
Lint / lint (frontend-embed) (pull_request) Successful in 2m18s
Lint / lint (frontend-shared) (pull_request) Successful in 2m19s
Test (backend) / e2e (22.11.0) (push) Failing after 11m35s
Lint / lint (misskey-bubble-game) (pull_request) Successful in 2m35s
Lint / lint (misskey-js) (pull_request) Successful in 2m20s
Lint / lint (misskey-reversi) (pull_request) Successful in 2m34s
Lint / lint (sw) (pull_request) Successful in 2m36s
Lint / typecheck (backend) (pull_request) Successful in 2m19s
Lint / typecheck (misskey-js) (pull_request) Successful in 1m38s
Lint / typecheck (sw) (pull_request) Successful in 1m50s
Lint / lint (backend) (push) Successful in 2m40s
Lint / lint (frontend) (push) Successful in 2m38s
Lint / lint (frontend-embed) (push) Successful in 2m30s
Lint / lint (frontend-shared) (push) Successful in 2m26s
Lint / lint (misskey-bubble-game) (push) Successful in 2m36s
Lint / lint (misskey-js) (push) Successful in 2m36s
Lint / lint (misskey-reversi) (push) Successful in 2m26s
Lint / lint (sw) (push) Successful in 2m27s
Lint / typecheck (misskey-js) (push) Failing after 36s
Lint / typecheck (backend) (push) Successful in 2m22s
Lint / typecheck (sw) (push) Successful in 1m28s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-20 02:07:13 -06:00
4ba0357d49
fix(backend): Atomically mark remote account deletions
Some checks failed
Lint / pnpm_install (push) Successful in 1m59s
Publish Docker image / Build (push) Successful in 4m56s
Test (production install and build) / production (22.11.0) (push) Successful in 1m2s
Lint / lint (backend) (push) Successful in 2m12s
Test (backend) / unit (22.11.0) (push) Successful in 8m22s
Lint / lint (frontend-embed) (push) Failing after 1m40s
Lint / lint (frontend) (push) Successful in 2m43s
Lint / lint (frontend-shared) (push) Successful in 2m20s
Lint / lint (misskey-bubble-game) (push) Successful in 2m17s
Test (backend) / e2e (22.11.0) (push) Failing after 11m44s
Lint / lint (misskey-js) (push) Successful in 2m28s
Lint / lint (misskey-reversi) (push) Successful in 2m25s
Lint / lint (sw) (push) Successful in 2m26s
Lint / typecheck (misskey-js) (push) Successful in 1m39s
Lint / typecheck (backend) (push) Successful in 2m37s
Lint / typecheck (sw) (push) Successful in 1m45s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 23:44:20 -06:00
416d71002a
improve emoji packing
Some checks failed
Test (production install and build) / production (22.11.0) (pull_request) Successful in 1m10s
Publish Docker image / Build (pull_request) Successful in 5m10s
Test (backend) / e2e (22.11.0) (pull_request) Successful in 12m39s
Lint / lint (backend) (pull_request) Successful in 2m52s
Lint / lint (frontend-embed) (pull_request) Successful in 2m56s
Lint / lint (frontend) (pull_request) Successful in 3m9s
Lint / lint (frontend-shared) (pull_request) Successful in 2m51s
Lint / lint (misskey-bubble-game) (pull_request) Successful in 2m51s
Lint / lint (misskey-js) (pull_request) Successful in 2m49s
Lint / lint (misskey-reversi) (pull_request) Successful in 3m0s
Lint / typecheck (backend) (pull_request) Successful in 2m48s
Lint / lint (sw) (pull_request) Successful in 2m50s
Lint / typecheck (misskey-js) (pull_request) Successful in 2m12s
Lint / typecheck (sw) (pull_request) Successful in 2m4s
Lint / pnpm_install (push) Successful in 1m56s
Publish Docker image / Build (push) Successful in 4m28s
Test (production install and build) / production (22.11.0) (push) Successful in 1m2s
Lint / lint (backend) (push) Successful in 2m8s
Test (backend) / unit (22.11.0) (push) Successful in 8m28s
Lint / lint (frontend) (push) Successful in 2m42s
Lint / lint (frontend-embed) (push) Successful in 2m19s
Lint / lint (frontend-shared) (push) Successful in 2m21s
Lint / lint (misskey-bubble-game) (push) Successful in 2m20s
Test (backend) / e2e (22.11.0) (push) Failing after 11m37s
Lint / lint (misskey-js) (push) Successful in 2m30s
Lint / lint (misskey-reversi) (push) Successful in 2m31s
Lint / lint (sw) (push) Successful in 2m28s
Lint / typecheck (misskey-js) (push) Successful in 1m37s
Lint / typecheck (backend) (push) Successful in 2m26s
Lint / typecheck (sw) (push) Successful in 1m52s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 10:57:45 -06:00
a236bbb8d4
CSP: allow blob images for cropping avatars
Some checks failed
Test (backend) / unit (22.11.0) (pull_request) Failing after 1m12s
Test (production install and build) / production (22.11.0) (pull_request) Successful in 1m9s
Lint / pnpm_install (push) Successful in 1m31s
Publish Docker image / Build (push) Successful in 4m56s
Test (backend) / e2e (22.11.0) (pull_request) Successful in 12m9s
Test (production install and build) / production (22.11.0) (push) Successful in 1m7s
Test (backend) / unit (22.11.0) (push) Successful in 8m3s
Test (backend) / e2e (22.11.0) (push) Successful in 12m2s
Lint / lint (frontend) (pull_request) Successful in 2m50s
Lint / lint (backend) (pull_request) Successful in 2m51s
Lint / lint (frontend-embed) (pull_request) Successful in 2m47s
Lint / lint (frontend-shared) (pull_request) Successful in 2m45s
Lint / lint (misskey-bubble-game) (pull_request) Successful in 2m44s
Lint / lint (misskey-js) (pull_request) Successful in 2m52s
Lint / lint (sw) (pull_request) Successful in 2m45s
Lint / lint (misskey-reversi) (pull_request) Successful in 2m47s
Lint / typecheck (backend) (pull_request) Successful in 2m41s
Lint / typecheck (misskey-js) (pull_request) Successful in 1m45s
Lint / typecheck (sw) (pull_request) Successful in 1m56s
Lint / lint (backend) (push) Successful in 2m43s
Lint / lint (frontend-embed) (push) Successful in 2m41s
Lint / lint (frontend) (push) Successful in 2m53s
Lint / lint (frontend-shared) (push) Successful in 2m46s
Lint / lint (misskey-bubble-game) (push) Successful in 2m44s
Lint / lint (misskey-js) (push) Successful in 2m42s
Lint / lint (misskey-reversi) (push) Successful in 2m33s
Lint / typecheck (backend) (push) Successful in 2m19s
Lint / lint (sw) (push) Successful in 2m44s
Lint / typecheck (misskey-js) (push) Successful in 1m39s
Lint / typecheck (sw) (push) Failing after 1m23s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 06:17:23 -06:00
3d3bfad5d0
fixup! more path sanitization
All checks were successful
Lint / pnpm_install (push) Successful in 1m58s
Publish Docker image / Build (push) Successful in 5m21s
Test (production install and build) / production (22.11.0) (push) Successful in 1m5s
Lint / lint (backend) (push) Successful in 2m14s
Test (backend) / unit (22.11.0) (push) Successful in 9m27s
Lint / lint (frontend) (push) Successful in 2m37s
Lint / lint (frontend-embed) (push) Successful in 2m18s
Lint / lint (frontend-shared) (push) Successful in 2m23s
Lint / lint (misskey-bubble-game) (push) Successful in 2m20s
Test (backend) / e2e (22.11.0) (push) Successful in 12m58s
Lint / lint (misskey-js) (push) Successful in 2m34s
Lint / lint (misskey-reversi) (push) Successful in 2m35s
Lint / lint (sw) (push) Successful in 2m19s
Lint / typecheck (misskey-js) (push) Successful in 1m37s
Lint / typecheck (backend) (push) Successful in 2m29s
Lint / typecheck (sw) (push) Successful in 1m38s
2024-11-19 03:15:48 -06:00
8a26ab9fb5
2024.11.0-yumechinokuni.5
All checks were successful
Test (production install and build) / production (22.11.0) (pull_request) Successful in 1m5s
Test (backend) / unit (22.11.0) (pull_request) Successful in 9m8s
Publish Docker image / Build (push) Successful in 4m37s
Lint / pnpm_install (push) Successful in 1m25s
Test (backend) / e2e (22.11.0) (pull_request) Successful in 11m50s
Test (production install and build) / production (22.11.0) (push) Successful in 1m1s
Lint / lint (backend) (pull_request) Successful in 1m56s
Test (backend) / unit (22.11.0) (push) Successful in 8m17s
Lint / lint (frontend) (pull_request) Successful in 2m13s
Lint / lint (frontend-embed) (pull_request) Successful in 2m30s
Lint / lint (frontend-shared) (pull_request) Successful in 2m19s
Test (backend) / e2e (22.11.0) (push) Successful in 12m12s
Lint / lint (misskey-bubble-game) (pull_request) Successful in 2m39s
Lint / lint (misskey-js) (pull_request) Successful in 2m37s
Lint / lint (misskey-reversi) (pull_request) Successful in 2m25s
Lint / typecheck (backend) (pull_request) Successful in 2m20s
Lint / lint (sw) (pull_request) Successful in 2m37s
Lint / typecheck (misskey-js) (pull_request) Successful in 1m34s
Lint / typecheck (sw) (pull_request) Successful in 1m51s
Lint / lint (backend) (push) Successful in 2m26s
Lint / lint (frontend) (push) Successful in 2m37s
Lint / lint (frontend-embed) (push) Successful in 2m15s
Lint / lint (frontend-shared) (push) Successful in 2m26s
Lint / lint (misskey-bubble-game) (push) Successful in 2m16s
Lint / lint (misskey-js) (push) Successful in 2m22s
Lint / lint (misskey-reversi) (push) Successful in 2m21s
Lint / lint (sw) (push) Successful in 2m22s
Lint / typecheck (backend) (push) Successful in 2m20s
Lint / typecheck (misskey-js) (push) Successful in 1m33s
Lint / typecheck (sw) (push) Successful in 1m34s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 01:41:59 -06:00
fe3fd951ed
Merge tag '2024.11.0-alpha.2' into develop
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 01:37:57 -06:00
5673f0fff6 Merge pull request 'Sync upstream/develop' (#29) from incoming into develop
Some checks failed
Lint / lint (frontend-embed) (push) Blocked by required conditions
Lint / lint (frontend-shared) (push) Blocked by required conditions
Lint / lint (misskey-bubble-game) (push) Blocked by required conditions
Lint / lint (misskey-js) (push) Blocked by required conditions
Lint / lint (misskey-reversi) (push) Blocked by required conditions
Lint / lint (sw) (push) Blocked by required conditions
Lint / typecheck (backend) (push) Blocked by required conditions
Lint / typecheck (misskey-js) (push) Blocked by required conditions
Lint / typecheck (sw) (push) Blocked by required conditions
Lint / pnpm_install (push) Successful in 2m9s
Publish Docker image / Build (push) Successful in 4m35s
Test (production install and build) / production (22.11.0) (push) Successful in 1m7s
Lint / lint (backend) (push) Successful in 2m15s
Lint / lint (frontend) (push) Has been cancelled
Test (backend) / e2e (22.11.0) (push) Has been cancelled
Test (backend) / unit (22.11.0) (push) Has been cancelled
Reviewed-on: #29
2024-11-19 01:34:05 -06:00
ccf81b9398
filter out duplicate events
All checks were successful
Test (production install and build) / production (22.11.0) (push) Successful in 1m27s
Publish Docker image / Build (push) Successful in 4m45s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-19 01:18:31 -06:00
github-actions[bot]
6c5d3113c6 Bump version to 2024.11.0-alpha.2
All checks were successful
API report (misskey.js) / report (push) Successful in 1m30s
2024-11-19 03:56:50 +00:00
syuilo
968f595606
New Crowdin updates (#14965)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)
2024-11-19 12:50:04 +09:00
おさむのひと
7b9c884a5d
refactor(backend): SystemWebhookで送信されるペイロードの型を追加 (#14980) 2024-11-19 10:41:39 +09:00
FineArchs
c271534aba
リノートメニューに「リノートの詳細」を追加 (#14985)
* add renote-detail menu

* changelog

* Apply suggestions from code review

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* Update CHANGELOG.md

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-11-19 10:34:33 +09:00
饺子w (Yumechi)
e800c0f85a
fix(backend): お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 (#14990)
* fix(backend): アナウンスメントを作成ときに画像URLを後悔できないのを修正

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* Update CHANGELOG.md

Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>
2024-11-19 10:29:42 +09:00
かっこかり
81348f1277
fix(frontend): TypeScriptの型チェック対象ファイルを限定して高速化するように (#14994)
* fix frontend tsconfig includes

* fix frontend-embed tsconfig includes

* fix eslint in frontend / frontend-embed

* Update Changelog

---------

Co-authored-by: Hazelnoot <acomputerdog@gmail.com>
2024-11-19 10:22:47 +09:00
71 changed files with 1818 additions and 790 deletions

View file

@ -1,3 +1,11 @@
## 2024.11.0-yumechinokuni.5
- Upstream: 2024.11.0-alpha.2 タッグをマージする
- Reliability: Activitypub event deduplication
- DevOps: Prometheus サーバーメトリクス
- Enhance: ハッシュタグランギングを改善
- Enhance: PgroongaのCWサーチ + パフォーマンス改善
## 2024.11.0-yumechinokuni.4
- Upstream: 2024.11.0-alpha.1 タッグをマージする
@ -28,6 +36,7 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
### General
- Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 )
- Enhance: 依存関係の更新
- Enhance: l10nの更新
@ -48,6 +57,7 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
- Enhance: 過去に送信したフォローリクエストを確認できるように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
- Enhance: リノートメニューに「リノートの詳細」を追加
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
@ -59,6 +69,8 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/815)
- Fix: TypeScriptの型チェック対象ファイルを限定してビルドを高速化するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/725)
### Server
- Enhance: DockerのNode.jsを22.11.0に更新

View file

@ -343,7 +343,6 @@ enableLocalTimeline: "تفعيل الخيط المحلي"
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية حتى وإن لم تفعّل."
registration: "إنشاء حساب"
enableRegistration: "تفعيل إنشاء الحسابات الجديدة"
invite: "دعوة"
driveCapacityPerLocalAccount: "حصة التخزين لكل مستخدم محلي"
driveCapacityPerRemoteAccount: "حصة التخزين لكل مستخدم بعيد"

View file

@ -339,7 +339,6 @@ enableLocalTimeline: "স্থানীয় টাইমলাইন চাল
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে"
registration: "নিবন্ধন"
enableRegistration: "নতুন ব্যাবহারকারী নিবন্ধন চালু করুন"
invite: "আমন্ত্রণ"
driveCapacityPerLocalAccount: "প্রত্যেক স্থানীয় ব্যাবহারকারীর জন্য ড্রাইভের জায়গা"
driveCapacityPerRemoteAccount: "প্রত্যেক রিমোট ব্যাবহারকারীর জন্য ড্রাইভের জায়গা"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "Activa la línia de temps local"
enableGlobalTimeline: "Activa la línia de temps global"
disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència."
registration: "Registre"
enableRegistration: "Permet el registre de nous usuaris"
invite: "Convida"
driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals"
driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots"
@ -1300,6 +1299,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de ses
lockdown: "Bloquejat"
pleaseSelectAccount: "Seleccionar un compte"
availableRoles: "Roles disponibles "
acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills."
_accountSettings:
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."
@ -1456,6 +1456,8 @@ _serverSettings:
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
openRegistration: "Registres oberts"
openRegistrationWarning: "Obrir els registres és arriscat. Es recomana obrir-los només si el servidor és monitorat constantment i per respondre immediatament davant qualsevol problema."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa."
_accountMigration:
moveFrom: "Migrar un altre compte a aquest"
@ -2738,3 +2740,6 @@ _selfXssPrevention:
description1: "Si posa alguna cosa al seu compte, un usuari malintencionat podria segrestar-la o robar-li les dades."
description2: "Si no entens que estàs fent %cpara ara mateix i tanca la finestra."
description3: "Per obtenir més informació. {link}"
_followRequest:
recieved: "Sol·licituds rebudes"
sent: "Sol·licituds enviades"

View file

@ -348,7 +348,6 @@ enableLocalTimeline: "Povolit lokální čas"
enableGlobalTimeline: "Povolit globální čas"
disablingTimelinesInfo: "Administrátoři a Moderátoři budou mít stálý přístup ke všem časovým osám i přes to že nejsou zapnuté."
registration: "Registrace"
enableRegistration: "Povolit registraci novým uživatelům"
invite: "Pozvat"
driveCapacityPerLocalAccount: "Kapacita disku na lokálního uživatele"
driveCapacityPerRemoteAccount: "Kapacita disku na vzdáleného uživatele"

View file

@ -10,6 +10,7 @@ username: "Benutzername"
password: "Passwort"
initialPasswordForSetup: "Initiales Passwort für die Einrichtung"
initialPasswordIsIncorrect: "Das initiale Passwort für die Einrichtung ist falsch"
initialPasswordForSetupDescription: "Verwende das in der Konfigurationsdatei angegebene Passwort, wenn du Misskey selbst installiert hast.\nWenn du einen Misskey-Hostingdienst o.ä. nutzt, verwende das dort angegebene Kennwort.\nWenn du kein Passwort festgelegt hast, lasse es leer, um fortzufahren."
forgotPassword: "Passwort vergessen"
fetchingAsApObject: "Wird aus dem Fediverse angefragt …"
ok: "OK"
@ -111,11 +112,14 @@ enterEmoji: "Gib ein Emoji ein"
renote: "Renote"
unrenote: "Renote zurücknehmen"
renoted: "Renote getätigt."
renotedToX: "Renoted zu {name}."
cantRenote: "Renote dieses Beitrags nicht möglich."
cantReRenote: "Renote einer Renote nicht möglich."
quote: "Zitieren"
inChannelRenote: "Kanal-interner Renote"
inChannelQuote: "Kanal-internes Zitat"
renoteToChannel: "Renote zu Kanal"
renoteToOtherChannel: "Renote zu anderem Kanal"
pinnedNote: "Angeheftete Notiz"
pinned: "Angeheftet"
you: "Du"
@ -127,12 +131,13 @@ reactions: "Reaktionen"
emojiPicker: "Emoji auswählen"
pinnedEmojisForReactionSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie beim Reagieren als Erstes anzuzeigen."
pinnedEmojisSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie in der Emoji-Auswahl als Erstes anzuzeigen"
emojiPickerDisplay: "Anzeige der Emoji-Auswahl"
overwriteFromPinnedEmojisForReaction: "Überschreiben mit den Reaktions-Einstellungen"
overwriteFromPinnedEmojis: "Überschreiben mit den allgemeinen Einstellungen"
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
rememberNoteVisibility: "Notizsichtbarkeit merken"
attachCancel: "Anhang entfernen"
deleteFile: "Datei gelöscht"
deleteFile: "Datei löschen"
markAsSensitive: "Als sensibel markieren"
unmarkAsSensitive: "Als nicht sensibel markieren"
enterFileName: "Dateinamen eingeben"
@ -180,6 +185,8 @@ addAccount: "Benutzerkonto hinzufügen"
reloadAccountsList: "Benutzerkontoliste aktualisieren"
loginFailed: "Anmeldung fehlgeschlagen"
showOnRemote: "Auf Ursprungsinstanz ansehen"
chooseServerOnMisskeyHub: "Wähle einen Server aus dem Misskey Hub"
inputHostName: "Gib die Domain an"
general: "Allgemein"
wallpaper: "Hintergrund"
setWallpaper: "Hintergrund festlegen"
@ -206,6 +213,7 @@ perDay: "Pro Tag"
stopActivityDelivery: "Senden von Aktivitäten einstellen"
blockThisInstance: "Diese Instanz blockieren"
silenceThisInstance: "Instanz stummschalten"
mediaSilenceThisInstance: "Medien dieses Servers stummschalten"
operations: "Aktionen"
software: "Software"
version: "Version"
@ -227,6 +235,8 @@ blockedInstances: "Blockierte Instanzen"
blockedInstancesDescription: "Gib die Hostnamen der Instanzen, welche blockiert werden sollen, durch Zeilenumbrüche getrennt an. Blockierte Instanzen können mit dieser instanz nicht mehr kommunizieren."
silencedInstances: "Stummgeschaltete Instanzen"
silencedInstancesDescription: "Gib die Hostnamen der Instanzen, welche stummgeschaltet werden sollen, durch Zeilenumbrüche getrennt an. Alle Konten dieser Instanzen werden als stummgeschaltet behandelt, können nur noch Follow-Anfragen stellen und wenn nicht gefolgt keine lokalen Konten erwähnen. Blockierte Instanzen sind davon nicht betroffen."
mediaSilencedInstances: "Medien-stummgeschaltete Server"
mediaSilencedInstancesDescription: "Gib pro Zeile die Hostnamen der Server ein, dessen Medien du stummschalten möchtest. Alle Benutzerkonten der aufgeführten Server werden als sensibel behandelt und können keine benutzerdefinierten Emojis verwenden. Gesperrte Server sind davon nicht betroffen."
muteAndBlock: "Stummschaltungen und Blockierungen"
mutedUsers: "Stummgeschaltete Benutzer"
blockedUsers: "Blockierte Benutzer"
@ -325,6 +335,7 @@ renameFolder: "Ordner umbenennen"
deleteFolder: "Ordner löschen"
folder: "Ordner"
addFile: "Datei hinzufügen"
showFile: "Datei anzeigen"
emptyDrive: "Deine Drive ist leer"
emptyFolder: "Dieser Ordner ist leer"
unableToDelete: "Nicht löschbar"
@ -367,7 +378,6 @@ enableLocalTimeline: "Lokale Chronik aktivieren"
enableGlobalTimeline: "Globale Chronik aktivieren"
disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind."
registration: "Registrieren"
enableRegistration: "Registrierung neuer Benutzer erlauben"
invite: "Einladen"
driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto"
driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen"
@ -473,6 +483,7 @@ retype: "Erneut eingeben"
noteOf: "Notiz von {user}"
quoteAttached: "Zitat"
quoteQuestion: "Als Zitat anhängen?"
attachAsFileQuestion: "Der Text in der Zwischenablage ist lang. Möchtest du ihn als Textdatei anhängen?"
noMessagesYet: "Noch keine Nachrichten vorhanden"
newMessageExists: "Du hast eine neue Nachricht"
onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden"
@ -498,7 +509,11 @@ uiLanguage: "Sprache der Benutzeroberfläche"
aboutX: "Über {x}"
emojiStyle: "Emoji-Stil"
native: "Nativ"
menuStyle: "Menü Stil"
style: "Stil"
popup: "Pop-up"
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
showReactionsCount: "Zeige die Anzahl der Reaktionen auf Notizen an"
noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren"
@ -579,6 +594,7 @@ ascendingOrder: "Aufsteigende Reihenfolge"
descendingOrder: "Absteigende Reihenfolge"
scratchpad: "Testumgebung"
scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen."
uiInspector: "UI-Inspektor"
output: "Ausgabe"
script: "Skript"
disablePagesScript: "AiScript auf Seiten deaktivieren"
@ -659,6 +675,7 @@ smtpSecure: "Für SMTP-Verbindungen implizit SSL/TLS verwenden"
smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest."
testEmail: "Emailversand testen"
wordMute: "Wortstummschaltung"
hardWordMute: "Harte Wort-Stummschaltung"
regexpError: "Fehler in einem regulären Ausdruck"
regexpErrorDescription: "Im regulären Ausdruck deiner in Zeile {line} von {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:"
instanceMute: "Instanzstummschaltungen"
@ -680,6 +697,7 @@ useGlobalSettingDesc: "Ist diese Option aktiviert, werden die Benachrichtigungse
other: "Anderes"
regenerateLoginToken: "Anmeldetoken regenerieren"
regenerateLoginTokenDescription: "Den zur Anmeldung intern verwendeten Token regenerieren. Normalerweise wird dies nicht benötigt. Bei Regeneration werden alle Geräte ausgeloggt."
theKeywordWhenSearchingForCustomEmoji: "Das ist das Schlagwort beim Suchen von benutzerdefinierten Emojis."
setMultipleBySeparatingWithSpace: "Trenne Elemente durch ein Leerzeichen um mehrere Einstellungen zu kofigurieren."
fileIdOrUrl: "Datei-ID oder URL"
behavior: "Verhalten"
@ -889,6 +907,8 @@ makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktion
classic: "Classic"
muteThread: "Thread stummschalten"
unmuteThread: "Threadstummschaltung aufheben"
followingVisibility: "Sichtbarkeit der Gefolgten"
followersVisibility: "Sichtbarkeit der Folgenden"
continueThread: "Weiteren Threadverlauf anzeigen"
deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?"
incorrectPassword: "Falsches Passwort."
@ -1021,6 +1041,7 @@ thisPostMayBeAnnoyingHome: "Zur Startseite schicken"
thisPostMayBeAnnoyingCancel: "Abbrechen"
thisPostMayBeAnnoyingIgnore: "Trotzdem schicken"
collapseRenotes: "Bereits gesehene Renotes verkürzt anzeigen"
collapseRenotesDescription: "Klappe Notizen ein, auf die du bereits reagiert oder die du renoted hast."
internalServerError: "Serverinterner Fehler"
internalServerErrorDescription: "Im Server ist ein unerwarteter Fehler aufgetreten."
copyErrorInfo: "Fehlerdetails kopieren"
@ -1045,6 +1066,7 @@ sensitiveWords: "Sensible Wörter"
sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden."
sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
prohibitedWords: "Verbotene Wörter"
prohibitedWordsDescription: "Aktiviert eine Fehlermeldung, wenn versucht wird, eine Notiz zu veröffentlichen, die das/die eingestellte(n) Wort(e) enthält. Mehrere Begriffe können durch Zeilenumbrüche getrennt festgelegt werden."
prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
hiddenTags: "Ausgeblendete Hashtags"
hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden."
@ -1170,6 +1192,9 @@ confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten
confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?"
externalServices: "Externe Dienste"
sourceCode: "Quellcode"
sourceCodeIsNotYetProvided: "Der Quellcode ist noch nicht verfügbar. Kontaktiere den Administrator, um das Problem zu lösen."
repositoryUrl: "Repository URL"
repositoryUrlOrTarballRequired: "Wenn du kein Repository veröffentlicht hast, musst du stattdessen einen Tarball bereitstellen. Siehe .config/example.yml für weitere Informationen."
impressum: "Impressum"
impressumUrl: "Impressums-URL"
impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend."
@ -1192,34 +1217,76 @@ cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Besch
doReaction: "Reagieren"
code: "Code"
remainingN: "Verbleibend: {n}"
overwriteContentConfirm: "Bist du sicher, dass du den aktuellen Inhalt überschreiben willst?"
seasonalScreenEffect: "Saisonaler Bildschirmeffekt"
decorate: "Dekorieren"
addMfmFunction: "MFM hinzufügen"
enableQuickAddMfmFunction: "Erweiterte MFM-Auswahl anzeigen"
sfx: "Soundeffekte"
soundWillBePlayed: "Es wird Ton wiedergegeben"
showReplay: "Wiederholung anzeigen"
ranking: "Rangliste"
lastNDays: "Letzten {n} Tage"
backToTitle: "Zurück zum Startbildschirm"
enableHorizontalSwipe: "Wischen, um zwischen Tabs zu wechseln"
loading: "Laden"
surrender: "Abbrechen"
gameRetry: "Erneut versuchen"
notUsePleaseLeaveBlank: "Leer lassen, wenn nicht verwendet"
useTotp: "Gib das Einmalpasswort ein"
useBackupCode: "Verwende die Backup-Codes"
launchApp: "Starte die App"
useNativeUIForVideoAudioPlayer: "Browser-Benutzeroberfläche für die Video- und Audiowiedergabe verwenden"
keepOriginalFilename: "Ursprünglichen Dateinamen beibehalten"
keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird der Dateiname beim Hochladen automatisch durch eine zufällige Zeichenfolge ersetzt."
noDescription: "Keine Beschreibung vorhanden"
tryAgain: "Bitte später erneut versuchen"
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
createdLists: "Erstellte Listen"
createdAntennas: "Erstellte Antennen"
fromX: "Von {x}"
genEmbedCode: "Einbettungscode generieren"
noteOfThisUser: "Notizen dieses Benutzers"
clipNoteLimitExceeded: "Zu diesem Clip können keine weiteren Notizen hinzugefügt werden."
discard: "Verwerfen"
thereAreNChanges: "Es gibt {n} Änderung(en)"
signinWithPasskey: "Mit Passkey anmelden"
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen"
prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen."
yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff"
yourNameContainsProhibitedWordsDescription: "Der Name enthält eine verbotene Zeichenfolge. Wende dich an deinen Serveradministrator, wenn du diesen Namen verwenden möchtest."
pleaseSelectAccount: "Bitte Konto auswählen"
availableRoles: "Verfügbare Rollen"
_accountSettings:
requireSigninToViewContents: "Anmeldung erfordern, um Inhalte anzuzeigen"
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
_abuseUserReport:
forward: "Weiterleiten"
forwardDescription: "Leite die Meldung an einen entfernten Server als anonymes Systemkonto weiter."
accept: "Akzeptieren"
reject: "Ablehnen"
_delivery:
stop: "Gesperrt"
_type:
none: "Wird veröffentlicht"
_bubbleGame:
howToPlay: "Wie man spielt"
hold: "Halten"
_score:
score: "Spielstand"
scoreYen: "Verdienter Geldbetrag"
highScore: "Höchstpunktzahl"
maxChain: "Maximale Anzahl an Verkettungen"
yen: "{yen} Yen"
_howToPlay:
section1: "Passe die Position an und lasse das Objekt in das Spielfeld fallen."
section2: "Wenn sich zwei Objekte der gleichen Art berühren, verwandeln sie sich in ein anderes Objekt und du bekommst Punkte."
section3: "Das Spiel ist vorbei, wenn die Objekte aus dem Spielfeld herausragen. Versuche eine hohe Punktzahl zu erreichen, indem du die Objekte miteinander verschmelzt, ohne dass das Spielfeld überläuft!"
_announcement:
forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
@ -1263,8 +1330,18 @@ _initialTutorial:
reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen."
_reaction:
title: "Was sind Reaktionen?"
description: "Auf Notizen kann mit verschiedenen Emojis reagiert werden. Reaktionen ermöglichen es dir, Nuancen auszudrücken, die mit einem einfachen „Gefällt mir“ vielleicht nicht ausgedrückt werden können."
letsTryReacting: "Reaktionen können durch Klicken auf die Schaltfläche „+“ in der Notiz hinzugefügt werden. Versuche, auf diese Beispielnotiz zu reagieren!"
reactToContinue: "Füge eine Reaktion hinzu, um fortzufahren."
reactNotification: "Du erhältst Echtzeit-Benachrichtigungen, wenn jemand auf deine Notiz reagiert."
reactDone: "Du kannst eine Reaktion zurücknehmen, indem du auf den '-' Button drückst."
_timeline:
title: "So funktionieren die Chroniken"
home: "Du kannst Beiträge von den Konten sehen, denen du folgst."
local: "Du kannst Beiträge aller Benutzer auf diesem Server sehen."
social: "Notizen von der Startseite und der lokalen Chronik werden angezeigt."
global: "Du kannst Notizen von allen föderierten Servern sehen."
description2: "Du kannst jederzeit am oberen Rand des Bildschirms zwischen den jeweiligen Chroniken wechseln."
_postNote:
_visibility:
description: "Du kannst einschränken, wer deine Notiz sehen kann."
@ -1272,8 +1349,16 @@ _initialTutorial:
doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!"
_cw:
title: "Inhaltswarnung"
_exampleNote:
note: "Ich hatte gerade einen Donut mit Schokoladenüberzug 🍩😋"
_howToMakeAttachmentsSensitive:
tryThisFile: "Versuche, das angehängte Bild als sensibel zu markieren!"
method: "Um einen Anhang als sensibel zu kennzeichnen, klicke auf das Vorschaubild der Datei, um das Menü zu öffnen, und klicke auf „Als sensibel markieren“."
sensitiveSucceeded: "Wenn du Dateien anhängst, stelle bitte die Sensibilität entsprechend der Serverrichtlinien ein."
doItToContinue: "Markiere die angehängte Datei als sensibel, um fortzufahren."
_done:
title: "Du hast das Tutorial abgeschlossen! 🎉"
description: "Die hier beschriebenen Funktionen sind nur ein kleiner Teil dessen, was Misskey zu bieten hat; um mehr darüber zu erfahren, wie du Misskey benutzen kannst, besuche bitte {link}."
_timelineDescription:
local: "In der lokalen Chronik siehst du Notizen von allen Benutzern auf diesem Server."
global: "In der globalen Chronik siehst du Notizen von allen föderierten Servern."
@ -1291,6 +1376,7 @@ _serverSettings:
fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden."
fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen"
fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. "
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
_accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren"
moveFromSub: "Alias für ein anderes Konto erstellen"
@ -1549,6 +1635,7 @@ _achievements:
title: "Testüberfluss"
description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne"
_tutorialCompleted:
title: "Misskey Grundkurs-Diplom"
description: "Tutorial abgeschlossen"
_bubbleGameExplodingHead:
title: "🤯"
@ -1594,6 +1681,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
mentionMax: "Maximale Anzahl von Erwähnungen in einer Notiz"
canInvite: "Erstellung von Einladungscodes für diese Instanz"
inviteLimit: "Maximalanzahl an Einladungen"
inviteLimitCycle: "Zyklus des Einladungslimits"
@ -1616,9 +1704,12 @@ _role:
canSearchNotes: "Nutzung der Notizsuchfunktion"
canUseTranslator: "Verwendung des Übersetzers"
avatarDecorationLimit: "Maximale Anzahl an Profilbilddekorationen, die angebracht werden können"
canImportAntennas: "Importieren von Antennen erlauben"
_condition:
isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz"
isCat: "Katzen-Benutzer"
isBot: "Bot-Benutzer"
createdLessThan: "Kontoerstellung liegt weniger als X zurück"
createdMoreThan: "Kontoerstellung liegt mehr als X zurück"
followersLessThanOrEq: "Hat X oder weniger Follower"
@ -1834,6 +1925,12 @@ _sfx:
note: "Notizen"
noteMy: "Meine Notizen"
notification: "Benachrichtigungen"
_soundSettings:
driveFile: "Audiodatei aus dem Drive verwenden"
driveFileWarn: "Wähle eine Audiodatei aus dem Drive"
driveFileTypeWarn: "Diese Datei wird nicht unterstützt"
driveFileTypeWarnDescription: "Bitte wähle eine Audiodatei"
driveFileDurationWarn: "Audio zu lang."
_ago:
future: "Zukunft"
justNow: "Gerade eben"
@ -1915,6 +2012,23 @@ _permissions:
"write:flash": "Deine Plays bearbeiten oder löschen"
"read:flash-likes": "Liste der Plays, die mir gefallen, lesen"
"write:flash-likes": "Liste der Plays, die mir gefallen, bearbeiten"
"write:admin:delete-account": "Benutzerkonto löschen"
"write:admin:delete-all-files-of-a-user": "Alle Dateien eines Benutzers löschen"
"read:admin:index-stats": "Statistiken zu Datenbankindizes einsehen"
"read:admin:table-stats": "Statistiken zu Datenbanktabellen einsehen"
"read:admin:user-ips": "IP-Adressen von Benutzern anzeigen"
"read:admin:meta": "Metadaten der Instanz einsehen"
"write:admin:reset-password": "Benutzerpasswort zurücksetzen"
"write:admin:send-email": "E-Mail versenden"
"read:admin:server-info": "Serverinformationen anzeigen"
"read:admin:show-moderation-log": "Moderationsprotokoll einsehen"
"read:admin:show-user": "Private Benutzerinformationen einsehen"
"write:admin:invite-codes": "Einladungscodes verwalten"
"read:admin:invite-codes": "Einladungscodes anzeigen"
"write:admin:announcements": "Ankündigungen verwalten"
"read:admin:announcements": "Ankündigungen einsehen"
"write:admin:avatar-decorations": "Kann Avatar-Dekorationen verwalten"
"read:admin:avatar-decorations": "Avatar-Dekorationen ansehen"
_auth:
shareAccessTitle: "Verteilung von App-Berechtigungen"
shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?"
@ -2329,3 +2443,31 @@ _reversi:
black: "Schwarz"
white: "Weiß"
total: "Gesamt"
_offlineScreen:
header: "Verbindung zum Server nicht möglich"
_urlPreviewSetting:
title: "Einstellungen der URL-Vorschau"
enable: "URL-Vorschau aktivieren"
timeout: "Zeitüberschreitung beim Abrufen der Vorschau (ms)"
maximumContentLength: "Maximale Content-Length (Bytes)"
_mediaControls:
playbackRate: "Wiedergabegeschwindigkeit"
_contextMenu:
title: "Kontextmenü"
app: "Anwendung"
_embedCodeGen:
title: "Einbettungscode anpassen"
header: "Kopfzeile anzeigen"
autoload: "Automatisch mehr laden (veraltet)"
maxHeight: "Maximale Höhe"
maxHeightDescription: "Der Wert 0 deaktiviert die Einstellung der maximalen Höhe. Gib einen Wert an, um zu verhindern, dass das Widget weiterhin vertikal vergrößert wird."
maxHeightWarn: "Die Begrenzung der maximalen Höhe ist deaktiviert (0). Wenn dies nicht beabsichtigt war, setze die maximale Höhe auf einen Wert fest."
applyToPreview: "Auf die Vorschau anwenden"
generateCode: "Einbettungscode generieren"
codeGenerated: "Der Code wurde generiert"
codeGeneratedDescription: "Füge den generierten Code in deine Website ein, um den Inhalt einzubetten."
_selfXssPrevention:
warning: "WARNUNG"
title: "„Füge in diesen Bereich etwas ein“ ist eine Betrugsmasche."
description1: "Wenn du hier etwas einfügst, könnte ein böswilliger Benutzer dein Konto übernehmen oder deine persönlichen Daten stehlen."
description3: "Weitere Informationen findest du hier. {link}"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "Enable local timeline"
enableGlobalTimeline: "Enable global timeline"
disablingTimelinesInfo: "Adminstrators and Moderators will always have access to all timelines, even if they are not enabled."
registration: "Register"
enableRegistration: "Enable new user registration"
invite: "Invite"
driveCapacityPerLocalAccount: "Drive capacity per local user"
driveCapacityPerRemoteAccount: "Drive capacity per remote user"
@ -1309,7 +1308,7 @@ _accountSettings:
makeNotesFollowersOnlyBeforeDescription: "While this feature is enabled, only followers can see notes past the set date and time or have been visible for a set time. When it is deactivated, the note publication status will also be restored."
makeNotesHiddenBefore: "Make past notes private"
makeNotesHiddenBeforeDescription: "While this feature is enabled, notes that are past the set date and time or have been visible only to you. When it is deactivated, the note publication status will also be restored."
mayNotEffectForFederatedNotes: "Notes federated to a remote server may not be effective."
mayNotEffectForFederatedNotes: "Notes federated to a remote server may not be affected."
notesHavePassedSpecifiedPeriod: "Note that the specified time has passed"
notesOlderThanSpecifiedDateAndTime: "Notes before the specified date and time"
_abuseUserReport:

View file

@ -373,7 +373,6 @@ enableLocalTimeline: "Habilitar linea de tiempo local"
enableGlobalTimeline: "Habilitar linea de tiempo global"
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos"
registration: "Registro"
enableRegistration: "Permitir nuevos registros"
invite: "Invitar"
driveCapacityPerLocalAccount: "Capacidad del drive por usuario local"
driveCapacityPerRemoteAccount: "Capacidad del drive por usuario remoto"

View file

@ -8,6 +8,9 @@ search: "Rechercher"
notifications: "Notifications"
username: "Nom dutilisateur·rice"
password: "Mot de passe"
initialPasswordForSetup: "Mot de passe initial pour la configuration"
initialPasswordIsIncorrect: "Mot de passe initial pour la configuration est incorrecte"
initialPasswordForSetupDescription: "Utilisez le mot de passe que vous avez entré pour le fichier de configuration si vous avez installé Misskey vous-même.\nSi vous utilisez un service d'hébergement Misskey, utilisez le mot de passe fourni.\nSi vous n'avez pas défini de mot de passe, laissez le champ vide pour continuer."
forgotPassword: "Mot de passe oublié"
fetchingAsApObject: "Récupération depuis le fédiverse …"
ok: "OK"
@ -60,6 +63,7 @@ copyFileId: "Copier l'identifiant du fichier"
copyFolderId: "Copier l'identifiant du dossier"
copyProfileUrl: "Copier l'URL du profil"
searchUser: "Chercher un·e utilisateur·rice"
searchThisUsersNotes: "Cherchez les notes de cet·te utilisateur·rice"
reply: "Répondre"
loadMore: "Afficher plus …"
showMore: "Voir plus"
@ -108,6 +112,7 @@ enterEmoji: "Insérer un émoji"
renote: "Renoter"
unrenote: "Annuler la Renote"
renoted: "Renoté !"
renotedToX: "Renoté en {name}"
cantRenote: "Ce message ne peut pas être renoté."
cantReRenote: "Impossible de renoter une Renote."
quote: "Citer"
@ -151,6 +156,7 @@ editList: "Modifier la liste"
selectChannel: "Sélectionner un canal"
selectAntenna: "Sélectionner une antenne"
editAntenna: "Modifier l'antenne"
createAntenna: "Créer une antenne"
selectWidget: "Sélectionner un widget"
editWidgets: "Modifier les widgets"
editWidgetsExit: "Valider les modifications"
@ -177,6 +183,7 @@ addAccount: "Ajouter un compte"
reloadAccountsList: "Rafraichir la liste des comptes"
loginFailed: "Échec de la connexion"
showOnRemote: "Voir sur linstance distante"
continueOnRemote: "Continuer sur l'instance distante"
general: "Général"
wallpaper: "Fond décran"
setWallpaper: "Définir le fond décran"
@ -187,6 +194,7 @@ followConfirm: "Êtes-vous sûr·e de vouloir suivre {name} ?"
proxyAccount: "Compte proxy"
proxyAccountDescription: "Un compte proxy se comporte, dans certaines conditions, comme un·e abonné·e distant·e pour les utilisateurs d'autres instances. Par exemple, quand un·e utilisateur·rice ajoute un·e utilisateur·rice distant·e à une liste, ses notes ne seront pas visibles sur l'instance si personne ne suit cet·te utilisateur·rice. Le compte proxy va donc suivre cet·te utilisateur·rice pour que ses notes soient acheminées."
host: "Serveur distant"
selectSelf: "Sélectionner manuellement"
selectUser: "Sélectionner un·e utilisateur·rice"
recipient: "Destinataire"
annotation: "Commentaires"
@ -320,6 +328,7 @@ renameFolder: "Renommer le dossier"
deleteFolder: "Supprimer le dossier"
folder: "Dossier"
addFile: "Ajouter un fichier"
showFile: "Voir les fichiers"
emptyDrive: "Le Disque est vide"
emptyFolder: "Le dossier est vide"
unableToDelete: "Suppression impossible"
@ -362,7 +371,6 @@ enableLocalTimeline: "Activer le fil local"
enableGlobalTimeline: "Activer le fil global"
disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur·rice·s et les modérateur·rice·s pourront toujours y accéder."
registration: "Sinscrire"
enableRegistration: "Autoriser les nouvelles inscriptions"
invite: "Inviter"
driveCapacityPerLocalAccount: "Capacité de stockage du Disque par utilisateur local"
driveCapacityPerRemoteAccount: "Capacité de stockage du Disque par utilisateur distant"
@ -430,10 +438,11 @@ token: "Jeton"
2fa: "Authentification à deux facteurs"
setupOf2fa: "Configuration de lauthentification à deux facteurs"
totp: "Application d'authentification"
totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification"
totpDescription: "Entrer un mot de passe à usage unique à l'aide d'une application d'authentification"
moderator: "Modérateur·rice·s"
moderation: "Modérations"
moderationNote: "Note de modération"
moderationNoteDescription: "Vous pouvez remplir des notes qui seront partagés seulement entre modérateurs."
addModerationNote: "Ajouter une note de modération"
moderationLogs: "Journal de modération"
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
@ -493,6 +502,10 @@ uiLanguage: "Langue daffichage de linterface"
aboutX: "À propos de {x}"
emojiStyle: "Style des émojis"
native: "Natif"
menuStyle: "Style du menu"
style: "Style"
drawer: "Sélecteur"
popup: "Pop-up"
showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol"
showReactionsCount: "Afficher le nombre de réactions des notes"
noHistory: "Pas d'historique"
@ -575,6 +588,7 @@ ascendingOrder: "Ascendant"
descendingOrder: "Descendant"
scratchpad: "ScratchPad"
scratchpadDescription: "ScratchPad fournit un environnement expérimental pour AiScript. Vous pouvez vérifier la rédaction de votre code, sa bonne exécution et le résultat de son interaction avec Misskey."
uiInspector: "Inspecteur UI"
output: "Sortie"
script: "Script"
disablePagesScript: "Désactiver AiScript sur les Pages"
@ -618,7 +632,7 @@ description: "Description"
describeFile: "Ajouter une description d'image"
enterFileDescription: "Saisissez une description"
author: "Auteur·rice"
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer ?"
leaveConfirm: "Vous avez des modifications non sauvegardées. Voulez-vous les ignorer ?"
manage: "Gestion"
plugins: "Extensions"
preferencesBackups: "Sauvegarder les paramètres"
@ -828,6 +842,7 @@ administration: "Gestion"
accounts: "Comptes"
switch: "Remplacer"
noMaintainerInformationWarning: "Informations administrateur non configurées."
noInquiryUrlWarning: "L'URL demandé n'est pas définie"
noBotProtectionWarning: "La protection contre les bots n'est pas configurée."
configure: "Configurer"
postToGallery: "Publier dans la galerie"
@ -892,6 +907,7 @@ followersVisibility: "Visibilité des abonnés"
continueThread: "Afficher la suite du fil"
deleteAccountConfirm: "Votre compte sera supprimé. Êtes vous certain ?"
incorrectPassword: "Le mot de passe est incorrect."
incorrectTotp: "Le mot de passe à usage unique est incorrect ou a expiré."
voteConfirm: "Confirmez-vous votre vote pour « {choice} » ?"
hide: "Masquer"
useDrawerReactionPickerForMobile: "Afficher le sélecteur de réactions en tant que panneau sur mobile"
@ -916,6 +932,9 @@ oneHour: "1 heure"
oneDay: "1 jour"
oneWeek: "1 semaine"
oneMonth: "Un mois"
threeMonths: "3 mois"
oneYear: "1 an"
threeDays: "3 jours"
reflectMayTakeTime: "Cela peut prendre un certain temps avant que cela ne se termine."
failedToFetchAccountInformation: "Impossible de récupérer les informations du compte."
rateLimitExceeded: "Limite de taux dépassée"
@ -923,7 +942,7 @@ cropImage: "Recadrer l'image"
cropImageAsk: "Voulez-vous recadrer cette image ?"
cropYes: "Rogner"
cropNo: "Utiliser en l'état"
file: "Fichiers"
file: "Fichier"
recentNHours: "Dernières {n} heures"
recentNDays: "Derniers {n} jours"
noEmailServerWarning: "Serveur de courrier non configuré."
@ -1055,6 +1074,7 @@ retryAllQueuesConfirmTitle: "Vraiment réessayer ?"
retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur."
enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants"
enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes"
enableStatsForFederatedInstances: "Recevoir les statistiques des instances distantes"
showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note"
reactionsDisplaySize: "Taille de l'affichage des réactions"
limitWidthOfReaction: "Limiter la largeur maximale des réactions et les afficher en taille réduite"
@ -1102,6 +1122,8 @@ preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA géné
preventAiLearningDescription: "Demander aux robots d'indexation de ne pas utiliser le contenu publié, tel que les notes et les images, dans l'apprentissage automatique d'IA générative. Cela est réalisé en incluant le drapeau « noai » dans la réponse HTML. Une prévention complète n'est toutefois pas possible, car il est au robot d'indexation de respecter cette demande."
options: "Options"
specifyUser: "Spécifier l'utilisateur·rice"
openTagPageConfirm: "Ouvrir une page d'hashtags ?"
specifyHost: "Spécifier un serveur distant"
failedToPreviewUrl: "Aperçu d'URL échoué"
update: "Mettre à jour"
rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction"
@ -1222,13 +1244,55 @@ enableHorizontalSwipe: "Glisser pour changer d'onglet"
loading: "Chargement en cours"
surrender: "Annuler"
gameRetry: "Réessayer"
notUsePleaseLeaveBlank: "Laisser vide si non utilisé"
useTotp: "Entrer un mot de passe à usage unique"
useBackupCode: "Utiliser le codes de secours"
launchApp: "Lancer l'app"
useNativeUIForVideoAudioPlayer: "Lire les vidéos et audios en utilisant l'UI du navigateur"
keepOriginalFilename: "Garder le nom original du fichier"
keepOriginalFilenameDescription: "Si vous désactivez ce paramètre, les noms de fichiers seront automatiquement remplacés par des noms aléatoires lorsque vous téléchargerez des fichiers."
noDescription: "Il n'y a pas de description"
alwaysConfirmFollow: "Confirmer lors d'un abonnement"
inquiry: "Contact"
tryAgain: "Veuillez réessayer plus tard"
confirmWhenRevealingSensitiveMedia: "Confirmer pour révéler du contenu sensible"
sensitiveMediaRevealConfirm: "Ceci pourrait être du contenu sensible. Voulez-vous l'afficher ?"
createdLists: "Listes créées"
createdAntennas: "Antennes créées"
fromX: "De {x}"
genEmbedCode: "Générer le code d'intégration"
noteOfThisUser: "Notes de cet·te utilisateur·rice"
clipNoteLimitExceeded: "Aucune note supplémentaire ne peut être ajoutée à ce clip."
performance: "Performance"
modified: "Modifié"
discard: "Annuler"
thereAreNChanges: "Il y a {n} modification(s)"
signinWithPasskey: "Se connecter avec une clé d'accès"
unknownWebAuthnKey: "Clé d'accès inconnue."
passkeyVerificationFailed: "La vérification de la clé d'accès a échoué."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La vérification de la clé d'accès a réussi, mais la connexion sans mot de passe est désactivée."
messageToFollower: "Message aux abonné·es"
target: "Destinataire"
prohibitedWordsForNameOfUser: "Mots interdits pour les noms d'utilisateur·rices"
lockdown: "Verrouiller"
pleaseSelectAccount: "Sélectionner un compte"
availableRoles: "Rôles disponibles"
_abuseUserReport:
forward: "Transférer"
forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme."
resolve: "Résoudre"
accept: "Accepter"
reject: "Rejeter"
resolveTutorial: "Si le signalement est légitime dans son contenu, sélectionnez « Accepter » pour marquer le cas comme résolu par l'affirmative.\nSi le contenu du rapport n'est pas légitime, sélectionnez « Rejeter » pour marquer le cas comme résolu par la négative."
_delivery:
status: "Statut de la diffusion"
stop: "Suspendu·e"
resume: "Reprendre"
_type:
none: "Publié"
manuallySuspended: "Suspendre manuellement"
goneSuspended: "L'instance est suspendue en raison de la suppression de ce dernier"
autoSuspendedForNotResponding: "L'instance est suspendue car elle ne répond pas"
_bubbleGame:
howToPlay: "Comment jouer"
hold: "Réserver"
@ -1239,6 +1303,7 @@ _bubbleGame:
maxChain: "Nombre maximum de chaînes"
yen: "{yen} yens"
estimatedQty: "{qty} pièces"
scoreSweets: "{onigiriQtyWithUnit} Onigiri(s)"
_announcement:
forExistingUsers: "Pour les utilisateurs existants seulement"
needConfirmationToRead: "Exiger la confirmation de la lecture"
@ -1258,6 +1323,7 @@ _initialAccountSetting:
profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité"
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
haveFun: "Profitez de {name} !"
youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {name}(Misskey) ou vous arrêter ici et commencer à l'utiliser immédiatement."
startTutorial: "Démarrer le tutoriel"
skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?"
@ -1351,18 +1417,60 @@ _achievements:
flavor: "Passez un bon moment avec Misskey !"
_notes10:
title: "Quelques notes"
description: "Poster 10 notes"
_notes100:
title: "Beaucoup de notes"
description: "Poster 100 notes"
_notes500:
title: "Couvert de notes"
description: "Poster 500 notes"
_notes1000:
title: "Une montagne de notes"
description: "Poster 1000 notes"
_notes5000:
title: "Débordement de notes"
description: "Poster 5 000 notes"
_notes10000:
title: "Super note"
description: "Poster 10 000 notes"
_notes20000:
title: "Encore... plus... de... notes..."
description: "Poster 20 000 notes"
_notes30000:
title: "Notes notes notes !"
description: "Poster 30 000 notes"
_notes40000:
title: "Usine de notes"
description: "Poster 40 000 notes"
_notes50000:
title: "Planète des notes"
description: "Poster 50 000 notes"
_notes60000:
title: "Quasar de note"
description: "Poster 50 000 notes"
_notes70000:
title: "Trou noir de notes"
description: "Poster 70 000 notes"
_notes80000:
title: "Galaxie de notes"
description: "Poster 80 000 notes"
_notes90000:
title: "Univers de notes"
description: "Poster 90 000 notes"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "Poster 100 000 notes"
flavor: "Avez-vous tant de choses à dire ?"
_login3:
title: "Débutant "
title: "Débutant I"
description: "Se connecter pour un total de 3 jours"
flavor: "Dès maintenant, appelez-moi Misskeynaute"
_login7:
title: "Débutant Ⅱ"
title: "Débutant II"
description: "Se connecter pour un total de 7 jours"
flavor: "On s'habitue ?"
_login15:
title: "Débutant Ⅲ"
title: "Débutant III"
description: "Se connecter pour un total de 15 jours"
_login30:
title: "Misskeynaute I"
@ -1386,6 +1494,7 @@ _achievements:
_login500:
title: "Expert I"
description: "Se connecter pour un total de 500 jours"
flavor: "Non, mes amis, j'aime les notes"
_login600:
title: "Expert II"
description: "Se connecter pour un total de 600 jours"
@ -1393,11 +1502,18 @@ _achievements:
title: "Expert III"
description: "Se connecter pour un total de 700 jours"
_login800:
title: "Maître des notes I"
description: "Se connecter pour un total de 800 jours"
_login900:
title: "Maître des notes II"
description: "Se connecter pour un total de 900 jours"
_login1000:
title: "Maître des notes III"
description: "Se connecter pour un total de 1 000 jours"
flavor: "Merci d'utiliser Misskey !"
_noteClipped1:
title: "Je... dois... clip..."
description: "Ajouter sa première note aux clips"
_profileFilled:
title: "Bien préparé"
description: "Configuration de votre profil"
@ -1456,21 +1572,31 @@ _achievements:
_driveFolderCircularReference:
title: "Référence circulaire"
_setNameToSyuilo:
title: "Complexe de dieu"
description: "Vous avez spécifié « syuilo » comme nom"
_passedSinceAccountCreated1:
title: "Premier anniversaire"
description: "Un an est passé depuis la création du compte"
_passedSinceAccountCreated2:
title: "Second anniversaire"
description: "Deux ans sont passés depuis la création du compte"
_passedSinceAccountCreated3:
title: "3ème anniversaire"
description: "Trois ans sont passés depuis la création du compte"
_loggedInOnBirthday:
title: "Joyeux Anniversaire !"
description: "Vous vous êtes connecté à la date de votre anniversaire"
_loggedInOnNewYearsDay:
title: "Bonne année !"
description: "Vous vous êtes connecté le premier jour de l'année"
flavor: "Merci pour le soutient continue sur cette instance."
_cookieClicked:
title: "Jeu de clic sur des cookies"
description: "Cliqué sur un cookie"
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
_brainDiver:
title: "Brain Diver"
description: "Poster le lien sur Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_smashTestNotificationButton:
title: "Débordement de tests"
@ -1478,6 +1604,11 @@ _achievements:
_tutorialCompleted:
title: "Diplôme de la course élémentaire de Misskey"
description: "Terminer le tutoriel"
_bubbleGameExplodingHead:
title: "🤯"
description: "Le plus gros objet du jeu de bulles"
_bubbleGameDoubleExplodingHead:
title: "Double🤯"
_role:
new: "Nouveau rôle"
edit: "Modifier le rôle"
@ -1508,9 +1639,11 @@ _role:
canManageCustomEmojis: "Gestion des émojis personnalisés"
canManageAvatarDecorations: "Gestion des décorations d'avatar"
driveCapacity: "Capacité de stockage du Disque"
antennaMax: "Nombre maximum d'antennes"
wordMuteMax: "Nombre maximal de caractères dans le filtre de mots"
canUseTranslator: "Usage de la fonctionnalité de traduction"
avatarDecorationLimit: "Nombre maximal de décorations d'avatar"
canImportAntennas: "Autoriser l'importation d'antennes"
_sensitiveMediaDetection:
description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement."
sensitivity: "Sensibilité de la détection"
@ -1793,6 +1926,29 @@ _permissions:
"write:gallery": "Éditer la galerie"
"read:gallery-likes": "Voir les mentions « J'aime » dans la galerie"
"write:gallery-likes": "Gérer les mentions « J'aime » dans la galerie"
"read:flash": "Voir le Play"
"write:flash": "Modifier le Play"
"read:flash-likes": "Lire vos mentions j'aime des Play"
"write:flash-likes": "Modifier vos mentions j'aime des Play"
"read:admin:abuse-user-reports": "Voir les utilisateurs signalés"
"write:admin:delete-account": "Supprimer le compte d'utilisateur"
"write:admin:delete-all-files-of-a-user": "Supprimer tous les fichiers d'un utilisateur"
"read:admin:index-stats": "Voir les statistiques sur les index de base de données"
"read:admin:table-stats": "Voir les statistiques sur les index de base de données"
"read:admin:user-ips": "Voir l'adresse IP de l'utilisateur"
"read:admin:meta": "Voir les métadonnées de l'instance"
"write:admin:reset-password": "Réinitialiser le mot de passe de l'utilisateur"
"write:admin:resolve-abuse-user-report": "Résoudre le signalement d'un utilisateur"
"write:admin:send-email": "Envoyer un mail"
"read:admin:server-info": "Voir les informations de l'instance"
"read:admin:show-moderation-log": "Voir les logs de modération"
"read:admin:show-user": "Voir les informations privées de l'utilisateur"
"write:admin:suspend-user": "Suspendre l'utilisateur"
"write:admin:unset-user-avatar": "Retirer l'avatar de l'utilisateur"
"write:admin:unset-user-banner": "Retirer la bannière de l'utilisateur"
"write:admin:unsuspend-user": "Lever la suspension d'un utilisateur"
"write:admin:meta": "Gérer les métadonnées de l'instance"
"write:admin:roles": "Gérer les rôles"
_auth:
shareAccess: "Autoriser \"{name}\" à accéder à votre compte ?"
shareAccessAsk: "Voulez-vous vraiment autoriser cette application à accéder à votre compte?"
@ -1944,7 +2100,16 @@ _timelines:
social: "Social"
global: "Global"
_play:
new: "Créer un Play"
edit: "Modifier un Play"
created: "Play créé"
updated: "Play édité"
deleted: "Play supprimé"
pageSetting: "Configuration du Play"
editThisPage: "Modifier ce Play"
viewSource: "Afficher la source"
my: "Mes Play"
liked: "Play aimés"
featured: "Populaire"
title: "Titre"
script: "Script"
@ -2018,10 +2183,13 @@ _notification:
achievementEarned: "Accomplissement déverrouillé"
testNotification: "Tester la notification"
reactedBySomeUsers: "{n} utilisateur·rice·s ont réagi"
likedBySomeUsers: "{n} utilisateurs ont aimé votre note"
renotedBySomeUsers: "{n} utilisateur·rice·s ont renoté"
followedBySomeUsers: "{n} utilisateur·rice·s se sont abonné·e·s à vous"
login: "Quelqu'un s'est connecté"
_types:
all: "Toutes"
note: "Nouvelles notes"
follow: "Nouvel·le abonné·e"
mention: "Mentions"
reply: "Réponses"
@ -2071,11 +2239,14 @@ _drivecleaner:
orderByCreatedAtAsc: "Date d'ajout ascendante"
_webhookSettings:
name: "Nom"
secret: "Secret"
trigger: "Activateur"
active: "Activé"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "E-mail "
keywords: "Mots clés "
_moderationLogTypes:
createRole: "Rôle créé"
deleteRole: "Rôle supprimé"
@ -2112,6 +2283,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "Décoration d'avatar supprimée"
unsetUserAvatar: "Supprimer l'avatar de l'utilisateur·rice"
unsetUserBanner: "Supprimer la bannière de l'utilisateur·rice"
deleteFlash: "Supprimer le Play"
_fileViewer:
title: "Détails du fichier"
type: "Type du fichier"
@ -2175,5 +2347,20 @@ _dataSaver:
title: "Mise en évidence du code"
description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données."
_reversi:
reversi: "Reversi"
blackIs: "{name} joue les noirs"
rules: "Règles"
waitingBoth: "Préparez-vous"
myTurn: "Cest votre tour"
turnOf: "C'est le tour de {name}"
pastTurnOf: "Tour de {name}"
surrender: "Se rendre"
surrendered: "Par abandon"
total: "Total"
playing: "En cours"
lookingForPlayer: "Recherche d'adversaire"
_mediaControls:
playbackRate: "Vitesse de lecture"
_embedCodeGen:
title: "Personnaliser le code d'intégration"
generateCode: "Générer le code d'intégration"

View file

@ -375,7 +375,6 @@ enableLocalTimeline: "Nyalakan lini masa lokal"
enableGlobalTimeline: "Nyalakan lini masa global"
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua lini masa meskipun lini masa tersebut tidak diaktifkan."
registration: "Pendaftaran"
enableRegistration: "Nyalakan pendaftaran pengguna baru"
invite: "Undang"
driveCapacityPerLocalAccount: "Kapasitas drive per pengguna lokal"
driveCapacityPerRemoteAccount: "Kapasitas drive per pengguna remote"

4
locales/index.d.ts vendored
View file

@ -2362,6 +2362,10 @@ export interface Locale extends ILocale {
*
*/
"details": string;
/**
*
*/
"renoteDetails": string;
/**
*
*/

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "Abilita la timeline locale"
enableGlobalTimeline: "Abilita la timeline federata"
disablingTimelinesInfo: "Anche disabilitandole, gli Amministratori e i Moderatori potranno comunque accedervi."
registration: "Iscriviti"
enableRegistration: "Consenti a chiunque di registrarsi"
invite: "Invita"
driveCapacityPerLocalAccount: "Capienza del Drive per profilo locale"
driveCapacityPerRemoteAccount: "Capienza del Drive per profilo remoto"

View file

@ -586,6 +586,7 @@ masterVolume: "マスター音量"
notUseSound: "サウンドを出力しない"
useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する"
details: "詳細"
renoteDetails: "リノートの詳細"
chooseEmoji: "絵文字を選択"
unableToProcess: "操作を完了できません"
recentUsed: "最近使用"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "ローカルタイムラインを使えるようにする
enableGlobalTimeline: "グローバルタイムラインを使えるようにするわ"
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
registration: "登録"
enableRegistration: "一見さんでも誰でもいらっしゃ~い"
invite: "来てや"
driveCapacityPerLocalAccount: "ローカルユーザーはんひとりあたりのドライブ容量"
driveCapacityPerRemoteAccount: "リモートユーザーはんひとりあたりのドライブ容量"

View file

@ -356,7 +356,6 @@ enableLocalTimeline: "로컬 타임라인 키기"
enableGlobalTimeline: "글로벌 타임라인 키기"
disablingTimelinesInfo: "요 타임라인얼 꺼도 간리자하고 중재자넌 고대로 설 수 잇십니다."
registration: "맨걸기"
enableRegistration: "누라도 새로 맨걸 수 잇거로 하기"
invite: "초대하기"
driveCapacityPerLocalAccount: "로컬 사용자 하나마중 드라이브 커기"
driveCapacityPerRemoteAccount: "웬겍 사용자 하나마중 드라이브 커기"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "로컬 타임라인 활성화"
enableGlobalTimeline: "글로벌 타임라인 활성화"
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
registration: "등록"
enableRegistration: "신규 회원가입을 활성화"
invite: "초대"
driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량"
driveCapacityPerRemoteAccount: "원격 사용자별 드라이브 용량"
@ -1257,7 +1256,7 @@ lastNDays: "최근 {n}일"
backToTitle: "타이틀로 가기"
hemisphere: "거주 지역"
withSensitive: "민감한 파일이 포함된 노트 보기"
userSaysSomethingSensitive: "{name} 같은 민감한 파일이 포함된 글"
userSaysSomethingSensitive: "{name}의 민감한 파일이 포함된 게시물"
enableHorizontalSwipe: "스와이프하여 탭 전환"
loading: "불러오는 중"
surrender: "그만두기"

View file

@ -299,7 +299,6 @@ enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ
enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ"
disablingTimelinesInfo: "ຜູ້ດູແລລະບບແລະຜູ້ຄວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍ່ຕາມ"
registration: "ລົງທະບຽນ"
enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່"
invite: "ເຊີນ"
driveCapacityPerLocalAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
driveCapacityPerRemoteAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ໄລຍະໄກ"

View file

@ -333,7 +333,6 @@ enableLocalTimeline: "Inschakelen lokale tijdlijn"
enableGlobalTimeline: "Inschakelen globale tijdlijn "
disablingTimelinesInfo: "Beheerders en moderators hebben altijd toegang tot alle tijdlijnen, ook als ze niet actief zijn."
registration: "Registreren"
enableRegistration: "Inschakelen registratie nieuwe gebruikers "
invite: "Uitnodigen"
driveCapacityPerLocalAccount: "Opslagruimte per lokale gebruiker"
driveCapacityPerRemoteAccount: "Opslagruimte per externe gebruiker"

View file

@ -260,7 +260,6 @@ enableLocalTimeline: "Aktiver lokal tidslinje"
enableGlobalTimeline: "Aktiver global tidslinje"
disablingTimelinesInfo: "Administratorer og Moderatorer vil alltid ha tilgang til alle tidslinjer, selv om de ikke er aktivert."
registration: "Registrer"
enableRegistration: "Aktiver registrering av nye brukere"
invite: "Inviter"
basicInfo: "Grunnleggende informasjon"
pinnedUsers: "Festede brukrere"

View file

@ -362,7 +362,6 @@ enableLocalTimeline: "Włącz lokalną oś czasu"
enableGlobalTimeline: "Włącz globalną oś czasu"
disablingTimelinesInfo: "Administratorzy i moderatorzy będą zawsze mieć dostęp do wszystkich osi czasu, nawet gdy są one wyłączone."
registration: "Zarejestruj się"
enableRegistration: "Włącz rejestrację nowych użytkowników"
invite: "Zaproś"
driveCapacityPerLocalAccount: "Powierzchnia dyskowa na lokalnego użytkownika"
driveCapacityPerRemoteAccount: "Powierzchnia dyskowa na zdalnego użytkownika"
@ -492,6 +491,10 @@ uiLanguage: "Język wyświetlania UI"
aboutX: "O {x}"
emojiStyle: "Styl emoji"
native: "Natywny"
menuStyle: "Styl Menu"
style: "Styl"
drawer: "Schowek"
popup: "Wyskakujące okienka"
showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszką"
showReactionsCount: "Wyświetl liczbę reakcji na notatkę"
noHistory: "Brak historii"
@ -574,6 +577,7 @@ ascendingOrder: "Rosnąco"
descendingOrder: "Malejąco"
scratchpad: "Brudnopis"
scratchpadDescription: "Brudnopis zawiera eksperymentalne środowisko dla AiScript. Możesz pisać, wykonywać i sprawdzać wyniki w interakcji z Misskey."
uiInspector: "Inspektor UI"
output: "Wyjście"
script: "Skrypt"
disablePagesScript: "Wyłącz AiScript na Stronach"
@ -654,6 +658,7 @@ smtpSecure: "Użyj niejawnego SSL/TLS dla połączeń SMTP"
smtpSecureInfo: "Wyłącz, jeżeli używasz STARTTLS"
testEmail: "Przetestuj dostarczanie wiadomości e-mail"
wordMute: "Wyciszenie słowa"
hardWordMute: "Wyciszaj przekleństwa"
regexpError: "Błąd wyrażenia regularnego"
regexpErrorDescription: "Wystąpił błąd w wyrażeniu regularnym w linii {line} twoich {tab} wyciszeń:"
instanceMute: "Wyciszone instancje"
@ -826,6 +831,7 @@ administration: "Zarządzanie"
accounts: "Konta"
switch: "Przełącz"
noMaintainerInformationWarning: "Informacje o administratorze nie są skonfigurowane."
noInquiryUrlWarning: "Adres URL zapytania nie został ustawiony"
noBotProtectionWarning: "Zabezpieczenie przed botami nie jest skonfigurowane."
configure: "Skonfiguruj"
postToGallery: "Opublikuj w galerii"
@ -890,6 +896,7 @@ followersVisibility: "Widoczność obserwujących"
continueThread: "Pokaż kontynuację wątku"
deleteAccountConfirm: "Spowoduje to nieodwracalne usunięcie Twojego konta. Kontynuować?"
incorrectPassword: "Nieprawidłowe hasło."
incorrectTotp: "Hasło pojedynczego użytku jest nie poprawne, lub straciło ważność"
voteConfirm: "Potwierdzić swój głos na \"{choice}\"?"
hide: "Ukryj"
useDrawerReactionPickerForMobile: "Wyświetlaj wybornik reakcji jako szufladę na urządzeniach mobilnych"
@ -914,6 +921,10 @@ oneHour: "1 godzina"
oneDay: "1 dzień"
oneWeek: "1 tydzień"
oneMonth: "jeden miesiąc"
threeMonths: "3 miesiące"
oneYear: "Rok"
threeDays: "3 dni"
reflectMayTakeTime: "Może minąć trochę czasu, zanim będzie to uwzględnione"
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
rateLimitExceeded: "Limit szybkości przekroczony"
cropImage: "Przytnij obraz"
@ -924,9 +935,11 @@ file: "Pliki"
recentNHours: "W ciągu ostatnich {n} godzin"
recentNDays: "W ciągu ostatnich {n} dni"
noEmailServerWarning: "Serwer Email nie jest skonfigurowany"
thereIsUnresolvedAbuseReportWarning: "Istnieją niewyjaśnione raporty"
recommended: "Zalecane"
check: "Zweryfikuj"
driveCapOverrideLabel: "Zmień limit pojemności dysku użytkownika"
driveCapOverrideCaption: "Resetuje pojemność do wartości domyślnej, przez wpisanie wartości 0 lub niższej"
requireAdminForView: "Aby to zobaczyć, musisz być administratorem"
isSystemAccount: "To jest konto stworzone i zarządzane przez system"
typeToConfirm: "Wprowadź {x}, aby potwierdzić"
@ -995,17 +1008,29 @@ unassign: "Cofnij przydzielenie"
color: "Kolor"
manageCustomEmojis: "Zarządzaj niestandardowymi Emoji"
manageAvatarDecorations: "Zarządzaj dekoracjami awatara"
youCannotCreateAnymore: "Limit kreacji został przekroczony"
cannotPerformTemporary: "Opcja tymczasowo niedostępna"
cannotPerformTemporaryDescription: "Ta akcja nie może zostać wykonana, z powodu przekroczenia limitu wykonań. Prosimy poczekać chwilę i spróbować ponownie"
invalidParamError: "Błąd parametrów"
invalidParamErrorDescription: "Wartości, które zostały podane są niepoprawne. Zwykle jest to spowodowane bugiem, lecz również może być to spowodowane przekroczeniem limitu wartości, lub podobnym problemem"
permissionDeniedError: "Odrzucono operacje"
permissionDeniedErrorDescription: "Konto nie posiada uprawnień"
preset: "Konfiguracja"
selectFromPresets: "Wybierz konfiguracje"
achievements: "Osiągnięcia"
gotInvalidResponseError: "Niepoprawna odpowiedź serwera"
gotInvalidResponseErrorDescription: "Wystąpił problem z Twoim połączeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce."
thisPostMayBeAnnoying: "Ten wpis może obrażać pozostałych użytkowników"
thisPostMayBeAnnoyingHome: "Opublikuj na domowej osi czasu"
thisPostMayBeAnnoyingCancel: "Odrzuć"
thisPostMayBeAnnoyingIgnore: "Zignoruj i wyślij"
collapseRenotes: "Zwiń wpisy, które już zobaczyłeś"
collapseRenotesDescription: "Zwiń wpisy, na które już zareagowałeś lub udostępniłeś"
internalServerError: "Wewnętrzny błąd serwera"
internalServerErrorDescription: "Niespodziewany błąd po stronie serwera"
copyErrorInfo: "Kopiuj informacje o błędzie"
joinThisServer: "Dołącz do chaty"
exploreOtherServers: "Szukaj innej instancji"
disableFederationOk: "Wyłącz federacje"
invitationRequiredToRegister: "Ten serwer wymaga zaproszenia. Tylko osoby z zaproszeniem mogą się zarejestrować"
emailNotSupported: "Wysyłanie wiadomości E-mail nie jest obsługiwane na tym serwerze"

View file

@ -376,7 +376,6 @@ enableLocalTimeline: "Ativar linha do tempo local"
enableGlobalTimeline: "Ativar linha do tempo global"
disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administradores e moderadores ainda poderão usá-las por conveniência."
registration: "Registar"
enableRegistration: "Permitir que qualquer pessoa se registre"
invite: "Convidar"
driveCapacityPerLocalAccount: "Capacidade do drive por usuário local"
driveCapacityPerRemoteAccount: "Capacidade do drive por usuário remoto"

View file

@ -341,7 +341,6 @@ enableLocalTimeline: "Activează cronologia locală"
enableGlobalTimeline: "Activeaza cronologia globală"
disablingTimelinesInfo: "Administratorii și Moderatorii vor avea mereu access la toate cronologiile, chiar dacă nu sunt activate."
registration: "Inregistrare"
enableRegistration: "Activează înregistrările pentru utilizatori noi"
invite: "Invită"
driveCapacityPerLocalAccount: "Capacitatea Drive-ului per utilizator local"
driveCapacityPerRemoteAccount: "Capacitatea Drive-ului per utilizator extern"

View file

@ -377,7 +377,6 @@ enableLocalTimeline: "Включить локальную ленту"
enableGlobalTimeline: "Включить глобальную ленту"
disablingTimelinesInfo: "У администраторов и модераторов есть доступ ко всем лентам, даже если они отключены."
registration: "Регистрация"
enableRegistration: "Разрешить регистрацию"
invite: "Пригласить"
driveCapacityPerLocalAccount: "Объём Диска на одного локального пользователя"
driveCapacityPerRemoteAccount: "Объём Диска на одного пользователя с другого экземпляра"

View file

@ -331,7 +331,6 @@ enableLocalTimeline: "Povoliť lokálnu časovú os"
enableGlobalTimeline: "Povoliť globálnu časovú os"
disablingTimelinesInfo: "Administrátori a moderátori majú vždy prístup ku všetkým časovým osiam, aj keď sú vypnuté."
registration: "Registrácia"
enableRegistration: "Povoliť registráciu nových používateľov"
invite: "Pozvať"
driveCapacityPerLocalAccount: "Kapacita disku pre používateľa"
driveCapacityPerRemoteAccount: "Kapacita disku pre vzdialeného používateľa"

View file

@ -333,7 +333,6 @@ disconnectService: "Koppla från"
enableLocalTimeline: "Aktivera lokal tidslinje"
enableGlobalTimeline: "Aktivera global tidslinje"
registration: "Registrera"
enableRegistration: "Aktivera registrering av nya användare"
invite: "Inbjudan"
inMb: "I megabyte"
bannerUrl: "URL till banner-bilden"
@ -481,6 +480,7 @@ nNotes: "{n} Noter"
backgroundColor: "Bakgrundsbild"
textColor: "Text"
saveAs: "Spara som..."
saveConfirm: "Spara ändringar?"
youAreRunningUpToDateClient: "Klienten du använder är uppdaterat."
newVersionOfClientAvailable: "Ny version av klienten är tillgänglig."
editCode: "Redigera kod"
@ -523,6 +523,7 @@ threeMonths: "3 månader"
oneYear: "1 år"
threeDays: "3 dagar"
file: "Filer"
deleteAccount: "Radera konto"
label: "Etikett"
cannotUploadBecauseNoFreeSpace: "Kan inte ladda upp filen för att det finns inget lagringsutrymme kvar."
cannotUploadBecauseExceedsFileSizeLimit: "Kan inte ladda upp filen för att den är större än filstorleksgränsen."
@ -575,9 +576,13 @@ _achievements:
_open3windows:
title: "Flera Fönster"
description: "Ha minst 3 fönster öppna samtidigt"
_role:
edit: "Redigera roll"
_ffVisibility:
public: "Publicera"
private: "Privat"
_accountDelete:
accountDelete: "Radera konto"
_ad:
back: "Tillbaka"
_gallery:
@ -587,6 +592,7 @@ _email:
title: "följde dig"
_aboutMisskey:
source: "Källkod"
projectMembers: "Projektmedlemmar"
_channel:
setBanner: "Välj banner"
removeBanner: "Ta bort banner"
@ -602,8 +608,17 @@ _theme:
_sfx:
note: "Noter"
notification: "Notifikationer"
_ago:
justNow: "Just nu"
_2fa:
step3Title: "Ange en autentiseringskod"
renewTOTPCancel: "Nej tack"
_permissions:
"read:reactions": "Visa dina reaktioner"
"write:reactions": "Redigera dina reaktioner"
"write:admin:delete-account": "Radera användarkonto"
"write:admin:roles": "Hantera roller"
"read:admin:roles": "Visa roller"
_antennaSources:
all: "Alla noter"
homeTimeline: "Noter från följda användare"
@ -666,6 +681,8 @@ _notification:
reply: "Svara"
renote: "Omnotera"
_deck:
addColumn: "Lägg till kolumn"
deleteProfile: "Radera profil"
_columns:
notifications: "Notifikationer"
tl: "Tidslinje"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ท
enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก"
disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม"
registration: "ลงทะเบียน"
enableRegistration: "เปิดใช้งานการลงทะเบียนผู้ใช้ใหม่"
invite: "คำเชิญ"
driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ท้องถิ่น"
driveCapacityPerRemoteAccount: "ความจุของไดรฟ์ต่อผู้ใช้ระยะไกล"

View file

@ -344,7 +344,6 @@ today: "Bugün"
monthX: "{month} ay"
pages: "Sayfalar"
integration: "Entegrasyon"
enableRegistration: "Kayıtlara izin ver"
basicInfo: "Temel bilgiler"
pinnedUsers: "Sabitlenmiş kullanıcılar"
pinnedNotes: "Sabitlenen"

View file

@ -334,7 +334,6 @@ enableLocalTimeline: "Увімкнути локальну стрічку"
enableGlobalTimeline: "Увімкнути глобальну стрічку"
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті."
registration: "Реєстрація"
enableRegistration: "Дозволити реєстрацію"
invite: "Запросити"
driveCapacityPerLocalAccount: "Об'єм диска на одного локального користувача"
driveCapacityPerRemoteAccount: "Об'єм диска на одного віддаленого користувача"

View file

@ -349,7 +349,6 @@ enableLocalTimeline: "Mahalliy vaqt mintaqasini yoqing"
enableGlobalTimeline: "Global vaqt mintaqasini yoqing"
disablingTimelinesInfo: "Administratorlar va Moderatorlar har doim barcha vaqt jadvallariga kirish huquqiga ega bo'ladilar, hatto ular yoqilmagan bo'lsa ham."
registration: "Ro'yxatdan o'tish"
enableRegistration: "Ro'yxatdan o'tishni yoqing"
invite: "Taklif qilish"
driveCapacityPerLocalAccount: "Har bir mahalliy foydalanuvchi uchun disk maydoni"
driveCapacityPerRemoteAccount: "Har bir masofaviy foydalanuvchi uchun disk maydoni"

View file

@ -357,7 +357,6 @@ enableLocalTimeline: "Bật bảng tin máy chủ"
enableGlobalTimeline: "Bật bảng tin liên hợp"
disablingTimelinesInfo: "Quản trị viên và Kiểm duyệt viên luôn có quyền truy cập mọi bảng tin, kể cả khi chúng không được bật."
registration: "Đăng ký"
enableRegistration: "Cho phép đăng ký mới"
invite: "Mời"
driveCapacityPerLocalAccount: "Dung lượng ổ đĩa tối đa cho mỗi người dùng"
driveCapacityPerRemoteAccount: "Dung lượng ổ đĩa tối đa cho mỗi người dùng từ xa"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "启用本地时间线"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和监察员也可以继续使用。"
registration: "注册"
enableRegistration: "允许任何人注册"
invite: "邀请"
driveCapacityPerLocalAccount: "每个用户的网盘容量"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
@ -587,6 +586,7 @@ masterVolume: "主音量"
notUseSound: "静音"
useSoundOnlyWhenActive: "仅在 Misskey 活跃时输出声音"
details: "详情"
renoteDetails: "转帖详情"
chooseEmoji: "选择表情符号"
unableToProcess: "操作无法完成"
recentUsed: "最近使用"
@ -1300,6 +1300,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需
lockdown: "锁定"
pleaseSelectAccount: "请选择帐户"
availableRoles: "可用角色"
acknowledgeNotesAndEnable: "理解注意事项后再开启。"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
@ -1456,6 +1457,8 @@ _serverSettings:
reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。"
inquiryUrl: "联络地址"
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
openRegistration: "开放注册"
openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
_accountMigration:
moveFrom: "从别的账号迁移到此账户"
@ -2738,3 +2741,6 @@ _selfXssPrevention:
description1: "如果在此处粘贴了什么,恶意用户可能会接管账户或者盗取个人资料。"
description2: "如果不能完全理解将要粘贴的内容,%c 请立即停止操作并关闭这个窗口。"
description3: "详情请看这里。{link}"
_followRequest:
recieved: "已收到申请"
sent: "已发送申请"

View file

@ -382,7 +382,6 @@ enableLocalTimeline: "啟用本地時間軸"
enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間軸功能,管理員和審查員仍可以繼續使用。"
registration: "註冊"
enableRegistration: "開放新使用者註冊"
invite: "邀請"
driveCapacityPerLocalAccount: "每個本地使用者的雲端硬碟容量"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
@ -1119,7 +1118,7 @@ vertical: "直向"
horizontal: "橫向"
position: "位置"
serverRules: "伺服器規則"
pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,請確認下列事項。"
pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,必須確認並同意以下內容。"
pleaseAgreeAllToContinue: "必須全部勾選「同意」才能繼續。"
continue: "繼續"
preservedUsernames: "保留的使用者名稱"
@ -1300,6 +1299,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登
lockdown: "鎖定"
pleaseSelectAccount: "請選擇帳戶"
availableRoles: "可用角色"
acknowledgeNotesAndEnable: "了解注意事項後再開啟。"
_accountSettings:
requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
@ -1456,6 +1456,8 @@ _serverSettings:
reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是Redis 記憶體使用量會增加。"
inquiryUrl: "聯絡表單網址"
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
openRegistration: "允許建立帳戶"
openRegistrationWarning: "開放註冊伴隨著風險。 建議只有在伺服器受到持續監控,並準備好在出現問題時能立即處理的情況下才開放註冊。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam如果一段期間內沒有偵測到審查員的活動此設定將自動關閉。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
@ -2738,3 +2740,6 @@ _selfXssPrevention:
description1: "如果您在此處貼上任何內容,惡意使用者可能會接管您的帳戶或竊取您的個人資訊。"
description2: "如果您不確切知道要貼上的內容,%c 請立即停止工作並關閉此視窗。"
description3: "細節請看這裡。{link}"
_followRequest:
recieved: "收到的請求"
sent: "送出的請求"

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.11.0-yumechinokuni.4p1",
"version": "2024.11.0-yumechinokuni.5",
"codename": "nasubi",
"repository": {
"type": "git",

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project and yumechi
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class IndexUserDeleted1732071810971 {
name = 'IndexUserDeleted1732071810971'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_199b79e682bdc5ba946f491686" ON "user" ("isDeleted")`);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_199b79e682bdc5ba946f491686"`);
}
}

View file

@ -154,9 +154,9 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
const convertedReports = abuseReports.map(it => {
return {
...it,
reporter: usersMap.get(it.reporterId),
targetUser: usersMap.get(it.targetUserId),
assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
reporter: usersMap.get(it.reporterId) ?? null,
targetUser: usersMap.get(it.targetUserId) ?? null,
assignee: it.assigneeId ? (usersMap.get(it.assigneeId) ?? null) : null,
};
});

View file

@ -36,7 +36,7 @@ import { GlobalEventService } from './GlobalEventService.js';
import { HashtagService } from './HashtagService.js';
import { HttpRequestService } from './HttpRequestService.js';
import { IdService } from './IdService.js';
import { ImageProcessingService } from './ImageProcessingService.js';
import { __YUME_PRIVATE_ImageProcessingService } from './ImageProcessingService.js';
import { InstanceActorService } from './InstanceActorService.js';
import { InternalStorageService } from './InternalStorageService.js';
import { MetaService } from './MetaService.js';
@ -67,7 +67,7 @@ import { UserMutingService } from './UserMutingService.js';
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
import { __YUME_PRIVATE_VideoProcessingService } from './VideoProcessingService.js';
import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
@ -179,7 +179,7 @@ const $GlobalEventService: Provider = { provide: 'GlobalEventService', useExisti
const $HashtagService: Provider = { provide: 'HashtagService', useExisting: HashtagService };
const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService };
const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
const $ImageProcessingService: Provider = { provide: '__YUME_PRIVATE_ImageProcessingService', useExisting: __YUME_PRIVATE_ImageProcessingService };
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
@ -212,7 +212,7 @@ const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService',
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
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: '__YUME_PRIVATE_VideoProcessingService', useExisting: __YUME_PRIVATE_VideoProcessingService };
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
HashtagService,
HttpRequestService,
IdService,
ImageProcessingService,
__YUME_PRIVATE_ImageProcessingService,
InstanceActorService,
InternalStorageService,
MetaService,
@ -363,7 +363,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
__YUME_PRIVATE_VideoProcessingService,
UserWebhookService,
SystemWebhookService,
WebhookTestService,
@ -625,7 +625,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
HashtagService,
HttpRequestService,
IdService,
ImageProcessingService,
__YUME_PRIVATE_ImageProcessingService,
InstanceActorService,
InternalStorageService,
MetaService,
@ -658,7 +658,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
__YUME_PRIVATE_VideoProcessingService,
UserWebhookService,
SystemWebhookService,
WebhookTestService,

View file

@ -47,6 +47,10 @@ export class DeleteAccountService {
});
}
if (!(await this.usersRepository.update({ id: user.id, isDeleted: false }, { isDeleted: true })).affected) {
return;
}
// 物理削除する前にDelete activityを送信する
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信

View file

@ -22,8 +22,8 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { contentDisposition } from '@/misc/content-disposition.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { ImageProcessingService } from '@/core/ImageProcessingService.js';
import { __YUME_PRIVATE_VideoProcessingService } from '@/core/VideoProcessingService.js';
import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js';
import type { IImage } from '@/core/ImageProcessingService.js';
import { QueueService } from '@/core/QueueService.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
@ -120,8 +120,8 @@ export class DriveService {
private downloadService: DownloadService,
private internalStorageService: InternalStorageService,
private s3Service: S3Service,
private imageProcessingService: ImageProcessingService,
private videoProcessingService: VideoProcessingService,
private privateImageProcessingService: __YUME_PRIVATE_ImageProcessingService,
private privateVideoProcessingService: __YUME_PRIVATE_VideoProcessingService,
private globalEventService: GlobalEventService,
private queueService: QueueService,
private roleService: RoleService,
@ -277,7 +277,7 @@ export class DriveService {
}
try {
const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path);
const thumbnail = await this.privateVideoProcessingService.generateVideoThumbnail(path);
return {
webpublic: null,
thumbnail,
@ -331,9 +331,9 @@ export class DriveService {
try {
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
webpublic = await this.privateImageProcessingService.convertSharpToWebp(img, 2048, 2048);
} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
webpublic = await this.privateImageProcessingService.convertSharpToPng(img, 2048, 2048);
} else {
this.registerLogger.debug('web image not created (not an required image)');
}
@ -352,9 +352,9 @@ export class DriveService {
try {
if (isAnimated) {
thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
thumbnail = await this.privateImageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
} else {
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
thumbnail = await this.privateImageProcessingService.convertSharpToWebp(img, 498, 422);
}
} catch (err) {
this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error);

View file

@ -46,7 +46,9 @@ import { bindThis } from '@/decorators.js';
import { Readable } from 'node:stream';
@Injectable()
export class ImageProcessingService {
// Prevent accidental import by upstream merge
// eslint-disable-next-line
export class __YUME_PRIVATE_ImageProcessingService {
constructor(
) {
}

View file

@ -7,13 +7,15 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
import { type SystemWebhookPayload } from '@/core/SystemWebhookService.js';
import { type UserWebhookPayload } from './UserWebhookService.js';
import type {
DbJobData,
DeliverJobData,
@ -30,13 +32,11 @@ import type {
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
UserWebhookDeliverQueue,
} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import type { Packed } from '@/misc/json-schema.js';
import { type UserWebhookPayload } from './UserWebhookService.js';
@Injectable()
export class QueueService {
@ -502,10 +502,10 @@ export class QueueService {
* @see SystemWebhookDeliverProcessorService
*/
@bindThis
public systemWebhookDeliver(
public systemWebhookDeliver<T extends SystemWebhookEventType>(
webhook: MiSystemWebhook,
type: SystemWebhookEventType,
content: unknown,
type: T,
content: SystemWebhookPayload<T>,
opts?: { attempts?: number },
) {
const data: SystemWebhookDeliverJobData = {

View file

@ -18,6 +18,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { bindThis } from '@/decorators.js';
import { ApResolverService } from './activitypub/ApResolverService.js';
@Injectable()
export class RemoteUserResolveService {
@ -35,6 +36,7 @@ export class RemoteUserResolveService {
private remoteLoggerService: RemoteLoggerService,
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
private apResolverService: ApResolverService,
) {
this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user');
}
@ -91,7 +93,7 @@ export class RemoteUserResolveService {
}
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href);
return await this.apPersonService.createPerson(self.href, this.apResolverService.createResolver());
}
// ユーザー情報が古い場合は、WebFingerからやりなおして返す

View file

@ -488,6 +488,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return ids.length > 0
? await this.usersRepository.findBy({
id: In(ids),
isDeleted: false,
})
: [];
}

View file

@ -15,8 +15,39 @@ import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
import { Packed } from '@/misc/json-schema.js';
import { AbuseReportResolveType } from '@/models/AbuseUserReport.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
export type AbuseReportPayload = {
id: string;
targetUserId: string;
targetUser: Packed<'UserLite'> | null;
targetUserHost: string | null;
reporterId: string;
reporter: Packed<'UserLite'> | null;
reporterHost: string | null;
assigneeId: string | null;
assignee: Packed<'UserLite'> | null;
resolved: boolean;
forwarded: boolean;
comment: string;
moderationNote: string;
resolvedAs: AbuseReportResolveType | null;
};
export type InactiveModeratorsWarningPayload = {
remainingTime: ModeratorInactivityRemainingTime;
};
export type SystemWebhookPayload<T extends SystemWebhookEventType> =
T extends 'abuseReport' | 'abuseReportResolved' ? AbuseReportPayload :
T extends 'userCreated' ? Packed<'UserLite'> :
T extends 'inactiveModeratorsWarning' ? InactiveModeratorsWarningPayload :
T extends 'inactiveModeratorsInvitationOnlyChanged' ? Record<string, never> :
never;
@Injectable()
export class SystemWebhookService implements OnApplicationShutdown {
private logger: Logger;
@ -168,7 +199,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
webhook: MiSystemWebhook | MiSystemWebhook['id'],
type: T,
content: unknown,
content: SystemWebhookPayload<T>,
) {
const webhookEntity = typeof webhook === 'string'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)

View file

@ -7,19 +7,21 @@ import { Inject, Injectable } from '@nestjs/common';
import FFmpeg from 'fluent-ffmpeg';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { ImageProcessingService } from '@/core/ImageProcessingService.js';
import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js';
import type { IImage } from '@/core/ImageProcessingService.js';
import { createTempDir } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
@Injectable()
export class VideoProcessingService {
// Prevent accidental import by upstream merge
// eslint-disable-next-line
export class __YUME_PRIVATE_VideoProcessingService {
constructor(
@Inject(DI.config)
private config: Config,
private imageProcessingService: ImageProcessingService,
private imageProcessingService: __YUME_PRIVATE_ImageProcessingService,
) {
}

View file

@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common';
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js';
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
@ -16,13 +16,7 @@ import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModera
const oneDayMillis = 24 * 60 * 60 * 1000;
type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
targetUser: Packed<'UserLite'> | null,
reporter: Packed<'UserLite'> | null,
assignee: Packed<'UserLite'> | null,
};
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
@ -407,7 +401,8 @@ export class WebhookTestService {
break;
}
// まだ実装されていない (#9485)
case 'reaction': return;
case 'reaction':
return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
@ -425,10 +420,10 @@ export class WebhookTestService {
* - on
*/
@bindThis
public async testSystemWebhook(
public async testSystemWebhook<T extends SystemWebhookEventType>(
params: {
webhookId: MiSystemWebhook['id'],
type: SystemWebhookEventType,
type: T,
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
},
) {
@ -438,7 +433,7 @@ export class WebhookTestService {
}
const webhook = webhooks[0];
const send = (contents: unknown) => {
const send = <U extends SystemWebhookEventType>(type: U, contents: SystemWebhookPayload<U>) => {
const merged = {
...webhook,
...params.override,
@ -446,12 +441,12 @@ export class WebhookTestService {
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ.
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
this.queueService.systemWebhookDeliver(merged, type, contents, { attempts: 1 });
};
switch (params.type) {
case 'abuseReport': {
send(generateAbuseReport({
send('abuseReport', generateAbuseReport({
targetUserId: dummyUser1.id,
targetUser: dummyUser1,
reporterId: dummyUser2.id,
@ -460,7 +455,7 @@ export class WebhookTestService {
break;
}
case 'abuseReportResolved': {
send(generateAbuseReport({
send('abuseReportResolved', generateAbuseReport({
targetUserId: dummyUser1.id,
targetUser: dummyUser1,
reporterId: dummyUser2.id,
@ -472,7 +467,7 @@ export class WebhookTestService {
break;
}
case 'userCreated': {
send(toPackedUserLite(dummyUser1));
send('userCreated', toPackedUserLite(dummyUser1));
break;
}
case 'inactiveModeratorsWarning': {
@ -482,15 +477,20 @@ export class WebhookTestService {
asHours: 24,
};
send({
send('inactiveModeratorsWarning', {
remainingTime: dummyTime,
});
break;
}
case 'inactiveModeratorsInvitationOnlyChanged': {
send({});
send('inactiveModeratorsInvitationOnlyChanged', {});
break;
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
}
}
}

View file

@ -29,7 +29,7 @@ import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost, yumeDowncastAccept, yumeDowncastAdd, yumeDowncastAnnounce, yumeDowncastBlock, yumeDowncastCreate, yumeDowncastDelete, yumeDowncastFlag, yumeDowncastFollow, yumeDowncastLike, yumeDowncastMove, yumeDowncastReject, yumeDowncastRemove, yumeDowncastUndo, yumeDowncastUpdate } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js';
@ -138,52 +138,92 @@ export class ApInboxService {
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
if (actor.isSuspended) return;
if (isCreate(activity)) {
const create = yumeDowncastCreate(activity);
if (create) {
mInboxReceived?.inc({ host: actor.host, type: 'create' });
return await this.create(actor, activity);
} else if (isDelete(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'update' });
return await this.update(actor, activity);
} else if (isFollow(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'follow' });
return await this.follow(actor, activity);
} else if (isAccept(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
return await this.accept(actor, activity);
} else if (isReject(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
return await this.reject(actor, activity);
} else if (isAdd(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'add' });
return await this.add(actor, activity);
} else if (isRemove(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
return await this.announce(actor, activity);
} else if (isLike(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'like' });
return await this.like(actor, activity);
} else if (isUndo(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'undo' });
return await this.undo(actor, activity);
} else if (isBlock(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'block' });
return await this.block(actor, activity);
} else if (isFlag(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'flag' });
return await this.flag(actor, activity);
} else if (isMove(activity)) {
mInboxReceived?.inc({ host: actor.host, type: 'move' });
return await this.move(actor, activity);
} else {
mInboxReceived?.inc({ host: actor.host, type: 'unknown' });
return `unrecognized activity type: ${activity.type}`;
return await this.create(actor, create);
}
const update = yumeDowncastUpdate(activity);
if (update) {
mInboxReceived?.inc({ host: actor.host, type: 'update' });
return await this.update(actor, update);
}
const del = yumeDowncastDelete(activity);
if (del) {
mInboxReceived?.inc({ host: actor.host, type: 'delete' });
return await this.delete(actor, del);
}
const follow = yumeDowncastFollow(activity);
if (follow) {
mInboxReceived?.inc({ host: actor.host, type: 'follow' });
return await this.follow(actor, follow);
}
const accept = yumeDowncastAccept(activity);
if (accept) {
mInboxReceived?.inc({ host: actor.host, type: 'accept' });
return await this.accept(actor, accept);
}
const reject = yumeDowncastReject(activity);
if (reject) {
mInboxReceived?.inc({ host: actor.host, type: 'reject' });
return await this.reject(actor, reject);
}
const add = yumeDowncastAdd(activity);
if (add) {
mInboxReceived?.inc({ host: actor.host, type: 'add' });
return await this.add(actor, add);
}
const remove = yumeDowncastRemove(activity);
if (remove) {
mInboxReceived?.inc({ host: actor.host, type: 'remove' });
return await this.remove(actor, remove);
}
const announce = yumeDowncastAnnounce(activity);
if (announce) {
mInboxReceived?.inc({ host: actor.host, type: 'announce' });
return await this.announce(actor, announce);
}
const like = yumeDowncastLike(activity);
if (like) {
mInboxReceived?.inc({ host: actor.host, type: 'like' });
return await this.like(actor, like);
}
const move = yumeDowncastMove(activity);
if (move) {
mInboxReceived?.inc({ host: actor.host, type: 'move' });
return await this.move(actor, move);
}
const undo = yumeDowncastUndo(activity);
if (undo) {
mInboxReceived?.inc({ host: actor.host, type: 'undo' });
return await this.undo(actor, undo);
}
const block = yumeDowncastBlock(activity);
if (block) {
mInboxReceived?.inc({ host: actor.host, type: 'block' });
return await this.block(actor, block);
}
const flag = yumeDowncastFlag(activity);
if (flag) {
mInboxReceived?.inc({ host: actor.host, type: 'flag' });
return await this.flag(actor, flag);
}
mInboxReceived?.inc({ host: actor.host, type: 'unknown' });
return `unrecognized activity type: ${activity.type}`;
}
@bindThis
@ -509,19 +549,12 @@ export class ApInboxService {
return `skip: delete actor ${actor.uri} !== ${uri}`;
}
const user = await this.usersRepository.findOneBy({ id: actor.id });
if (user == null) {
return 'skip: actor not found';
} else if (user.isDeleted) {
if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
return 'skip: already deleted';
}
const job = await this.queueService.createDeleteAccountJob(actor);
await this.usersRepository.update(actor.id, {
isDeleted: true,
});
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
return `ok: queued ${job.name} ${job.id}`;

View file

@ -30,7 +30,7 @@ import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import { markOutgoing, type IAccept, type IActivity, type IAdd, type IAnnounce, type IApDocument, type IApEmoji, type IApHashtag, type IApImage, type IApMention, type IBlock, type ICreate, type IDelete, type IFlag, type IFollow, type IKey, type ILike, type IMove, type IObject, type IPost, type IQuestion, type IReject, type IRemove, type ITombstone, type IUndo, type IUpdate } from './type.js';
@Injectable()
export class ApRendererService {
@ -66,21 +66,21 @@ export class ApRendererService {
@bindThis
public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept {
return {
return markOutgoing({
type: 'Accept',
actor: this.userEntityService.genLocalUserUri(user.id),
object,
};
}, undefined);
}
@bindThis
public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
return {
return markOutgoing({
type: 'Add',
actor: this.userEntityService.genLocalUserUri(user.id),
target,
object,
};
}, undefined);
}
@bindThis
@ -103,7 +103,7 @@ export class ApRendererService {
throw new Error('renderAnnounce: cannot render non-public note');
}
return {
return markOutgoing({
id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Announce',
@ -111,7 +111,7 @@ export class ApRendererService {
to,
cc,
object,
};
}, undefined);
}
/**
@ -125,23 +125,23 @@ export class ApRendererService {
throw new Error('renderBlock: missing blockee uri');
}
return {
return markOutgoing({
type: 'Block',
id: `${this.config.url}/blocks/${block.id}`,
actor: this.userEntityService.genLocalUserUri(block.blockerId),
object: block.blockee.uri,
};
}, undefined);
}
@bindThis
public renderCreate(object: IObject, note: MiNote): ICreate {
const activity: ICreate = {
const activity: ICreate = markOutgoing({
id: `${this.config.url}/notes/${note.id}/activity`,
actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Create',
published: this.idService.parse(note.id).date.toISOString(),
object,
};
}, undefined);
if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc;
@ -151,28 +151,28 @@ export class ApRendererService {
@bindThis
public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete {
return {
return markOutgoing({
type: 'Delete',
actor: this.userEntityService.genLocalUserUri(user.id),
object,
published: new Date().toISOString(),
};
}, undefined);
}
@bindThis
public renderDocument(file: MiDriveFile): IApDocument {
return {
return markOutgoing({
type: 'Document',
mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment,
sensitive: file.isSensitive,
};
}, undefined);
}
@bindThis
public renderEmoji(emoji: MiEmoji): IApEmoji {
return {
return markOutgoing( {
id: `${this.config.url}/emojis/${emoji.name}`,
type: 'Emoji',
name: `:${emoji.name}:`,
@ -183,28 +183,28 @@ export class ApRendererService {
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
},
};
}, undefined);
}
// to anonymise reporters, the reporting actor must be a system user
@bindThis
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag {
return {
return markOutgoing({
type: 'Flag',
actor: this.userEntityService.genLocalUserUri(user.id),
content,
object,
};
}, undefined);
}
@bindThis
public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow {
return {
return markOutgoing({
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
type: 'Follow',
actor: this.userEntityService.genLocalUserUri(relayActor.id),
object: 'https://www.w3.org/ns/activitystreams#Public',
};
}, undefined);
}
/**
@ -223,36 +223,36 @@ export class ApRendererService {
followee: MiPartialLocalUser | MiPartialRemoteUser,
requestId?: string,
): IFollow {
return {
return markOutgoing({
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow',
actor: this.userEntityService.getUserUri(follower),
object: this.userEntityService.getUserUri(followee),
};
}, undefined);
}
@bindThis
public renderHashtag(tag: string): IApHashtag {
return {
return markOutgoing({
type: 'Hashtag',
href: `${this.config.url}/tags/${encodeURIComponent(tag)}`,
name: `#${tag}`,
};
}, undefined);
}
@bindThis
public renderImage(file: MiDriveFile): IApImage {
return {
return markOutgoing({
type: 'Image',
url: this.driveFileEntityService.getPublicUrl(file),
sensitive: file.isSensitive,
name: file.comment,
};
}, undefined);
}
@bindThis
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
return {
return markOutgoing({
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
type: 'Key',
owner: this.userEntityService.genLocalUserUri(user.id),
@ -260,21 +260,21 @@ export class ApRendererService {
type: 'spki',
format: 'pem',
}),
};
}, undefined);
}
@bindThis
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> {
const reaction = noteReaction.reaction;
const object: ILike = {
const object: ILike = markOutgoing({
type: 'Like',
id: `${this.config.url}/likes/${noteReaction.id}`,
actor: `${this.config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
content: reaction,
_misskey_reaction: reaction,
};
}, undefined);
if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', '');
@ -288,11 +288,11 @@ export class ApRendererService {
@bindThis
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
return {
return markOutgoing({
type: 'Mention',
href: this.userEntityService.getUserUri(mention),
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
};
}, undefined);
}
@bindThis
@ -302,13 +302,13 @@ export class ApRendererService {
): IMove {
const actor = this.userEntityService.getUserUri(src);
const target = this.userEntityService.getUserUri(dst);
return {
return markOutgoing({
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor,
type: 'Move',
object: actor,
target,
};
}, undefined);
}
@bindThis
@ -422,7 +422,7 @@ export class ApRendererService {
})),
} as const : {};
return {
return markOutgoing({
id: `${this.config.url}/notes/${note.id}`,
type: 'Note',
attributedTo,
@ -445,7 +445,7 @@ export class ApRendererService {
sensitive: note.cw != null || files.some(file => file.isSensitive),
tag,
...asPoll,
};
}, undefined);
}
@bindThis
@ -529,7 +529,7 @@ export class ApRendererService {
@bindThis
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
return {
return markOutgoing({
type: 'Question',
id: `${this.config.url}/questions/${note.id}`,
actor: this.userEntityService.genLocalUserUri(user.id),
@ -542,78 +542,78 @@ export class ApRendererService {
totalItems: poll.votes[i],
},
})),
};
}, 'question');
}
@bindThis
public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject {
return {
return markOutgoing({
type: 'Reject',
actor: this.userEntityService.genLocalUserUri(user.id),
object,
};
}, undefined);
}
@bindThis
public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
return {
return markOutgoing({
type: 'Remove',
actor: this.userEntityService.genLocalUserUri(user.id),
target,
object,
};
}, undefined);
}
@bindThis
public renderTombstone(id: string): ITombstone {
return {
return markOutgoing({
id,
type: 'Tombstone',
};
}, undefined);
}
@bindThis
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
return {
return markOutgoing({
type: 'Undo',
...(id ? { id } : {}),
actor: this.userEntityService.genLocalUserUri(user.id),
object,
published: new Date().toISOString(),
};
}, undefined);
}
@bindThis
public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate {
return {
return markOutgoing( {
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
actor: this.userEntityService.genLocalUserUri(user.id),
type: 'Update',
to: ['https://www.w3.org/ns/activitystreams#Public'],
object,
published: new Date().toISOString(),
};
}, undefined);
}
@bindThis
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
return {
return markOutgoing({
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
actor: this.userEntityService.genLocalUserUri(user.id),
type: 'Create',
to: [pollOwner.uri],
published: new Date().toISOString(),
object: {
object: markOutgoing({
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
type: 'Note',
attributedTo: this.userEntityService.genLocalUserUri(user.id),
to: [pollOwner.uri],
inReplyTo: note.uri,
name: poll.choices[vote.choice],
},
};
}, undefined),
}, undefined);
}
@bindThis

View file

@ -16,11 +16,11 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { isCollectionOrOrderedCollection, yumeNormalizeObject } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js';
import type { IObject, ICollection, IOrderedCollection, IUnsanitizedObject } from './type.js';
export class Resolver {
private history: Set<string>;
@ -67,7 +67,7 @@ export class Resolver {
}
@bindThis
public async resolve(value: string | IObject): Promise<IObject> {
public async resolveNotNormalized(value: string | IObject): Promise<IUnsanitizedObject> {
if (typeof value !== 'string') {
return value;
}
@ -117,6 +117,13 @@ export class Resolver {
return object;
}
@bindThis
public async resolve(value: string | IObject): Promise<IObject> {
const object = await this.resolveNotNormalized(value);
return yumeNormalizeObject(object);
}
@bindThis
private resolveLocal(url: string): Promise<IObject> {
const parsed = this.apDbResolverService.parseUri(url);

View file

@ -115,10 +115,7 @@ export class ApNoteService {
* Noteを作成します
*/
@bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
public async createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<MiNote | null> {
const object = await resolver.resolve(value);
const entryUri = getApId(value);
@ -356,7 +353,7 @@ export class ApNoteService {
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
return await this.createNote(createFrom, options.resolver, true);
return await this.createNote(createFrom, this.apResolverService.createResolver(), true);
} finally {
unlock();
}

View file

@ -277,16 +277,13 @@ export class ApPersonService implements OnModuleInit {
* Personを作成します
*/
@bindThis
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
public async createPerson(uri: string, resolver: Resolver): Promise<MiRemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
if (uri.startsWith(this.config.url)) {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
}
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
@ -557,7 +554,9 @@ export class ApPersonService implements OnModuleInit {
if (moving) updates.movedAt = new Date();
// Update user
await this.usersRepository.update(exist.id, updates);
if (!(await this.usersRepository.update({ id: exist.id, isDeleted: false }, updates)).affected) {
return 'skip';
}
if (person.publicKey) {
await this.userPublickeysRepository.update({ userId: exist.id }, {
@ -662,7 +661,7 @@ export class ApPersonService implements OnModuleInit {
@bindThis
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false });
if (!this.userEntityService.isRemoteUser(user)) return;
if (!user.featured) return;

View file

@ -3,10 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { toASCII } from "node:punycode";
export type Obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[];
export interface IObject {
export interface IUnsanitizedObject {
'@context'?: string | string[] | Obj | Obj[];
type: string | string[];
id?: string;
@ -36,6 +38,83 @@ export interface IObject {
sensitive?: boolean;
}
export interface IObject extends IUnsanitizedObject {
__yume_normalized_object: true | 'outgoing';
};
export interface YumeDowncastSanitizedBadge<L extends 'question' | undefined> {
__yume_normalized_badge: L | 'outgoing';
};
export function markOutgoing<T, L extends 'question' | undefined>(object: T, _badge: L): T & IObject & YumeDowncastSanitizedBadge<L> {
return object as T & IObject & YumeDowncastSanitizedBadge<L>;
}
export function yumeNormalizeURL(url: string): string {
const u = new URL(url);
u.hash = '';
u.host = toASCII(u.host);
if (u.protocol && u.protocol !== 'https:') {
throw new Error('protocol is not https');
}
u.protocol = 'https:';
if (u.port && u.port !== '443') {
throw new Error('port is not 443');
}
return u.toString();
}
export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (IUnsanitizedObject | string)[]>(object: O, depth = 0):
IObject | string | (IObject | string)[] {
if (depth > 16) {
throw new Error('recursion limit exceeded');
}
if (typeof object === 'string') {
return yumeNormalizeURL(object);
}
if (Array.isArray(object)) {
if (object.length > 64) {
throw new Error('array length limit exceeded');
}
return object.flatMap(yumeNormalizeRecursive);
}
return yumeNormalizeObject(object);
}
export function yumeNormalizeObject(object: IUnsanitizedObject): IObject {
if (object.cc) {
object.cc = yumeNormalizeRecursive(object.cc);
}
if (object.to) {
object.to = yumeNormalizeRecursive(object.to);
}
if (object.attributedTo) {
object.attributedTo = yumeNormalizeRecursive(object.attributedTo);
}
if (object.id) {
object.id = yumeNormalizeURL(object.id);
}
if (object.url) {
object.url = yumeNormalizeRecursive(object.url);
}
if (object.attachment) {
object.attachment = object.attachment.map(yumeNormalizeRecursive);
}
if (object.inReplyTo) {
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo);
}
return {
...object,
__yume_normalized_object: true,
};
}
/**
* Get array of ActivityStreams Objects id
*/
@ -80,7 +159,7 @@ export function getOneApHrefNullable(value: ApObject | undefined): string | unde
}
export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
if (typeof value === 'string') return value;
if (typeof value === 'string') return value;
if (typeof value?.href === 'string') return value.href;
return undefined;
}
@ -122,7 +201,7 @@ export const isPost = (object: IObject): object is IPost => {
return type != null && validPost.includes(type);
};
export interface IPost extends IObject {
export interface IPost extends IObject{
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
source?: {
content: string;
@ -133,7 +212,7 @@ export interface IPost extends IObject {
quoteUrl?: string;
}
export interface IQuestion extends IObject {
export interface IUnsanitizedQuestion extends IObject {
type: 'Note' | 'Question';
actor: string;
source?: {
@ -148,7 +227,25 @@ export interface IQuestion extends IObject {
closed?: Date;
}
export const isQuestion = (object: IObject): object is IQuestion =>
export interface IQuestion extends IUnsanitizedQuestion, YumeDowncastSanitizedBadge<'question'> {}
export function yumeSanitizeQuestion(object: IUnsanitizedQuestion): IQuestion {
return {
type: object.type,
actor: yumeNormalizeURL(object.actor),
source: object.source,
_misskey_quote: object._misskey_quote,
quoteUrl: object.quoteUrl ? yumeNormalizeURL(object.quoteUrl) : '',
oneOf: object.oneOf,
anyOf: object.anyOf,
endTime: object.endTime,
closed: object.closed,
__yume_normalized_object: true,
__yume_normalized_badge: 'question',
};
}
export const isQuestion = (object: IObject): object is IUnsanitizedQuestion =>
getApType(object) === 'Note' || getApType(object) === 'Question';
interface IQuestionChoice {
@ -349,3 +446,185 @@ export const isBlock = (object: IObject): object is IBlock => getApType(object)
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
export function yumeDowncastCreate(object: IObject): ICreate | null {
if (getApType(object) !== 'Create') return null;
const obj = object as ICreate;
return {
type: 'Create',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastDelete(object: IObject): IDelete | null {
if (getApType(object) !== 'Delete') return null;
const obj = object as IDelete;
return {
type: 'Delete',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
if (getApType(object) !== 'Update') return null;
const obj = object as IUpdate;
return {
type: 'Update',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastRead(object: IObject): IRead | null {
if (getApType(object) !== 'Read') return null;
const obj = object as IRead;
return {
type: 'Read',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastUndo(object: IObject): IUndo | null {
if (getApType(object) !== 'Undo') return null;
const obj = object as IUndo;
return {
type: 'Undo',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastFollow(object: IObject): IFollow | null {
if (getApType(object) !== 'Follow') return null;
const obj = object as IFollow;
return {
type: 'Follow',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastAccept(object: IObject): IAccept | null {
if (getApType(object) !== 'Accept') return null;
const obj = object as IAccept;
return {
type: 'Accept',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastReject(object: IObject): IReject | null {
if (getApType(object) !== 'Reject') return null;
const obj = object as IReject;
return {
type: 'Reject',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastAdd(object: IObject): IAdd | null {
if (getApType(object) !== 'Add') return null;
const obj = object as IAdd;
return {
type: 'Add',
actor: obj.actor,
object: obj.object,
target: obj.target,
__yume_normalized_object: true,
};
}
export function yumeDowncastRemove(object: IObject): IRemove | null {
if (getApType(object) !== 'Remove') return null;
const obj = object as IRemove;
return {
type: 'Remove',
actor: obj.actor,
object: obj.object,
target: obj.target,
__yume_normalized_object: true,
};
}
export function yumeDowncastLike(object: IObject): ILike | null {
if (getApType(object) !== 'Like') return null;
const obj = object as ILike;
return {
type: 'Like',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
if (getApType(object) !== 'Announce') return null;
const obj = object as IAnnounce;
return {
type: 'Announce',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastBlock(object: IObject): IBlock | null {
if (getApType(object) !== 'Block') return null;
const obj = object as IBlock;
return {
type: 'Block',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastFlag(object: IObject): IFlag | null {
if (getApType(object) !== 'Flag') return null;
const obj = object as IFlag;
return {
type: 'Flag',
actor: obj.actor,
object: obj.object,
__yume_normalized_object: true,
};
}
export function yumeDowncastMove(object: IObject): IMove | null {
if (getApType(object) !== 'Move') return null;
const obj = object as IMove;
return {
type: 'Move',
actor: obj.actor,
object: obj.object,
target: obj.target,
__yume_normalized_object: true,
};
}
export function yumeDowncastMention(object: IObject): IApMention {
if (getApType(object) !== 'Mention') {
throw new Error('not a mention');
}
const href = getApHrefNullable(object);
return {
...object,
type: 'Mention',
href: href ? yumeNormalizeURL(href) : '',
name: object.name ?? '',
};
}

View file

@ -18,7 +18,6 @@ import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
@ -43,7 +42,6 @@ export class DriveFileEntityService {
private utilityService: UtilityService,
private driveFolderEntityService: DriveFolderEntityService,
private videoProcessingService: VideoProcessingService,
private idService: IdService,
) {
}
@ -86,11 +84,7 @@ export class DriveFileEntityService {
@bindThis
public getThumbnailUrl(file: MiDriveFile): string | null {
if (file.type.startsWith('video')) {
if (file.thumbnailUrl) return file.thumbnailUrl;
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url);
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
// 動画ではなくリモートかつメディアプロキシ
return this.getProxiedUrl(file.uri, 'static');
}

View file

@ -10,46 +10,85 @@ import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiEmoji } from '@/models/Emoji.js';
import { bindThis } from '@/decorators.js';
import { In } from 'typeorm';
import type { Config } from '@/config.js';
@Injectable()
export class EmojiEntityService {
constructor(
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.config)
private config: Config,
) {
}
private stripProxyIfOrigin(url: string): string {
try {
const u = new URL(url);
let origin = u.origin;
if (u.origin === new URL(this.config.mediaProxy).origin) {
const innerUrl = u.searchParams.get('url');
if (innerUrl) {
origin = new URL(innerUrl).origin;
}
}
if (origin === u.origin) {
return url;
}
} catch (e) {
return url;
}
return url;
}
@bindThis
public packSimpleNoQuery(
emoji: MiEmoji,
): Packed<'EmojiSimple'> {
return {
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
};
}
@bindThis
public async packSimple(
src: MiEmoji['id'] | MiEmoji,
): Promise<Packed<'EmojiSimple'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
return {
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
};
return this.packSimpleNoQuery(emoji);
}
@bindThis
public packSimpleMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.packSimple(x)));
public async packSimpleMany(
emojis: MiEmoji['id'][] | MiEmoji[],
): Promise<Packed<'EmojiSimple'>[]> {
if (emojis.length === 0) {
return [];
}
if (typeof emojis[0] === 'string') {
const res = await this.emojisRepository.findBy({ id: In(emojis as MiEmoji['id'][]) });
return res.map(this.packSimpleNoQuery);
}
return (emojis as MiEmoji[]).map(this.packSimpleNoQuery);
}
@bindThis
public async packDetailed(
src: MiEmoji['id'] | MiEmoji,
): Promise<Packed<'EmojiDetailed'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
public packDetailedNoQuery(
emoji: MiEmoji,
): Packed<'EmojiDetailed'> {
return {
id: emoji.id,
aliases: emoji.aliases,
@ -57,7 +96,7 @@ export class EmojiEntityService {
category: emoji.category,
host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
license: emoji.license,
isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly,
@ -66,10 +105,28 @@ export class EmojiEntityService {
}
@bindThis
public packDetailedMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.packDetailed(x)));
public async packDetailed(
src: MiEmoji['id'] | MiEmoji,
): Promise<Packed<'EmojiDetailed'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
return this.packDetailedNoQuery(emoji);
}
@bindThis
public async packDetailedMany(
emojis: MiEmoji['id'][] | MiEmoji[],
) : Promise<Packed<'EmojiDetailed'>[]> {
if (emojis.length === 0) {
return [];
}
if (typeof emojis[0] === 'string') {
const res = await this.emojisRepository.findBy({ id: In(emojis as MiEmoji['id'][]) });
return res.map(this.packDetailedNoQuery);
}
return (emojis as MiEmoji[]).map(this.packDetailedNoQuery);
}
}

View file

@ -7,6 +7,8 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
export type AbuseReportResolveType = 'accept' | 'reject';
@Entity('abuse_user_report')
export class MiAbuseUserReport {
@PrimaryColumn(id())
@ -76,7 +78,7 @@ export class MiAbuseUserReport {
@Column('varchar', {
length: 128, nullable: true,
})
public resolvedAs: 'accept' | 'reject' | null;
public resolvedAs: AbuseReportResolveType | null;
//#region Denormalized fields
@Index()

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, ViewEntity } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@ -98,3 +98,4 @@ export class MiFollowing {
public followeeSharedInbox: string | null;
//#endregion
}

View file

@ -4,9 +4,9 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { DataSource, MoreThan, QueryFailedError, TypeORMError } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { MiUser, type DriveFilesRepository, type NotesRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
@ -26,6 +26,9 @@ export class DeleteAccountProcessorService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.db)
private db: DataSource,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@ -52,6 +55,14 @@ export class DeleteAccountProcessorService {
return;
}
if (!user.isDeleted) {
this.logger.warn('User is not pre-marked as deleted, this is likely a bug');
if (process.env.NODE_ENV !== 'production') {
throw new Error('User is not pre-marked as deleted'); // make some noise to make sure tests fail
}
await this.usersRepository.update({ id: user.id }, { isDeleted: true });
}
{ // Delete notes
let cursor: MiNote['id'] | null = null;
@ -121,13 +132,46 @@ export class DeleteAccountProcessorService {
}
}
// soft指定されている場合は物理削除しない
if (job.data.soft) {
// nop
} else {
await this.usersRepository.delete(job.data.user.id);
// Deadlockが発生した場合にリトライする
for (let remaining = 3; remaining > 0; remaining--) {
try {
// soft指定されている場合は物理削除しない
await this.db.transaction(async txn => {
// soft指定してもデータをすべで削除する
await txn.delete(MiUser, user.id);
if (job.data.soft) {
await txn.insert(MiUser, {
...user,
isRoot: false,
updatedAt: new Date(),
emojis: [],
hideOnlineStatus: true,
followersCount: 0,
followingCount: 0,
avatarUrl: null,
avatarId: null,
notesCount: 0,
inbox: null,
sharedInbox: null,
featured: null,
uri: null,
followersUri: null,
token: null,
isDeleted: true,
});
}
});
return 'Account deleted';
} catch (e) {
// 40P01 = deadlock_detected
// https://www.postgresql.org/docs/current/errcodes-appendix.html
if (remaining > 0 && e instanceof QueryFailedError && e.driverError.code === '40P01') {
this.logger.warn(`Deadlock occurred, retrying after 1s... [${remaining - 1} remaining]`);
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
throw e;
}
}
return 'Account deleted';
}
}

View file

@ -8,27 +8,19 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import rename from 'rename';
import sharp from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js';
import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js';
import { contentDisposition } from '@/misc/content-disposition.js';
import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import { InternalStorageService } from '@/core/InternalStorageService.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@ -46,11 +38,8 @@ export class FileServerService {
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private fileInfoService: FileInfoService,
private downloadService: DownloadService,
private imageProcessingService: ImageProcessingService,
private videoProcessingService: VideoProcessingService,
private internalStorageService: InternalStorageService,
private fileInfoService: FileInfoService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
@ -134,165 +123,78 @@ export class FileServerService {
return;
}
try {
if (file.state === 'remote') {
let image: IImageStreamable | null = null;
if (file.state === 'remote') {
const url = new URL(`${this.config.mediaProxy}/`);
if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
url.searchParams.set('url', file.url);
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
return await reply.redirect(url.toString(), 301);
}
file.cleanup();
return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
return await reply.redirect(externalThumbnail, 301);
}
if (file.fileRole !== 'original') {
const filename = rename(file.filename, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : '.unknown',
}).toString();
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', filename));
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(url.toString(), 301);
}
}
if (!image) {
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
image = {
data: fs.createReadStream(file.path, {
start,
end,
}),
ext: file.ext,
type: file.mime,
};
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
} else {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
}
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext),
),
);
return image.data;
const chunksize = end - start + 1;
const fileStream = fs.createReadStream(file.path, {
start,
end,
});
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return fileStream;
}
if (file.fileRole !== 'original') {
const filename = rename(file.filename, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : '.unknown',
}).toString();
return fs.createReadStream(file.path);
} else {
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', filename));
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
const fileStream = fs.createReadStream(file.path, {
start,
end,
});
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return fileStream;
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
return fs.createReadStream(file.path);
} else {
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
const fileStream = fs.createReadStream(file.path, {
start,
end,
});
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return fileStream;
}
return fs.createReadStream(file.path);
const chunksize = end - start + 1;
const fileStream = fs.createReadStream(file.path, {
start,
end,
});
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return fileStream;
}
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
return fs.createReadStream(file.path);
}
}
@bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
let url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
@ -302,234 +204,56 @@ export class FileServerService {
// アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query;
if (this.config.externalMediaProxyEnabled) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const externalURL = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
for (const [key, value] of Object.entries(request.query)) {
externalURL.searchParams.append(key, value);
}
if (mustOrigin) {
url = `${this.config.mediaProxy}?url=${encodeURIComponent(url)}`;
} else {
return await reply.redirect(
externalURL.toString(),
301,
);
}
if (!this.config.mediaProxy) {
reply.code(501);
}
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
const proxiedURL = new URL(`${this.config.mediaProxy}/?url=${encodeURIComponent(url)}`);
for (const [key, value] of Object.entries(request.query)) {
if (key.toLowerCase() === 'url') continue;
proxiedURL.searchParams.append(key, value);
}
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
}
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
if (
'emoji' in request.query ||
'avatar' in request.query ||
'static' in request.query ||
'preview' in request.query ||
'badge' in request.query
) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
}
let image: IImageStreamable | null = null;
if ('emoji' in request.query || 'avatar' in request.query) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
} else {
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
.resize({
height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query) {
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
} else if ('preview' in request.query) {
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
} else if ('badge' in request.query) {
const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, {
fit: 'contain',
position: 'centre',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (file.mime === 'image/svg+xml') {
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
if (!image) {
if (request.headers.range && file.file && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
image = {
data: fs.createReadStream(file.path, {
start,
end,
}),
ext: file.ext,
type: file.mime,
};
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
} else {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
}
if ('cleanup' in file) {
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext),
),
if (!mustOrigin) {
return await reply.redirect(
proxiedURL.toString(),
301,
);
return image.data;
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async getStreamAndTypeFromUrl(url: string): Promise<
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
if (url.startsWith(`${this.config.url}/files/`)) {
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
return await this.getFileFromKey(key);
}
return await this.downloadAndDetectTypeFromUrl(url);
}
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
@bindThis
private async downloadAndDetectTypeFromUrl(url: string): Promise<
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
> {
const [path, cleanup] = await createTemp();
try {
const { filename } = await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
state: 'remote',
mime, ext,
path, cleanup,
filename,
};
} catch (e) {
cleanup();
throw e;
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
}
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
}
// directly proxy request through
const res = await fetch(proxiedURL, {
headers: {
'X-Forwarded-For': request.headers['x-forwarded-for']?.at(0) ?? request.ip,
'User-Agent': request.headers['user-agent'],
},
});
reply.code(res.status);
for (const [key, value] of res.headers.entries()) {
reply.header(key, value);
}
reply.send(res.body);
}
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; filename: string; url: string; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404'
| '204'
@ -548,15 +272,10 @@ export class FileServerService {
if (!file.storedInternal) {
if (!(file.isLink && file.uri)) return '204';
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
return {
...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
filename: file.name,
};
return { state: 'remote',
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
filename: file.name
, url: file.uri };
}
const path = this.internalStorageService.resolvePath(key);

View file

@ -33,7 +33,6 @@ import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
import { makeHstsHook } from './hsts.js';
import { generateCSP } from './csp.js';
import * as prom from 'prom-client';
import { sanitizeRequestURI } from '@/misc/log-sanitization.js';
import { metricCounter, metricGauge, metricHistogram, MetricsService } from './api/MetricsService.js';
@ -110,6 +109,11 @@ const mLastSuccessfulRequest = metricGauge({
labelNames: [],
});
// This function is used to determine if a path is safe to redirect to.
function redirectSafePath(path: string): boolean {
return ['/files/', '/identicon/', '/proxy/', '/static-assets/', '/vite/', '/embed_vite/'].some(prefix => path.startsWith(prefix));
}
@Injectable()
export class ServerService implements OnApplicationShutdown {
private logger: Logger;
@ -348,7 +352,7 @@ export class ServerService implements OnApplicationShutdown {
name: name,
});
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
reply.header('Content-Security-Policy', 'default-src \'none\'');
if (emoji == null) {
if ('fallback' in request.query) {
@ -359,16 +363,26 @@ export class ServerService implements OnApplicationShutdown {
}
}
const dbUrl = emoji?.publicUrl || emoji?.originalUrl;
const dbUrlParsed = new URL(dbUrl);
const instanceUrl = new URL(this.config.url);
if (dbUrlParsed.origin === instanceUrl.origin) {
if (!redirectSafePath(dbUrlParsed.pathname)) {
return await reply.status(508);
}
return await reply.redirect(dbUrl, 301);
}
let url: URL;
if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('url', dbUrl);
url.searchParams.set('badge', '1');
} else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('url', dbUrl);
url.searchParams.set('emoji', '1');
if ('static' in request.query) url.searchParams.set('static', '1');
}
@ -392,6 +406,16 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400');
if (user) {
const dbUrl = user?.avatarUrl ?? this.userEntityService.getIdenticonUrl(user);
const dbUrlParsed = new URL(dbUrl);
const instanceUrl = new URL(this.config.url);
if (dbUrlParsed.origin === instanceUrl.origin) {
if (!redirectSafePath(dbUrlParsed.pathname)) {
return await reply.status(508);
}
return await reply.redirect(dbUrl, 301);
}
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
} else {
reply.redirect('/static-assets/user-unknown.png');

View file

@ -134,8 +134,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.mergePack(
me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
isPost(object) ? await this.apNoteService.createNote(getApId(object), resolver, true) : null,
);
}

View file

@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check if the circular reference will occur
const checkCircle = async (folderId: string, limit: number = 32): Promise<boolean> => {
if (limit <= 0) {
return false;
return true;
}
const folder2 = await this.driveFoldersRepository.findOneByOrFail({
id: folderId,

View file

@ -50,18 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.find({
where: {
host: IsNull(),
},
order: {
category: 'ASC',
name: 'ASC',
},
});
const emojis = await this.emojisRepository
.createQueryBuilder()
.where({ host: IsNull() })
.orderBy('LOWER(category)', 'ASC')
.addOrderBy('LOWER(name)', 'ASC')
.getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),
emojis: emojis.map(this.emojiEntityService.packSimpleNoQuery),
};
});
}

View file

@ -106,6 +106,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: In(ps.userIds),
} : {
id: In(ps.userIds),
isDeleted: false,
isSuspended: false,
});

View file

@ -30,6 +30,7 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
[
'\'self\'',
'data:',
'blob:',
// 'https://avatars.githubusercontent.com', // uncomment this for contributor avatars to work
options.mediaProxy
].filter(Boolean)],

View file

@ -19,7 +19,7 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import { yumeNormalizeObject, type IActor, type IApDocument, type ICollection, type IObject, type IPost } from '@/core/activitypub/type.js';
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
@ -42,6 +42,7 @@ function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
id: actorId,
type: 'Person',
preferredUsername,
__yume_normalized_object: true,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
@ -55,6 +56,7 @@ function createRandomNote(actor: NonTransientIActor): NonTransientIPost {
id: noteId,
type: 'Note',
attributedTo: actor.id,
__yume_normalized_object: true,
content: 'test test foo',
};
}
@ -71,6 +73,7 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe
type: 'Collection',
id: actor.outbox as string,
totalItems: items.length,
__yume_normalized_object: true,
items,
};
}
@ -162,6 +165,34 @@ describe('ActivityPub', () => {
content: 'あ',
};
const punnyPost = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `https://あ.com/users/${secureRndstr(8)}`,
type: 'Note',
attributedTo: actor.id,
to: 'https://www.w3.org/ns/activitystreams#Public',
content: 'あ',
};
test('punnyPost normalization', async () => {
const normalized = yumeNormalizeObject(punnyPost);
assert.strictEqual(normalized.id, 'https://xn--l8j.com/users/あ');
});
const portedHost = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `https://あ.com:12443/users/${secureRndstr(8)}`,
type: 'Note',
to: 'https://www.w3.org/ns/activitystreams#Public',
content: 'あ',
}
test('actor with port should be rejected', async () => {
assert.throws(() => {
yumeNormalizeObject(portedHost);
});
});
test('Minimum Actor', async () => {
resolver.register(actor.id, actor);
@ -220,6 +251,7 @@ describe('ActivityPub', () => {
type: 'OrderedCollection',
totalItems: 0,
first: `${actor.id}/following?page=1`,
__yume_normalized_object: true,
};
actor.followers = `${actor.id}/followers`;
@ -229,6 +261,7 @@ describe('ActivityPub', () => {
type: 'OrderedCollection',
totalItems: 0,
first: `${actor.followers}?page=1`,
__yume_normalized_object: true,
});
const user = await personService.createPerson(actor.id, resolver);
@ -244,6 +277,7 @@ describe('ActivityPub', () => {
id: `${actor.id}/following`,
type: 'OrderedCollection',
totalItems: 0,
__yume_normalized_object: true,
// first: …
};
actor.followers = `${actor.id}/followers`;
@ -348,6 +382,7 @@ describe('ActivityPub', () => {
mediaType: 'image/png',
url: 'http://host1.test/foo.png',
name: '',
__yume_normalized_object: true,
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
@ -361,6 +396,7 @@ describe('ActivityPub', () => {
url: 'http://host1.test/bar.png',
name: '',
sensitive: true,
__yume_normalized_object: true,
};
const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
@ -377,6 +413,7 @@ describe('ActivityPub', () => {
mediaType: 'image/png',
url: 'http://host1.test/foo.png',
name: '',
__yume_normalized_object: true,
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
@ -390,6 +427,7 @@ describe('ActivityPub', () => {
url: 'http://host1.test/bar.png',
name: '',
sensitive: true,
__yume_normalized_object: true,
};
const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
@ -406,6 +444,7 @@ describe('ActivityPub', () => {
mediaType: 'image/png',
url: 'http://host1.test/foo.png',
name: '',
__yume_normalized_object: true,
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
@ -419,6 +458,7 @@ describe('ActivityPub', () => {
url: 'http://host1.test/bar.png',
name: '',
sensitive: true,
__yume_normalized_object: true,
};
const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
@ -431,6 +471,7 @@ describe('ActivityPub', () => {
const linkObject: IObject = {
type: 'Link',
href: 'https://example.com/',
__yume_normalized_object: true,
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),

View file

@ -33,7 +33,7 @@
"./node_modules"
],
"types": [
"vite/client",
"vite/client"
],
"lib": [
"esnext",
@ -44,8 +44,9 @@
},
"compileOnSave": false,
"include": [
"./**/*.ts",
"./**/*.vue"
"./src/**/*.ts",
"./src/**/*.vue",
"./@types/**/*.ts"
],
"exclude": [
".storybook/**/*"

View file

@ -187,6 +187,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
@ -566,15 +567,24 @@ function showRenoteMenu(): void {
};
}
const renoteDetailsMenu: MenuItem = {
type: 'link',
text: i18n.ts.renoteDetails,
icon: 'ti ti-info-circle',
to: notePage(note.value),
};
if (isMyRenote) {
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([
renoteDetailsMenu,
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getUnrenote(),
], renoteTime.value);
} else {
os.popupMenu([
renoteDetailsMenu,
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),

View file

@ -45,8 +45,11 @@
},
"compileOnSave": false,
"include": [
"./**/*.ts",
"./**/*.vue"
"./src/**/*.ts",
"./src/**/*.vue",
"./test/**/*.ts",
"./test/**/*.vue",
"./@types/**/*.ts"
],
"exclude": [
".storybook/**/*"

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.11.0-yumechinokuni.3",
"version": "2024.11.0-yumechinokuni.5",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -43,9 +43,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.15"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
@ -58,36 +58,36 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.8"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -115,9 +115,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.7.7"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
@ -221,16 +221,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
version = "1.7.2"
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cc"
version = "1.1.30"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
dependencies = [
"shlex",
]
@ -258,9 +264,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.20"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
@ -268,9 +274,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.20"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
@ -292,15 +298,15 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
[[package]]
name = "colorchoice"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "core-foundation"
@ -348,10 +354,21 @@ dependencies = [
]
[[package]]
name = "encoding_rs"
version = "0.8.34"
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
@ -397,14 +414,14 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "fedivet"
version = "0.1.0"
source = "git+https://forge.yumechi.jp/yume/fedivet?tag=testing-audit+relay#b0266202cfe72479774689db8879c3200190ea5f"
source = "git+https://forge.yumechi.jp/yume/fedivet?tag=testing-audit%2Brelay%2Bfilter#b1b051dc2f1319a3948d7afcecfd3ac8f92a07de"
dependencies = [
"async-trait",
"axum",
@ -425,9 +442,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.34"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -596,9 +613,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]]
name = "heck"
@ -720,9 +737,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
@ -761,13 +778,142 @@ dependencies = [
]
[[package]]
name = "idna"
version = "0.5.0"
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
@ -777,7 +923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown 0.15.0",
"hashbrown 0.15.1",
]
[[package]]
@ -809,9 +955,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.160"
version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "linux-raw-sys"
@ -819,6 +965,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -908,6 +1060,7 @@ dependencies = [
"clap",
"env_logger",
"fedivet",
"rand",
"serde",
"tokio",
]
@ -992,18 +1145,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.6"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec"
checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.6"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8"
checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
dependencies = [
"proc-macro2",
"quote",
@ -1012,9 +1165,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
@ -1029,10 +1182,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "proc-macro2"
version = "1.0.88"
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@ -1046,6 +1208,36 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
@ -1057,9 +1249,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@ -1069,9 +1261,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
@ -1086,9 +1278,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.8"
version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [
"base64",
"bytes",
@ -1152,9 +1344,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.37"
version = "0.38.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
dependencies = [
"bitflags",
"errno",
@ -1165,9 +1357,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.15"
version = "0.23.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e"
dependencies = [
"once_cell",
"rustls-pki-types",
@ -1244,9 +1436,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
dependencies = [
"core-foundation-sys",
"libc",
@ -1254,18 +1446,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
@ -1274,9 +1466,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@ -1352,6 +1544,12 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
@ -1366,9 +1564,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@ -1390,6 +1588,17 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
@ -1413,9 +1622,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.13.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
@ -1426,18 +1635,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
dependencies = [
"proc-macro2",
"quote",
@ -1445,25 +1654,20 @@ dependencies = [
]
[[package]]
name = "tinyvec"
version = "1.8.0"
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"tinyvec_macros",
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.40.0"
version = "1.41.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
dependencies = [
"backtrace",
"bytes",
@ -1554,9 +1758,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [
"bitflags",
"bytes",
@ -1608,27 +1812,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "unicode-bidi"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-normalization"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
dependencies = [
"tinyvec",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@ -1637,9 +1826,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [
"form_urlencoded",
"idna",
@ -1647,6 +1836,18 @@ dependencies = [
"serde",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -1743,9 +1944,9 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "wasm-streams"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
@ -1885,8 +2086,108 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yoke"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -7,6 +7,7 @@ edition = "2021"
axum = "0.7"
clap = { version = "4.5.20", features = ["derive"] }
env_logger = "0.11.5"
fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "testing-audit+relay" }
fedivet = { git = "https://forge.yumechi.jp/yume/fedivet", tag = "testing-audit+relay+filter" }
rand = "0.8.5"
serde = { version = "1.0.210", features = ["derive"] }
tokio = { version = "1" }

View file

@ -1,16 +1,22 @@
use std::collections::HashMap;
use std::hash::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Instant;
use axum::response::IntoResponse;
use axum::http::StatusCode;
use clap::Parser;
use fedivet::evaluate::chain::audit::AuditOptions;
use fedivet::evaluate::Disposition;
use fedivet::evaluate::Evaluator;
use fedivet::model::error::MisskeyError;
use fedivet::serve;
use fedivet::BaseAppState;
use fedivet::HasAppState;
use serde::Serialize;
use rand::random;
#[derive(Parser)]
pub struct Args {
@ -24,14 +30,84 @@ pub struct Args {
pub tls_key: Option<String>,
}
struct LastSeenEntry {
ts: Instant,
count: u64,
}
const ERR_DEDUP: MisskeyError = MisskeyError::new_const(
StatusCode::OK,
"TOO_MANY_DUPLICATES",
"02fe81b0-c1ef-4d4e-bd5c-288ae379bff7",
"Too many duplicates, further duplicates will be ignored.",
);
#[allow(clippy::unused_async)]
async fn build_state<E: IntoResponse + Clone + Serialize + Send + Sync + 'static>(
base: Arc<BaseAppState<E>>,
async fn build_state(
base: Arc<BaseAppState<MisskeyError>>,
_args: &Args,
) -> impl HasAppState<E> + Evaluator<E> {
base
.extract_meta()
.audited(AuditOptions::new(PathBuf::from("/store/log/audit/incoming")))
) -> impl HasAppState<MisskeyError> + Evaluator<MisskeyError> {
let dedup_last_seen = Arc::new(RwLock::new(HashMap::<_, LastSeenEntry>::new()));
let dedup_last_seen_clone = dedup_last_seen.clone();
let dedup_last_seen_clone2 = dedup_last_seen.clone();
tokio::spawn(async move {
let mut ticker = tokio::time::interval(tokio::time::Duration::from_secs(60 * 60));
loop {
ticker.tick().await;
let mut dedup_last_seen = dedup_last_seen_clone.write().unwrap();
dedup_last_seen.retain(|_, entry| entry.ts.elapsed().as_secs() < 60 * 5);
}
});
base.extract_meta()
.filter(
move |event| match event.activity.as_ref() {
Err(_) => Disposition::Next,
Ok(activity) => {
let dedup_last_seen = dedup_last_seen.read().unwrap();
let mut hash = DefaultHasher::new();
activity.meta_obj.ty_.hash(&mut hash);
activity.meta_obj.id.hash(&mut hash);
let count = dedup_last_seen
.get(&hash.finish())
.map(|entry| entry.count)
.unwrap_or(0);
if count > 5 {
Disposition::Intercept(ERR_DEDUP)
} else {
Disposition::Next
}
}
},
move |_resp, info| match info.activity.as_ref() {
Err(_) => {}
Ok(activity) => {
let mut hash = DefaultHasher::new();
activity.meta_obj.ty_.hash(&mut hash);
activity.meta_obj.id.hash(&mut hash);
let mut dedup_last_seen = dedup_last_seen_clone2.write().unwrap();
dedup_last_seen
.entry(hash.finish())
.or_insert(LastSeenEntry {
ts: Instant::now(),
count: 1,
});
if dedup_last_seen.len() * std::mem::size_of::<(u64, LastSeenEntry)>()
> 64 << 20
{
dedup_last_seen.retain(|_, _| random::<bool>());
}
}
},
)
.audited(AuditOptions::new(PathBuf::from(
"/store/log/audit/incoming",
)))
}
#[tokio::main]
@ -43,7 +119,7 @@ async fn main() {
let args = Args::parse();
let state = build_state::<MisskeyError>(
let state = build_state(
Arc::new(
BaseAppState::new(args.backend.parse().expect("Invalid backend URL")).with_empty_ctx(),
),