Compare commits

..

126 commits

Author SHA1 Message Date
Mizah a858ee31c6 Added doc comment to alert()
Some checks failed
Check SPDX-License-Identifier / check-spdx-license-id (push) Has been cancelled
Check copyright year / check_copyright_year (push) Has been cancelled
Publish Docker image (develop) / Build (linux/amd64) (push) Has been cancelled
Publish Docker image (develop) / Build (linux/arm64) (push) Has been cancelled
Dockle / dockle (push) Has been cancelled
Lint / pnpm_install (push) Has been cancelled
Storybook / build (push) Has been cancelled
Test (frontend) / vitest (20.16.0) (push) Has been cancelled
Test (frontend) / e2e (chrome, 20.16.0) (push) Has been cancelled
Test (production install and build) / production (20.16.0) (push) Has been cancelled
Publish Docker image (develop) / merge (push) Has been cancelled
Lint / lint (backend) (push) Has been cancelled
Lint / lint (frontend) (push) Has been cancelled
Lint / lint (frontend-embed) (push) Has been cancelled
Lint / lint (frontend-shared) (push) Has been cancelled
Lint / lint (misskey-bubble-game) (push) Has been cancelled
Lint / lint (misskey-js) (push) Has been cancelled
Lint / lint (misskey-reversi) (push) Has been cancelled
Lint / lint (sw) (push) Has been cancelled
Lint / typecheck (backend) (push) Has been cancelled
Lint / typecheck (misskey-js) (push) Has been cancelled
Lint / typecheck (sw) (push) Has been cancelled
2024-11-17 14:34:54 +02:00
Mizah 0bd7ed8191 Added doc comment to toast() 2024-11-17 14:34:47 +02:00
Mizah ee574ae154 Added doc comment to pageWindow 2024-11-17 14:34:39 +02:00
Mizah a505f36252 Added doc comment to popup 2024-11-17 14:34:31 +02:00
Mizah a516383c66 Added internal comments to popup() 2024-11-17 14:34:21 +02:00
Mizah 1613dafc39 Added docs to z-index generator. 2024-11-17 14:23:44 +02:00
Mizah 8457fa9b3b Added docs to popup list and popup id counter. 2024-11-17 14:23:31 +02:00
Mizah 2e51e779e7 Added docs idb proxy.
Some checks are pending
Check SPDX-License-Identifier / check-spdx-license-id (push) Waiting to run
Check copyright year / check_copyright_year (push) Waiting to run
Publish Docker image (develop) / Build (linux/amd64) (push) Waiting to run
Publish Docker image (develop) / Build (linux/arm64) (push) Waiting to run
Publish Docker image (develop) / merge (push) Blocked by required conditions
Dockle / dockle (push) Waiting to run
Storybook / build (push) Waiting to run
Test (production install and build) / production (20.16.0) (push) Waiting to run
2024-11-17 14:02:17 +02:00
Mizah 7f6b486976 Added docs to Keys for localStorage. 2024-11-17 14:02:11 +02:00
Mizah 8c1508fae4 Added docs to miLocalStorage. 2024-11-17 14:02:08 +02:00
Mizah aeb568664d Added docs (and observations) to notes count. 2024-11-17 14:02:02 +02:00
Mizah ecb990fb77 Added docs to $i, iAmModerator, iAmAdmin. 2024-11-17 14:01:53 +02:00
饺子w (Yumechi) a11b77a415
fix(backend): Webhook Test一致性 (#14863)
* fix(backend): Webhook Test一致性

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

* UserWebhookPayload<'followed'> 修正

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

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-12 09:51:18 +09:00
syuilo 1bc4f400c0 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-11-11 16:35:23 +09:00
syuilo 458c72c153 Update about-misskey.vue 2024-11-11 16:35:13 +09:00
syuilo 6bd3ed2074
Update CHANGELOG.md 2024-11-10 15:10:04 +09:00
かっこかり 31e5f0bd09
fix(frontend): メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正 (#14928)
* fix(frontend): メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正

* Update MkSignupDialog.form.vue

* fix condition
2024-11-10 15:08:58 +09:00
かっこかり e0a83e9c9e
Update CHANGELOG.md (書き方を揃える) 2024-11-09 15:57:10 +09:00
かっこかり 1496700b37
Update CHANGELOG.md
たぶんリリースワークフローはこうしないと認識してくれない
2024-11-09 15:51:49 +09:00
syuilo 00cbf9fe80
Update CONTRIBUTING.md 2024-11-09 14:09:02 +09:00
github-actions[bot] cf09aa21f0 Bump version to 2024.11.0-alpha.0 2024-11-09 02:28:02 +00:00
github-actions[bot] 9f7d41eb47 Bump version to 2024.10.2-alpha.3 2024-11-09 02:25:42 +00:00
かっこかり 4a62051ce7
fix(backend): ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正 (#14879)
* fix: make sure mentions of local users get rendered correctly during AP delivery (resolves #645)

* Update Changelog

* indent

---------

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-11-09 10:58:09 +09:00
かっこかり 3a421837bf
refactor(frontend): 動画UIのフルスクリーン周りの調整 (#14877)
* refactor(frontend): フルスクリーン周りの調整

(cherry picked from commit 783032caec5853d78d5af3391e29cf364f2282e8)

* refactor(frontend): deviceKindの循環参照を除去

(cherry picked from commit 1ca471f57e968a1a6e2259bde4a7c6da1fe0c54e)

* fix

---------

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
2024-11-09 10:57:04 +09:00
momoirodouhu a4c5ce1413
enhance(backend) : リモートユーザーの照会をオリジナルにリダイレクトするように (#12892) (#14897)
* enhance(backend) : リモートユーザーの照会をオリジナルにリダイレクトするように (#12892)

* オリジンリダイレクトのテストをtodoとして追加。

e2eテストにリモートユーザー考慮のテストがなさそうなので。

次のコマンドで動くことは確認済みです。
curl "http://localhost:3000/@foo@bar" -H "accept: application/activity+json" -L

* Acctのパースを既存のパーサーでするように修正

* lint
2024-11-09 10:54:44 +09:00
かっこかり e75b62f3f5
enhance(frontend): 個別お知らせページではmetaタグを出力するように (#14902)
* enhance(frontend): 個別お知らせページではmetaタグを出力するように

* Update Changelog
2024-11-09 10:53:09 +09:00
かっこかり 5b60ae810b
fix(frontend): 外部URLへのリダイレクトのバリデーションを強化 (#14919)
* Fix code scanning alert no. 25: Incomplete URL scheme check (MisskeyIO#799)

* Fix code scanning alert no. 26: Incomplete URL scheme check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Fix code scanning alert no. 25: Incomplete URL scheme check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
(cherry picked from commit 7d7552e076c0152a5966e919be0e9a60b3736208)

* ✌️

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-11-09 10:52:07 +09:00
かっこかり 98b4717c45
fix(backend): SQLのサニタイズを強化 (#14920)
* Fix code scanning alert no. 28: Incomplete string escaping or encoding (MisskeyIO#800)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
(cherry picked from commit 443335c662b14f609d6a81a8f3807e95709aebc1)

* ✌️

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-11-09 10:51:28 +09:00
syuilo 8a4ce16e90
Update CONTRIBUTING.md 2024-11-08 18:00:55 +09:00
4ster1sk 794cb9ffe2
fix(backend): followedMessageではなくdescriptionになっていたのを修正 (#14908) 2024-11-07 17:16:51 +09:00
syuilo 0b976064ca
Update CHANGELOG.md 2024-11-07 15:10:38 +09:00
4ster1sk bca690f256
fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように (#14904) 2024-11-07 15:10:10 +09:00
Linca f1eb17f66c
chore: little type trick in pizzax.ts (#14891)
Make `makeGetterSetter` take the correct type associated with getter and setter
2024-11-06 22:01:58 +09:00
かっこかり b1c82213a3
fix(backend): FTT無効時にユーザーリストタイムラインが使用できない問題を修正 (#14878)
* fix: return getfromdb when FanoutTimeline is not enabled

* Update Changelog

* fix

---------

Co-authored-by: Lhc_fl <lhcfl@outlook.com>
2024-11-06 22:01:21 +09:00
かっこかり a896c39dbf
fix(frontend): ノート投稿ボタンにホバー時のスタイルが適用されていない (#14887)
* fix(frontend): ノート投稿ボタンにホバー時のスタイルが適用されていない (#305)

(cherry picked from commit 711ab846a967feeddbe0c908bee4b91646cec321)

* Update Changelog

---------

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
2024-11-06 15:15:28 +09:00
かっこかり 6718a54f6f
fix(backend): ノートを連合する際にリモートユーザーのacctの大小文字を区別して処理している問題を修正 (#14880)
* fix: make sure outgoing remote mentions get resolved correctly if referenced with non-canonical casing (resolves #646)

* Update Changelog

* Update Changelog

* indent

---------

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
2024-11-03 08:26:51 +09:00
かっこかり d57b8bf2e2
fix(frontend): withSensitiveフィルタ周りの挙動修正 (#14884)
* fix(frontend): withSensitiveフィルタ周りの挙動修正

* Update MkNote.vue
2024-11-03 08:23:52 +09:00
syuilo 224bbd486f refactor 2024-10-31 13:50:50 +09:00
syuilo 724dea8136 lint 2024-10-31 13:47:47 +09:00
syuilo ceb60d61b0 refactor 2024-10-31 13:47:30 +09:00
かっこかり 17d9aca5a7
refactor(frontend): asとanyをすぐなおせる範囲で除去 (#14848)
* refactor(frontend): できるだけanyを除去

* refactor

* lint

* fix

* remove unused

* Update packages/frontend/src/components/MkReactionsViewer.details.vue

* Update packages/frontend/src/components/MkUsersTooltip.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-31 13:46:42 +09:00
かっこかり 7fc8a2a7b0
fix(frontend): 一部のノート表示で設定にかかわらずセンシティブなファイルを含むノートが最小化される問題を修正
Fix https://github.com/misskey-dev/misskey/pull/14772#discussion_r1821707117
2024-10-30 09:57:54 +09:00
github-actions[bot] a96f09cee3 Bump version to 2024.10.2-alpha.2 2024-10-28 12:23:59 +00:00
かっこかり f30d19051f
enhance(backend): check_connect.js で全RedisとDBへの接続を確認するように (#14853)
* fix race conditions in check_connect.js

(cherry picked from commit 524ddb96770690455b82522104a543c5b0b1f3b3)

* fix

* Update Changelog

---------

Co-authored-by: Hazelnoot <acomputerdog@gmail.com>
2024-10-28 21:06:54 +09:00
Tamme Schichler 8eb7749e44
fix(backend): Accept arrays in ActivityPub icon and image properties (#14825)
This is allowed according to the Activity vocabulary: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
The issue is noticeable in combination with Bridgy Fed: https://github.com/snarfed/bridgy-fed/issues/1408
2024-10-28 21:06:16 +09:00
Pinapelz 0472d43ee9
fix: encode RSS uris with escape sequences before fetching (#14826)
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-28 21:04:46 +09:00
syuilo eb701f2ff4 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-10-28 20:42:23 +09:00
syuilo 74847bce30 enhance: アイコンデコレーション管理画面の改善 2024-10-28 20:42:14 +09:00
woxtu eecfac1dd9
Remove undefined styles (#14858) 2024-10-28 20:22:07 +09:00
syuilo e927507886 🎨 2024-10-28 18:34:18 +09:00
syuilo b1073714ba Update about-misskey.vue 2024-10-28 12:46:39 +09:00
かっこかり 04b37a1315
enhance(i18n): カタルーニャ語を追加 (#14842)
* enhance(i18n): 対応言語の追加

* 翻訳進捗が70%に満たないものを除外

* Update Changelog

* 翻訳進捗が70%を超えたら導入の旨を明記

* typo

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-28 12:32:17 +09:00
syuilo 93a03e6b6d
New Crowdin updates (#14767)
* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

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

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

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

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

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

* New translations ja-jp.yml (Catalan)

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

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

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

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Indonesian)

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Hungarian)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

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

* New translations ja-jp.yml (English)

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

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

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)
2024-10-28 11:43:55 +09:00
かっこかり ec4358d1e8
fix(misskey-js): WebSocketの型定義をReconnectingWebsocketに依存するように (#14850)
* fix(misskey-js): WebSocketの型定義をReconnectingWebsocketに依存するように

* Update Changelog

* run api extractor

* fix

* fix
2024-10-28 11:43:05 +09:00
かっこかり a6a1e3d733
enhance(frontend): Self-XSS防止用のメッセージを追加 (#14839)
* enhance(frontend): Self-XSS防止用のメッセージを追加

* Update Changelog

* embedにも同様の記述を追加
2024-10-26 22:07:26 +09:00
github-actions[bot] ded6ef207b Bump version to 2024.10.2-alpha.1 2024-10-25 13:16:43 +00:00
かっこかり db95b6b0d6
🎨
https://github.com/misskey-dev/misskey/pull/14828 のデザイン修正
2024-10-25 19:37:01 +09:00
かっこかり eeea4ec00b
fix(backend): 招待コード発行可能残り数算出に使用すべきロールポリシーの値が違うのを修正 (#14834)
* fix: should use invite limit cycle to calculate invite/limit

* Update Changelog

* Update changelog

---------

Co-authored-by: Lhc_fl <lhcfl@outlook.com>
2024-10-25 15:09:37 +09:00
かっこかり 07b2c3e5b2
fix(frontend): 管理画面のリンク切れを修正 (#14831)
* fix(frontend): 管理画面のリンク切れを修正

* Update Changelog
2024-10-25 15:09:07 +09:00
かっこかり 076cc953e2
enhance(frontend): 外部アプリ認証画面の改良 (#14828)
* enhance(frontend): 外部アプリ認証画面の改良

* 🎨

* lint

* Update Changelog

* indent

* lint

* enhance: miauthのリダイレクト先をUI内でも表示するように

* 🎨

* fix

* fix
2024-10-25 14:20:33 +09:00
かっこかり 15ae1605ec
enhance(frontend): 「単なるラッキー」の調整 (#14807)
* enhance(frontend): 「単なるラッキー」の調整

* refactor

* comment

* Update Changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-23 14:23:29 +09:00
饺子w (Yumechi) 48d1539f3b
Merge commit from fork
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-10-22 18:17:56 +09:00
github-actions[bot] 8b6d321a76 Bump version to 2024.10.2-alpha.0 2024-10-22 08:45:08 +00:00
syuilo 952fec5665
feat: 過去のノートを非公開化/フォロワーのみ表示可能にできる機能 (#14814)
* wip

* Update CHANGELOG.md

* wip

* wip

* wip

* Update privacy.vue

* wip
2024-10-22 17:08:53 +09:00
syuilo 70b2a8f72e fix(frontend): /iのレスポンスに含まれないプロパティが消えずに残り続ける問題を修正 2024-10-21 19:59:20 +09:00
syuilo c4f1ca2fd9 fix(frontend): MkSelectでmodelValueが更新されない限り値を更新しないように 2024-10-21 19:14:02 +09:00
Kisaragi 9d0f7eeb9c
docs: ActivityPub層の変更を含む場合にやるべきことを明文化 (#14812) 2024-10-21 15:12:28 +09:00
かっこかり bc1fce9af6
fix(frontend): デッキのタイムラインカラムでwithSensitiveが利用できない問題を修正 (#14772)
* fix(frontend): デッキのタイムラインカラムでwithSensitiveが利用できない問題を修正

* Update Changelog

* Update Changelog

* Update packages/frontend/src/ui/deck/tl-column.vue
2024-10-21 13:22:21 +09:00
かっこかり 5f12bc515d
Update CHANGELOG.md 2024-10-21 13:11:11 +09:00
Yuba 2f9c04b23b
refs#10866 投稿ダイアログでEscキーが押されたときIME入力中ならダイアログは閉じない (#14787) 2024-10-21 12:51:45 +09:00
syuilo 5c79d8db20
feat: ノートの閲覧にログイン必須にする設定 (#14799)
* wip

* wip

* wip

* Update packages/frontend/src/pages/note.vue

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

* wip

* Update WebhookTestService.ts

* Update privacy.vue

* wip

* rename

* Update locales/ja-JP.yml

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>

* 🎨

* wip

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
2024-10-21 12:49:29 +09:00
かっこかり bc0c53b92b
fix(frontend): Captcha のエラーハンドリング (#14811)
* fix(frontend): Captcha のエラーハンドリングを修正 (MisskeyIO#768)

(cherry picked from commit 88912d0f8c63a762fbb1d43e5c1abf4fd9fc05d4)

* Update Changelog

* typo

---------

Co-authored-by: riku6460 <17585784+riku6460@users.noreply.github.com>
2024-10-21 11:44:57 +09:00
かっこかり d6caa4d9c4
fix(frontend): 通知の範囲指定が必要ない通知設定でも範囲指定がでている問題を修正 (#14798)
* fix(frontend): 通知の範囲指定が必要ない通知設定でも範囲指定がでている問題を修正

* Update Changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-20 17:29:41 +09:00
syuilo 041c9caf31 🎨 2024-10-20 16:38:27 +09:00
tetsuya-ki 1d106b3ae8
Enhance: ドライブでソートができるように (#14801)
* Enhance: ドライブでソートができるように

* Update CHANGELOG.md
2024-10-20 16:17:16 +09:00
かっこかり 58419e1621
refactor(frontend): ページ内でdocument.titleを直接操作させない, タイポ修正 など (taiyme#288) (#14778)
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
2024-10-19 21:45:25 +09:00
かっこかり 2250e521e4
refactor(frontend): getBgColorを共通化 (#14782)
* refactor: getBgColor関数の切り出し + fix types (taiyme#291)

* move thing

* revert unnecesary changes

---------

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
2024-10-19 18:02:09 +09:00
かっこかり a3a99467f0
enhance(frontend): Bull Dashboard に relationship queue を追加 (#14777)
* spec(frontend): Bull Dashboard に relationship queue を追加 (MisskeyIO#751)

(cherry picked from commit a8bbccbefa67ca0f2c1ec0880da88dfc7517b6a0)

* Update Changelog

* Update Changelog

---------

Co-authored-by: riku6460 <17585784+riku6460@users.noreply.github.com>
2024-10-19 17:25:11 +09:00
github-actions[bot] b1aac6acc3 [skip ci] Update CHANGELOG.md (prepend template) 2024-10-15 04:53:48 +00:00
github-actions[bot] d2e8dc4fe3 Release: 2024.10.1 2024-10-15 04:53:43 +00:00
zyoshoka b990ae6b23
test(backend): add federation test (#14582)
* test(backend): add federation test

* fix(ci): install pnpm

* fix(ci): cd

* fix(ci): build entire project

* fix(ci): skip frontend build

* fix(ci): pull submodule when checkout

* chore: show log for debugging

* Revert "chore: show log for debugging"

This reverts commit a930964b8d.

* fix(ci): build entire project

* chore: omit unused globals

* refactor: use strictEqual and simplify some asserts

* test: follow requests

* refactor: add resolveRemoteNote function

* refactor: refine resolveRemoteUser function

* refactor: cache admin credentials

* refactor: simplify assertion with excluded fields

* refactor: use assert

* test: note

* chore: labeler detect federation

* test: blocking

* test: move

* fix: use appropriate TLD

* chore: shorter purge interval

* fix(ci): change TLD

* refactor: delete trivial comment

* test(user): isCat

* chore: use jest

* chore: omit logs

* chore: add memo

* fix(ci): omit unnecessary build

* test: pinning Note

* fix: build daemon in container

* style: indent

* test(streaming): timeline

* chore: rename

* fix: delete role after test

* refactor: resolve users by uri

* fix: delete antenna after test

* test: api timeline

* test: Note deletion

* refactor: sleep function

* test: notification

* style: indent

* refactor: type-safe host

* docs: update description

* refactor: resolve function params

* fix(block): wrong test name

* fix: invalid type

* fix: longer timeout for fire testing

* test(timeline): hashtag

* test(note): vote delivery

* fix: wrong description

* fix: hashtag channel param type

* refactor: wrap basic cases

* test(timeline): add homeTimeline tests

* fix(timeline): correct wrong case and description

* test(notification): add tests for Note

* refactor(user): wrap profile consistency with describe

* chore(note): add issue link

* test(timeline): add test

* test(user): suspension

* test: emoji

* refactor: fetch admin first

* perf: faster tests

* test(drive): sensitive flag

* test(emoji): add tests

* chore: ignore .config/docker.env

* chore: hard-coded tester IP address

* test(emoji): custom emoji are surrounded by zero width space

* refactor: client and username as property

* test(notification): mute

* fix(notification): correct description

* test(block): mention

* refactor(emoji): addCustomEmoji function

* fix: typo

* test(note): add reaction tests

* test(timeline): Note deletion

* fix: unnecessary ts-expect-error

* refactor: unnecessary fetch mocking

* chore: add TODO comments

* test(user): deletion

* chore: enable --frozen-lockfile

* fix(ci): copying configs

* docs: update CONTRIBUTING.md

* docs: fix typo

* chore: set default sleep duration

* fix(notification): omit flaky tests

* fix(notification): correct type

* test(notification): add api endpoint tests

* chore: remove redundant mute test

* refactor: use param client

* fix: start timer after trigger

* refactor: remove unnecessary any

* chore: shorter timeout for checking if fired

* fix(block): remove outdated comment

* refactor: shorten remote user variable name

* refactor(block): use existing function

* refactor: file upload

* docs: update description

* test(user): ffVisibility

* fix: `/api/signin` -> `/api/signin-flow`

* test: abuse report

* refactor: use existing type

* refactor: extract duplicate configs to template file

* fix: typo

* fix: avoid conflict

* refactor: change container dependency

* perf: start misskey parallelly

* fix: remove dependency

* chore(backend): add typecheck

* test: add check for #14728

* chore: enable eslint check

* perf: don't start linked services when test

* test(note): remote note deletion for moderation

* chore: define config template

* chore: write setup script

* refactor: omit unnecessary conditional

* refactor: clarify scope

* refactor: omit type assertion

* refactor: omit logs

* style

* refactor: redundant promise

* refactor: unnecessary imports

* refactor: use readable error code

* refactor: cache set in signin function

* refactor: optimize import
2024-10-15 13:37:00 +09:00
syuilo 3cea890eba fix(frontend): blinkアニメーションが動作していないのを修正 2024-10-15 13:01:06 +09:00
syuilo 21a2aa5243
Update CHANGELOG.md 2024-10-15 12:30:40 +09:00
syuilo 825d218692 Update CHANGELOG.md 2024-10-15 10:39:36 +09:00
syuilo b5de525548 add note 2024-10-15 10:32:00 +09:00
syuilo 5005cc8ae3 add note 2024-10-14 21:00:20 +09:00
syuilo f13c3909a0 refactor(backend): remove unnecessary any 2024-10-14 17:54:27 +09:00
syuilo 77ebabb3dc Revert "refactor"
This reverts commit 7fd8ef344b.
2024-10-14 17:51:47 +09:00
syuilo 7fd8ef344b refactor 2024-10-14 17:43:44 +09:00
syuilo b0a251d231 🎨 2024-10-14 15:35:44 +09:00
github-actions[bot] 04e74aa28c Bump version to 2024.10.1-beta.6 2024-10-14 04:19:47 +00:00
syuilo 140322b8e2 Update CHANGELOG.md 2024-10-14 12:38:12 +09:00
syuilo 3b361a9d0b perf(frontend): make skipNoteRender on by default 2024-10-14 12:37:18 +09:00
syuilo c46d6d8edd perf(frontend-embed): improve note rendering performance 2024-10-14 12:34:03 +09:00
syuilo 64bbce4cf4 perf(frontend): improve notification rendering performance 2024-10-14 12:32:59 +09:00
github-actions[bot] ddca6bdc01 Bump version to 2024.10.1-beta.5 2024-10-14 02:34:17 +00:00
かっこかり 8b7290d6b0
enhance(backend): 個人宛のお知らせはわかったを押すとアーカイブするように (#14762)
* enhance(backend): 個人宛のお知らせはわかったを押すとアーカイブするように

* Update Changelog

* enhance(frontend): アーカイブ済みのものを読み込めるように

* Update Changelog

* fix changelog

* 🎨
2024-10-14 11:23:26 +09:00
syuilo 521d92014d
New Crowdin updates (#14753)
* New translations ja-jp.yml (Chinese Traditional)

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

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

* New translations ja-jp.yml (Korean)

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

* New translations ja-jp.yml (Catalan)

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

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

* New translations ja-jp.yml (Korean)

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

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

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)
2024-10-14 11:22:51 +09:00
syuilo 2190092de6 perf(frontend): ノートのレンダリングをスキップできるように 2024-10-14 11:22:02 +09:00
syuilo 064d6ca56f fix(backend): RBT有効時、リノートのリアクションが反映されない問題を修正 2024-10-14 09:11:03 +09:00
かっこかり d0bb0b51f5
fix(frontend): タイムラインで、広告がない際にも広告のwrapperが出てしまうのを修正 (#14763) 2024-10-14 03:06:10 +09:00
かっこかり 088e05ea66
fix(frontend): 使用されているexposeを復活させる (#14764) 2024-10-14 02:54:01 +09:00
github-actions[bot] fb23b24f5c Bump version to 2024.10.1-beta.4 2024-10-13 11:43:27 +00:00
おさむのひと 33b34ad7b8
feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知 (#14757)
* feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知

* fix misskey-js.api.md

* Revert "feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知"

This reverts commit 3ab953bdf8.

* 通知をやめてユーザ単位でのお知らせ機能に変更

* テスト用実装を戻す

* Update packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* fix remove empty then

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-13 20:32:12 +09:00
syuilo 5229f5de4d refactor(backend): remove unnecessary .then 2024-10-13 20:32:02 +09:00
syuilo ff47fef572
feat: リモートサーバーのサーバー情報を収集しないオプション (#14634)
* wip

* wip

* Update FetchInstanceMetadataService.ts

* Update FetchInstanceMetadataService.ts

* Update types.ts
2024-10-13 20:22:16 +09:00
かっこかり 45d42b8641
feat: ユーザーの名前に禁止ワードを設定できるように (#14756)
* wip

* 🎨

* Enhance: モデレーター以上は制限の影響を受けないように

* refactor

* better error handling

* fix

* Revert "better error handling"

This reverts commit 5670b29cfa.

* error handling

* エラーが出ないのを修正

* translation

* Update Changelog

* status code

* ✌️

* モデレーター以上は影響ないことを明記

* 🎨

* update changelog

* spdx

* Update update.ts

* refactor

* eliminate `screen name`

* remove untracked file

---------

Co-authored-by: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com>
2024-10-13 20:21:25 +09:00
syuilo c4c69cd267 🎨 2024-10-12 11:28:58 +09:00
FineArchs ee08e9f51e
refactor: MkStickyContainerで<style />を使う (#14755)
* remove rootEL ref

* use css module

* use v-bind in css

* --MI prefix

* remove unused ref

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-12 11:20:55 +09:00
syuilo 85bb1ff1db 🎨 2024-10-12 11:18:26 +09:00
syuilo 824c51a19f fix(frontend): fix style
Fix #14754
2024-10-12 10:32:00 +09:00
syuilo ef90f83917 Update index.d.ts 2024-10-12 10:31:40 +09:00
syuilo a87a18f40d Update about-misskey.vue 2024-10-12 10:11:55 +09:00
かっこかり 2f09d69773
fix(backend): キューのエラーログを簡略化するように (#14748)
* reduce federation log spam

* Don't record stack trace for unrecoverable errors.
* Avoid logging duplicate stace traces.

(cherry picked from commit ed0570110bf8cb8e8959591dccfa3c35999106ce)

* improve error summaries

(cherry picked from commit 20dd66f735d9778df0371001e303549dce619260)

* fix lint errors

(cherry picked from commit 83869e1c470b12b3bf4b23d885514d926620662a)

* condense job info

(cherry picked from commit 786702e076ad1af14538849512ad31c0ced7afe6)

* fix maxAttempts calculation

(cherry picked from commit b4d10aa8f821e594ec9c907eb2a5bdb3c73c67d5)

* condense error info

(cherry picked from commit f62cd8941ced74a4865aa5eae4f4a1c7aa1d30f1)

* normalize ID logging

(cherry picked from commit d8e1e4890d28347239162e26235eb68b1ff96654)

* further condense error details

(cherry picked from commit d867c2089b3b24680df0713a2aa0914789e45670)

* collapse AbortErrors

(cherry picked from commit 5171ba7113ebc7242527768afb9ab4cec534e3b3)

* don't log job name unless it has one

(cherry picked from commit a5316c06ed770b60f7b4c7ff5aa8c71cc0558db7)

* Update Changelog

* Record origin

---------

Co-authored-by: Hazel K <acomputerdog@gmail.com>
2024-10-11 21:29:03 +09:00
github-actions[bot] 777804605e Bump version to 2024.10.1-beta.3 2024-10-11 12:13:47 +00:00
syuilo af1cbc131f
wip (#14745) 2024-10-11 21:05:53 +09:00
syuilo c397b42242 chore: add description 2024-10-11 21:01:50 +09:00
おさむのひと a2cd6a7709
feat(backend): 7日間運営のアクティビティがないサーバを自動的に招待制にする (#14746)
* feat(backend): 7日間運営のアクティビティがないサーバを自動的に招待制にする

* fix RoleService.

* fix

* fix

* fix

* add test and fix

* fix

* fix CHANGELOG.md

* fix test
2024-10-11 20:59:36 +09:00
FineArchs 12bc671511
fix: admin/emoji/update で不正なエラーが発生する (#14750)
* fix emoji updating bug

* update changelog

* type fix

* " -> '

* conprehensiveness check

* lint

* undefined -> null
2024-10-11 17:17:45 +09:00
かっこかり d376aab45e
Update CHANGELOG.md (書き方を揃える) 2024-10-10 17:39:20 +09:00
syuilo 1ad3148533 clean up 2024-10-10 17:35:10 +09:00
github-actions[bot] 132c4ba6ce Bump version to 2024.10.1-beta.2 2024-10-10 07:24:24 +00:00
syuilo 67a5fccb3b Update CHANGELOG.md 2024-10-10 16:16:47 +09:00
syuilo 4c84842f3d 🎨 2024-10-10 16:14:32 +09:00
syuilo 54849bde6c clean up 2024-10-10 16:14:11 +09:00
かっこかり b668d161a9
refactor(frontend): prefix css variables (UI) (#14739)
* refactor(frontend): prefix css variables

* `MI_UI` -> `MI`

* fix

* `stickyBottom`

* stickyTop
2024-10-10 16:12:16 +09:00
371 changed files with 8429 additions and 1646 deletions

2
.github/labeler.yml vendored
View file

@ -6,7 +6,7 @@
'packages/backend:test': 'packages/backend:test':
- any: - any:
- changed-files: - changed-files:
- any-glob-to-any-file: ['packages/backend/test/**/*'] - any-glob-to-any-file: ['packages/backend/test/**/*', 'packages/backend/test-federation/**/*']
'packages/frontend': 'packages/frontend':
- any: - any:

View file

@ -113,7 +113,7 @@ jobs:
- name: send message - name: send message
if: steps.check-changes.outputs.changes == 'true' if: steps.check-changes.outputs.changes == 'true'
uses: thollander/actions-comment-pull-request@v3 uses: thollander/actions-comment-pull-request@v2
with: with:
comment_tag: check-misskey-js-autogen comment_tag: check-misskey-js-autogen
message: |- message: |-
@ -127,7 +127,7 @@ jobs:
- name: send message - name: send message
if: steps.check-changes.outputs.changes == 'false' if: steps.check-changes.outputs.changes == 'false'
uses: thollander/actions-comment-pull-request@v3 uses: thollander/actions-comment-pull-request@v2
with: with:
comment_tag: check-misskey-js-autogen comment_tag: check-misskey-js-autogen
mode: delete mode: delete

View file

@ -89,13 +89,13 @@ jobs:
fi fi
echo "$FOOTER" >> ./output.md echo "$FOOTER" >> ./output.md
- uses: thollander/actions-comment-pull-request@v3 - uses: thollander/actions-comment-pull-request@v2
with: with:
pr_number: ${{ steps.load-pr-num.outputs.pr-number }} pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
comment_tag: show_diff comment_tag: show_diff
filePath: ./output.md filePath: ./output.md
- name: Tell error to PR - name: Tell error to PR
uses: thollander/actions-comment-pull-request@v3 uses: thollander/actions-comment-pull-request@v2
if: failure() && steps.load-pr-num.outputs.pr-number if: failure() && steps.load-pr-num.outputs.pr-number
with: with:
pr_number: ${{ steps.load-pr-num.outputs.pr-number }} pr_number: ${{ steps.load-pr-num.outputs.pr-number }}

59
.github/workflows/test-federation.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: Test (federation)
on:
push:
branches:
- master
- develop
paths:
- packages/backend/**
- packages/misskey-js/**
- .github/workflows/test-federation.yml
pull_request:
paths:
- packages/backend/**
- packages/misskey-js/**
- .github/workflows/test-federation.yml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.16.0]
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Build Misskey
run: |
corepack enable && corepack prepare
pnpm i --frozen-lockfile
pnpm build
- name: Setup
run: |
cd packages/backend/test-federation
bash ./setup.sh
sudo chmod 644 ./certificates/*.test.key
- name: Start servers
# https://github.com/docker/compose/issues/1294#issuecomment-374847206
run: |
cd packages/backend/test-federation
docker compose up -d --scale tester=0
- name: Test
run: |
cd packages/backend/test-federation
docker compose run --no-deps tester
- name: Stop servers
run: |
cd packages/backend/test-federation
docker compose down

2
.gitignore vendored
View file

@ -37,7 +37,7 @@ coverage
!/.config/docker_example.env !/.config/docker_example.env
!/.config/cypress-devcontainer.yml !/.config/cypress-devcontainer.yml
docker-compose.yml docker-compose.yml
compose.yml ./compose.yml
.devcontainer/compose.yml .devcontainer/compose.yml
!/.devcontainer/compose.yml !/.devcontainer/compose.yml

View file

@ -1,14 +1,77 @@
## 2024.10.1 ## 2024.11.0
### General ### General
- - Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Enhance: 依存関係の更新
- Enhance: l10nの更新
### Client ### Client
- メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正 - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
- Enhance: ドライブでソートができるように
- Enhance: アイコンデコレーション管理画面の改善
- Enhance: 「単なるラッキー」の取得条件を変更
- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように #10866
- Enhance: MiAuth, OAuthの認可画面の改善
- どのアカウントで認証しようとしているのかがわかるように
- 認証するアカウントを切り替えられるように
- Enhance: Self-XSS防止用の警告を追加
- Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
- Fix: リンク切れを修正
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
### Server ### Server
- - Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように
- Fix: Nested proxy requestsを検出した際にブロックするように
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
- Fix: ローカルユーザーへのメンションを含むートが連合される際に正しいURLに変換されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Fix: User Webhookテスト機能のMock Payloadを修正
### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
## 2024.10.1
### Note
- スパム対策として、モデレータ権限を持つユーザのアクティビティが7日以上確認できない場合は自動的に招待制へと切り替えコントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)るようになりました。 ( #13437 )
- 切り替わった際はモデレーターへお知らせとして通知されます。登録をオープンな状態で継続したい場合は、コントロールパネルから再度設定を行ってください。
### General
- Feat: ユーザーの名前に禁止ワードを設定できるように
### Client
- Enhance: タイムライン表示時のパフォーマンスを向上
- Enhance: アーカイブした個人宛のお知らせを表示・編集できるように
- Enhance: l10nの更新
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
### Server
- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと切り替えるように ( #13437 )
- Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように
- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
- Fix: RBT有効時、リートのリアクションが反映されない問題を修正
- Fix: キューのエラーログを簡略化するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
## 2024.10.0 ## 2024.10.0

View file

@ -64,9 +64,29 @@ Thank you for your PR! Before creating a PR, please check the following:
Thanks for your cooperation 🤗 Thanks for your cooperation 🤗
### Additional things for ActivityPub payload changes
*This section is specific to misskey-dev implementation. Other fork or implementation may take different way. A significant difference is that non-"misskey-dev" extension is not described in the misskey-hub's document.*
If PR includes changes to ActivityPub payload, please reflect it in [misskey-hub's document](https://github.com/misskey-dev/misskey-hub-next/blob/master/content/ns.md) by sending PR.
The name of purporsed extension property (referred as "extended property" in later) to ActivityPub shall be prefixed by `_misskey_`. (i.e. `_misskey_quote`)
The extended property in `packages/backend/src/core/activitypub/type.ts` **must** be declared as optional because ActivityPub payloads that comes from older Misskey or other implementation may not contain it.
The extended property must be included in the context definition. Context is defined in `packages/backend/src/core/activitypub/misc/contexts.ts`.
The key shall be same as the name of extended property, and the value shall be same as "short IRI".
"Short IRI" is defined in misskey-hub's document, but usually takes form of `misskey:<name of extended property>`. (i.e. `misskey:_misskey_quote`)
One should not add property that has defined before by other implementation, or add custom variant value to "well-known" property.
## Reviewers guide ## Reviewers guide
Be willing to comment on the good points and not just the things you want fixed 💯 Be willing to comment on the good points and not just the things you want fixed 💯
読んでおくといいやつ
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421
### Review perspective ### Review perspective
- Scope - Scope
- Are the goals of the PR clear? - Are the goals of the PR clear?
@ -116,7 +136,8 @@ You can improve our translations with your Crowdin account.
Your changes in Crowdin are automatically submitted as a PR (with the title "New Crowdin translations") to the repository. Your changes in Crowdin are automatically submitted as a PR (with the title "New Crowdin translations") to the repository.
The owner [@syuilo](https://github.com/syuilo) merges the PR into the develop branch before the next release. The owner [@syuilo](https://github.com/syuilo) merges the PR into the develop branch before the next release.
If your language is not listed in Crowdin, please open an issue. If your language is not listed in Crowdin, please open an issue. We will add it to Crowdin.
For newly added languages, once the translation progress per language exceeds 70%, it will be officially introduced into Misskey and made available to users.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
@ -181,31 +202,45 @@ MK_DEV_PREFER=backend pnpm dev
- HMR may not work in some environments such as Windows. - HMR may not work in some environments such as Windows.
## Testing ## Testing
- Test codes are located in [`/packages/backend/test`](/packages/backend/test). You can run non-backend tests by executing following commands:
```sh
### Run test pnpm --filter frontend test
Create a config file. pnpm --filter misskey-js test
``` ```
Backend tests require manual preparation of servers. See the next section for more on this.
### Backend
There are three types of test codes for the backend:
- Unit tests: [`/packages/backend/test/unit`](/packages/backend/test/unit)
- Single-server E2E tests: [`/packages/backend/test/e2e`](/packages/backend/test/e2e)
- Multiple-server E2E tests: [`/packages/backend/test-federation`](/packages/backend/test-federation)
#### Running Unit Tests or Single-server E2E Tests
1. Create a config file:
```sh
cp .github/misskey/test.yml .config/ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing.
``` 2. Start DB and Redis servers for testing:
```sh
docker compose -f packages/backend/test/compose.yml up docker compose -f packages/backend/test/compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Instead, you can prepare an empty (data can be erased) DB and edit `.config/test.yml` appropriately.
Run all test. 3. Run all tests:
```sh
pnpm --filter backend test # unit tests
pnpm --filter backend test:e2e # single-server E2E tests
``` ```
pnpm test If you want to run a specific test, run as a following command:
```sh
pnpm --filter backend test -- packages/backend/test/unit/activitypub.ts
pnpm --filter backend test:e2e -- packages/backend/test/e2e/nodeinfo.ts
``` ```
#### Run specify test #### Running Multiple-server E2E Tests
``` See [`/packages/backend/test-federation/README.md`](/packages/backend/test-federation/README.md).
pnpm jest -- foo.ts
```
### e2e tests
TODO
## Environment Variable ## Environment Variable

View file

@ -2,7 +2,7 @@
_lang_: "Català" _lang_: "Català"
headlineMisskey: "Una xarxa connectada per notes" headlineMisskey: "Una xarxa connectada per notes"
introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat de codi obert.\nCrea \"notes\" per compartir els teus pensaments amb tots els que t'envolten. 📡\nAmb \"reaccions\", també pots expressar ràpidament els teus sentiments sobre les notes de tothom. 👍\nExplorem un món nou! 🚀" introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat de codi obert.\nCrea \"notes\" per compartir els teus pensaments amb tots els que t'envolten. 📡\nAmb \"reaccions\", també pots expressar ràpidament els teus sentiments sobre les notes de tothom. 👍\nExplorem un món nou! 🚀"
poweredByMisskeyDescription: "{name} És un del serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert <b>Misskey</b>." poweredByMisskeyDescription: "{name} És un dels serveis (anomenats instàncies de Misskey) que utilitzen la plataforma de codi obert <b>Misskey</b>."
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Cercar" search: "Cercar"
notifications: "Notificacions" notifications: "Notificacions"
@ -10,6 +10,7 @@ username: "Nom d'usuari"
password: "Contrasenya" password: "Contrasenya"
initialPasswordForSetup: "Contrasenya inicial per la configuració inicial" initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
initialPasswordIsIncorrect: "La contrasenya no és correcta." initialPasswordIsIncorrect: "La contrasenya no és correcta."
initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc."
forgotPassword: "Contrasenya oblidada" forgotPassword: "Contrasenya oblidada"
fetchingAsApObject: "Cercant en el Fediverse..." fetchingAsApObject: "Cercant en el Fediverse..."
ok: "OK" ok: "OK"
@ -17,7 +18,7 @@ gotIt: "Ho he entès!"
cancel: "Cancel·lar" cancel: "Cancel·lar"
noThankYou: "No, gràcies" noThankYou: "No, gràcies"
enterUsername: "Introdueix el teu nom d'usuari" enterUsername: "Introdueix el teu nom d'usuari"
renotedBy: "Impulsat per {usuari}" renotedBy: "Impulsat per {user}"
noNotes: "Cap nota" noNotes: "Cap nota"
noNotifications: "Cap notificació" noNotifications: "Cap notificació"
instance: "Servidor" instance: "Servidor"
@ -946,6 +947,9 @@ oneHour: "1 hora"
oneDay: "Un dia" oneDay: "Un dia"
oneWeek: "Una setmana" oneWeek: "Una setmana"
oneMonth: "Un mes" oneMonth: "Un mes"
threeMonths: "3 mesos"
oneYear: "1 any"
threeDays: "3 dies"
reflectMayTakeTime: "Això pot trigar una estona a tenir efecte" reflectMayTakeTime: "Això pot trigar una estona a tenir efecte"
failedToFetchAccountInformation: "No es pot obtenir la informació del compte" failedToFetchAccountInformation: "No es pot obtenir la informació del compte"
rateLimitExceeded: "S'ha arribat al màxim de peticions" rateLimitExceeded: "S'ha arribat al màxim de peticions"
@ -1086,6 +1090,7 @@ retryAllQueuesConfirmTitle: "Tornar a intentar-ho tot?"
retryAllQueuesConfirmText: "Això farà que la càrrega del servidor augmenti temporalment." retryAllQueuesConfirmText: "Això farà que la càrrega del servidor augmenti temporalment."
enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots" enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots"
enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes" enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes"
enableStatsForFederatedInstances: "Activa les estadístiques de les instàncies remotes federades"
showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota" showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota"
reactionsDisplaySize: "Mida de les reaccions" reactionsDisplaySize: "Mida de les reaccions"
limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda " limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda "
@ -1286,6 +1291,26 @@ passkeyVerificationFailed: "La verificació a fallat"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya." passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya."
messageToFollower: "Missatge als meus seguidors" messageToFollower: "Missatge als meus seguidors"
target: "Assumpte " target: "Assumpte "
testCaptchaWarning: "És una característica dissenyada per a la prova de CAPTCHA. <strong>No l'utilitzes en l'entorn real.</strong>"
prohibitedWordsForNameOfUser: "Noms prohibits per escollir noms d'usuari "
prohibitedWordsForNameOfUserDescription: "Si qualsevol d'aquestes paraules es troben a un nom d'usuari la creació de l'usuari no es durà a terme. Als moderadors no els afecta aquesta restricció."
yourNameContainsProhibitedWords: "El nom conté paraules prohibides "
yourNameContainsProhibitedWordsDescription: "Si de veritat vols fer servir aquest nom posat en contacte amb l'administrador."
thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure"
lockdown: "Bloquejat"
pleaseSelectAccount: "Seleccionar un compte"
_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ó."
requireSigninToViewContentsDescription2: "També es desactivaran les vistes prèvies d'URLS (OGP), la incrustació a pàgines web i la visualització des de servidors que no admetin la citació de notes."
requireSigninToViewContentsDescription3: "Aquestes restriccions pot ser que no s'apliquin als continguts federats en servidors remots."
makeNotesFollowersOnlyBefore: "Permetre que les notes antigues només es mostrin als seguidors."
makeNotesFollowersOnlyBeforeDescription: "Mentre aquesta funció estigui activada, les notes que hagin passat la data i hora fixada o hagi passat els temps establert seran visibles només per als teus seguidors. Quan es desactivi, també es restableix l'estat públic de la nota."
makeNotesHiddenBefore: "Fes que les notes antigues siguin privades"
makeNotesHiddenBeforeDescription: "Mentres aquesta funció estigui activada les notes que hagin superat una data i hora fixada o hagi passat el temps establert només seran visibles per a tu. Si la desactives es restablirà també l'estat públic de les notes."
mayNotEffectForFederatedNotes: "Això pot ser que no afecti les notes federades."
notesHavePassedSpecifiedPeriod: "Notes publicades durant un període de temps especificat."
notesOlderThanSpecifiedDateAndTime: "Notes més antigues de la data i temps especificat "
_abuseUserReport: _abuseUserReport:
forward: "Reenviar " forward: "Reenviar "
forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima." forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima."
@ -1430,6 +1455,7 @@ _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." 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 " 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ó." 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ó."
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: _accountMigration:
moveFrom: "Migrar un altre compte a aquest" moveFrom: "Migrar un altre compte a aquest"
moveFromSub: "Crear un àlies per un altre compte" moveFromSub: "Crear un àlies per un altre compte"
@ -2149,8 +2175,11 @@ _auth:
permissionAsk: "Aquesta aplicació demana els següents permisos" permissionAsk: "Aquesta aplicació demana els següents permisos"
pleaseGoBack: "Si us plau, torna a l'aplicació" pleaseGoBack: "Si us plau, torna a l'aplicació"
callback: "Tornant a l'aplicació" callback: "Tornant a l'aplicació"
accepted: "Accés garantit"
denied: "Accés denegat" denied: "Accés denegat"
scopeUser: "Opera com si fossis aquest usuari"
pleaseLogin: "Si us plau, identificat per autoritzar l'aplicació." pleaseLogin: "Si us plau, identificat per autoritzar l'aplicació."
byClickingYouWillBeRedirectedToThisUrl: "Si es garanteix l'accés, seràs redirigit automàticament a la següent adreça URL"
_antennaSources: _antennaSources:
all: "Totes les publicacions" all: "Totes les publicacions"
homeTimeline: "Publicacions dels usuaris seguits" homeTimeline: "Publicacions dels usuaris seguits"
@ -2400,7 +2429,8 @@ _notification:
renotedBySomeUsers: "L'han impulsat {n} usuaris" renotedBySomeUsers: "L'han impulsat {n} usuaris"
followedBySomeUsers: "Et segueixen {n} usuaris" followedBySomeUsers: "Et segueixen {n} usuaris"
flushNotification: "Netejar notificacions" flushNotification: "Netejar notificacions"
exportOfXCompleted: "Completada l'exportació de {n}" exportOfXCompleted: "Completada l'exportació de {x}"
login: "Algú ha iniciat sessió "
_types: _types:
all: "Tots" all: "Tots"
note: "Notes noves" note: "Notes noves"
@ -2483,6 +2513,8 @@ _webhookSettings:
abuseReport: "Quan reps un nou informe de moderació " abuseReport: "Quan reps un nou informe de moderació "
abuseReportResolved: "Quan resols un informe de moderació " abuseReportResolved: "Quan resols un informe de moderació "
userCreated: "Quan es crea un usuari" userCreated: "Quan es crea un usuari"
inactiveModeratorsWarning: "Quan el compte d'un moderador no té activitat durant un temps"
inactiveModeratorsInvitationOnlyChanged: "Quan el compte d'un moderador no té activitat durant un temps, i el servidor es canvia a registre per invitacions"
deleteConfirm: "Segur que vols esborrar el webhook?" deleteConfirm: "Segur que vols esborrar el webhook?"
testRemarks: "Si feu clic al botó a la dreta de l'interruptor, podeu enviar un webhook de prova amb dades dummy." testRemarks: "Si feu clic al botó a la dreta de l'interruptor, podeu enviar un webhook de prova amb dades dummy."
_abuseReport: _abuseReport:
@ -2610,8 +2642,81 @@ _dataSaver:
description: "Les imatges en miniatura que serveixen com a vista prèvia de les URLs no es tornaran a carregar." description: "Les imatges en miniatura que serveixen com a vista prèvia de les URLs no es tornaran a carregar."
_code: _code:
title: "Ressaltat del codi " title: "Ressaltat del codi "
description: "Quan s'utilitza codi MFM, no es llegeix fins que es copiï. En els punts destacats del codi s'han de llegir els fitxers definits per a cada llengua que resulti alt, però no es poden llegir automàticament, per la qual cosa es poden reduir les quantitats de comunicació."
_hemisphere:
N: "Hemisferi Nord "
S: "Hemisferi Sud"
caption: "El fan servir alguns clients per determinar l'estació de l'any."
_reversi: _reversi:
reversi: "Reversi"
gameSettings: "Opcions del joc"
chooseBoard: "Escull un taulell"
blackOrWhite: "Negres/Blanques"
blackIs: "{name} juga amb negres "
rules: "Regles"
thisGameIsStartedSoon: "El joc començarà en breu"
waitingForOther: "Esperant la tirada de l'oponent "
waitingForMe: "Esperant el teu torn"
waitingBoth: "Prepara't "
ready: "Preparat "
cancelReady: " No preparat "
opponentTurn: "Torn de l'oponent "
myTurn: "El teu torn"
turnOf: "Li toca a {name}"
pastTurnOf: "Torn de {name}"
surrender: "Rendeix-te"
surrendered: "T'has rendit"
timeout: "Temps esgotat"
drawn: "Empat"
won: "{name} ha guanyat"
black: "Negres"
white: "Blanques"
total: "Total" total: "Total"
turnCount: "Torn {count}"
myGames: "Jugades"
allGames: "Totes les jugades"
ended: "Acabat"
playing: "Jugant"
isLlotheo: "Qui tingui menys pedres guanya (Llotheo)"
loopedMap: "Mapa de recursiu"
canPutEverywhere: "Les fitxes es poden posar a qualsevol lloc"
timeLimitForEachTurn: "Temps límit per jugada"
freeMatch: "Partida lliure"
lookingForPlayer: "Buscant contrincant..."
gameCanceled: "La partida s'ha cancel·lat "
shareToTlTheGameWhenStart: "Compartir la partida a la línia de temps quan comenci"
iStartedAGame: "La partida ha començat! #MisskeyReversi"
opponentHasSettingsChanged: "L'oponent h canviat la seva configuració "
allowIrregularRules: "Regles irregulars (totalment lliure)"
disallowIrregularRules: "Sense regles irregulars"
showBoardLabels: "Mostrar el número de línia i columna al tauler de joc"
useAvatarAsStone: "Fer servir els avatars dels usuaris com a fitxes"
_offlineScreen:
title: "Fora de línia - No es pot connectar amb el servidor"
header: "Impossible connectar amb el servidor"
_urlPreviewSetting:
title: "Configuració per a la previsualització de l'URL"
enable: "Activa la previsualització de l'URL"
timeout: "Temps màxim per carregar la previsualització de l'URL (ms)"
timeoutDescription: "Si l'obtenció de la previsualització triga més que el temps establert, no es generarà la vista prèvia."
maximumContentLength: "Longitud màxima del contingut (bytes)"
maximumContentLengthDescription: "Si la màxima longitud és més gran que aquest valor, la previsualització no es generarà."
requireContentLength: "Generar la previsualització només si es pot obtenir la longitud màxima "
requireContentLengthDescription: "Si l'altre servidor no proporciona la longitud màxima, la previsualització no es generarà."
userAgent: "User-Agent"
userAgentDescription: "Estableix l'User-Agent que és farà servir per a la recuperació de la vista prèvia. Si és deixa en blanc es farà servir l'User-Agent per defecte."
summaryProxy: "Proxy endpoints per generar vistes prèvies"
summaryProxyDescription: "La vista prèvia es genera fent servir Summaly proxy, no la genera el mateix Misskey."
summaryProxyDescription2: "Els següents paràmetres són passats al proxy com cadenes de consulta. Si el proxy no els admet, s'ignoren els valors configurats."
_mediaControls:
pip: "Imatge sobre impressionada "
playbackRate: "Velocitat de reproducció "
loop: "Reproducció en bucle"
_contextMenu:
title: "Menú contextual"
app: "Aplicació "
appWithShift: "Aplicació amb la tecla shift"
native: "Interfície del navegador"
_embedCodeGen: _embedCodeGen:
title: "Personalitza el codi per incrustar" title: "Personalitza el codi per incrustar"
header: "Mostrar la capçalera" header: "Mostrar la capçalera"
@ -2626,3 +2731,9 @@ _embedCodeGen:
generateCode: "Crea el codi per incrustar" generateCode: "Crea el codi per incrustar"
codeGenerated: "Codi generat" codeGenerated: "Codi generat"
codeGeneratedDescription: "Si us plau, enganxeu el codi generat al lloc web." codeGeneratedDescription: "Si us plau, enganxeu el codi generat al lloc web."
_selfXssPrevention:
warning: "Advertència "
title: "\"Enganxa qualsevol cosa en aquesta finestra\" És tot un engany."
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}"

View file

@ -331,7 +331,6 @@ selectFile: "Select a file"
selectFiles: "Select files" selectFiles: "Select files"
selectFolder: "Select a folder" selectFolder: "Select a folder"
selectFolders: "Select folders" selectFolders: "Select folders"
fileNotSelected: ""
renameFile: "Rename file" renameFile: "Rename file"
folderName: "Folder name" folderName: "Folder name"
createFolder: "Create a folder" createFolder: "Create a folder"
@ -947,6 +946,9 @@ oneHour: "One hour"
oneDay: "One day" oneDay: "One day"
oneWeek: "One week" oneWeek: "One week"
oneMonth: "One month" oneMonth: "One month"
threeMonths: "3 months"
oneYear: "1 year"
threeDays: "3 days"
reflectMayTakeTime: "It may take some time for this to be reflected." reflectMayTakeTime: "It may take some time for this to be reflected."
failedToFetchAccountInformation: "Could not fetch account information" failedToFetchAccountInformation: "Could not fetch account information"
rateLimitExceeded: "Rate limit exceeded" rateLimitExceeded: "Rate limit exceeded"
@ -1087,6 +1089,7 @@ retryAllQueuesConfirmTitle: "Really retry all?"
retryAllQueuesConfirmText: "This will temporarily increase the server load." retryAllQueuesConfirmText: "This will temporarily increase the server load."
enableChartsForRemoteUser: "Generate remote user data charts" enableChartsForRemoteUser: "Generate remote user data charts"
enableChartsForFederatedInstances: "Generate remote instance data charts" enableChartsForFederatedInstances: "Generate remote instance data charts"
enableStatsForFederatedInstances: "Receive remote server stats"
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
reactionsDisplaySize: "Reaction display size" reactionsDisplaySize: "Reaction display size"
limitWidthOfReaction: "Limit the maximum width of reactions and display them in reduced size." limitWidthOfReaction: "Limit the maximum width of reactions and display them in reduced size."
@ -1287,6 +1290,26 @@ passkeyVerificationFailed: "Passkey verification has failed."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled." passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
messageToFollower: "Message to followers" messageToFollower: "Message to followers"
target: "Target" target: "Target"
testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\n<strong>Do not use in a production environment.</strong>"
prohibitedWordsForNameOfUser: "Prohibited words for user names"
prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction."
yourNameContainsProhibitedWords: "Your name contains prohibited words"
yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator."
thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
lockdown: "Lockdown"
pleaseSelectAccount: "Select an account"
_accountSettings:
requireSigninToViewContents: "Require sign-in to view contents"
requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
requireSigninToViewContentsDescription2: "Content will not be displayed in URL previews (OGP), embedded in web pages, or on servers that don't support note quotes."
requireSigninToViewContentsDescription3: "These restrictions may not apply to federated content from other remote servers."
makeNotesFollowersOnlyBefore: "Make past notes to be displayed only to followers"
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."
notesHavePassedSpecifiedPeriod: "Note that the specified time has passed"
notesOlderThanSpecifiedDateAndTime: "Notes before the specified date and time"
_abuseUserReport: _abuseUserReport:
forward: "Forward" forward: "Forward"
forwardDescription: "Forward the report to a remote server as an anonymous system account." forwardDescription: "Forward the report to a remote server as an anonymous system account."
@ -1431,6 +1454,7 @@ _serverSettings:
reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase."
inquiryUrl: "Inquiry URL" inquiryUrl: "Inquiry URL"
inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam."
_accountMigration: _accountMigration:
moveFrom: "Migrate another account to this one" moveFrom: "Migrate another account to this one"
moveFromSub: "Create alias to another account" moveFromSub: "Create alias to another account"
@ -2150,8 +2174,11 @@ _auth:
permissionAsk: "This application requests the following permissions" permissionAsk: "This application requests the following permissions"
pleaseGoBack: "Please go back to the application" pleaseGoBack: "Please go back to the application"
callback: "Returning to the application" callback: "Returning to the application"
accepted: "Access granted"
denied: "Access denied" denied: "Access denied"
scopeUser: "Operate as the following user"
pleaseLogin: "Please log in to authorize applications." pleaseLogin: "Please log in to authorize applications."
byClickingYouWillBeRedirectedToThisUrl: "When access is granted, you will automatically be redirected to the following URL"
_antennaSources: _antennaSources:
all: "All notes" all: "All notes"
homeTimeline: "Notes from followed users" homeTimeline: "Notes from followed users"
@ -2485,6 +2512,8 @@ _webhookSettings:
abuseReport: "When received a new report" abuseReport: "When received a new report"
abuseReportResolved: "When resolved report" abuseReportResolved: "When resolved report"
userCreated: "When user is created" userCreated: "When user is created"
inactiveModeratorsWarning: "When moderators have been inactive for a while"
inactiveModeratorsInvitationOnlyChanged: "When a moderator has been inactive for a while, and the server is changed to invitation-only"
deleteConfirm: "Are you sure you want to delete the Webhook?" deleteConfirm: "Are you sure you want to delete the Webhook?"
testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data." testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
_abuseReport: _abuseReport:

View file

@ -8,6 +8,8 @@ search: "Buscar"
notifications: "Notificaciones" notifications: "Notificaciones"
username: "Nombre de usuario" username: "Nombre de usuario"
password: "Contraseña" password: "Contraseña"
initialPasswordForSetup: "Contraseña para iniciar la inicialización"
initialPasswordIsIncorrect: "La contraseña para iniciar la configuración inicial es incorrecta."
forgotPassword: "Olvidé mi contraseña" forgotPassword: "Olvidé mi contraseña"
fetchingAsApObject: "Buscando en el fediverso" fetchingAsApObject: "Buscando en el fediverso"
ok: "OK" ok: "OK"
@ -502,6 +504,8 @@ uiLanguage: "Idioma de visualización de la interfaz"
aboutX: "Acerca de {x}" aboutX: "Acerca de {x}"
emojiStyle: "Estilo de emoji" emojiStyle: "Estilo de emoji"
native: "Nativo" native: "Nativo"
menuStyle: "Diseño del menú"
style: "Diseño"
showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor"
showReactionsCount: "Mostrar el número de reacciones en las notas" showReactionsCount: "Mostrar el número de reacciones en las notas"
noHistory: "No hay datos en el historial" noHistory: "No hay datos en el historial"
@ -925,6 +929,9 @@ oneHour: "1 hora"
oneDay: "1 día" oneDay: "1 día"
oneWeek: "1 semana" oneWeek: "1 semana"
oneMonth: "1 mes" oneMonth: "1 mes"
threeMonths: "Tres meses"
oneYear: "Un año"
threeDays: "Tres días"
reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios" reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios"
failedToFetchAccountInformation: "No se pudo obtener información de la cuenta" failedToFetchAccountInformation: "No se pudo obtener información de la cuenta"
rateLimitExceeded: "Se excedió el límite de peticiones" rateLimitExceeded: "Se excedió el límite de peticiones"
@ -1240,6 +1247,14 @@ useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reprod
keepOriginalFilename: "Mantener el nombre original del archivo" keepOriginalFilename: "Mantener el nombre original del archivo"
noDescription: "No hay descripción" noDescription: "No hay descripción"
alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien" alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien"
inquiry: "Contacto"
tryAgain: "Por favor , inténtalo de nuevo"
performance: "Rendimiento"
unknownWebAuthnKey: "Esto no se ha registrado llave maestra."
messageToFollower: "Mensaje a seguidores"
_abuseUserReport:
accept: "Acepte"
reject: "repudio"
_delivery: _delivery:
stop: "Suspendido" stop: "Suspendido"
_type: _type:
@ -2340,6 +2355,7 @@ _notification:
roleAssigned: "Rol asignado" roleAssigned: "Rol asignado"
achievementEarned: "Logro desbloqueado" achievementEarned: "Logro desbloqueado"
login: "Iniciar sesión" login: "Iniciar sesión"
test: "Pruebas de nofiticaciones"
app: "Notificaciones desde aplicaciones" app: "Notificaciones desde aplicaciones"
_actions: _actions:
followBack: "Te sigue de vuelta" followBack: "Te sigue de vuelta"
@ -2398,6 +2414,8 @@ _webhookSettings:
renote: "Cuando reciba un \"re-note\"" renote: "Cuando reciba un \"re-note\""
reaction: "Cuando se recibe una reacción" reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención" mention: "Cuando hay una mención"
_systemEvents:
userCreated: "Cuando se crea el usuario."
_abuseReport: _abuseReport:
_notificationRecipient: _notificationRecipient:
_recipientType: _recipientType:

View file

@ -1,5 +1,5 @@
--- ---
_lang_: "Japán" _lang_: "Magyar"
monthAndDay: "{month}.{day}." monthAndDay: "{month}.{day}."
search: "Keresés" search: "Keresés"
notifications: "Értesítések" notifications: "Értesítések"

View file

@ -196,6 +196,7 @@ followConfirm: "Apakah kamu yakin ingin mengikuti {name}?"
proxyAccount: "Akun proksi" proxyAccount: "Akun proksi"
proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya." proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya."
host: "Host" host: "Host"
selectSelf: "Pilih diri sendiri"
selectUser: "Pilih pengguna" selectUser: "Pilih pengguna"
recipient: "Penerima" recipient: "Penerima"
annotation: "Keterangan konten" annotation: "Keterangan konten"
@ -232,6 +233,7 @@ blockedInstances: "Instansi terblokir"
blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini." blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini."
silencedInstances: "Instansi yang disenyapkan" silencedInstances: "Instansi yang disenyapkan"
silencedInstancesDescription: "Daftar nama host dari instansi yang ingin kamu senyapkan. Semua akun dari instansi yang terdaftar akan diperlakukan sebagai disenyapkan. Hal ini membuat akun hanya dapat membuat permintaan mengikuti, dan tidak dapat menyebutkan akun lokal apabila tidak mengikuti. Hal ini tidak akan mempengaruhi instansi yang diblokir." silencedInstancesDescription: "Daftar nama host dari instansi yang ingin kamu senyapkan. Semua akun dari instansi yang terdaftar akan diperlakukan sebagai disenyapkan. Hal ini membuat akun hanya dapat membuat permintaan mengikuti, dan tidak dapat menyebutkan akun lokal apabila tidak mengikuti. Hal ini tidak akan mempengaruhi instansi yang diblokir."
federationAllowedHosts: "Server yang membolehkan federasi"
muteAndBlock: "Bisukan / Blokir" muteAndBlock: "Bisukan / Blokir"
mutedUsers: "Pengguna yang dibisukan" mutedUsers: "Pengguna yang dibisukan"
blockedUsers: "Pengguna yang diblokir" blockedUsers: "Pengguna yang diblokir"
@ -330,6 +332,7 @@ renameFolder: "Ubah nama folder"
deleteFolder: "Hapus folder" deleteFolder: "Hapus folder"
folder: "Folder" folder: "Folder"
addFile: "Tambahkan berkas" addFile: "Tambahkan berkas"
showFile: "Tampilkan berkas"
emptyDrive: "Drive kosong" emptyDrive: "Drive kosong"
emptyFolder: "Folder kosong" emptyFolder: "Folder kosong"
unableToDelete: "Tidak dapat menghapus" unableToDelete: "Tidak dapat menghapus"
@ -504,6 +507,8 @@ uiLanguage: "Bahasa antarmuka pengguna"
aboutX: "Tentang {x}" aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji" emojiStyle: "Gaya emoji"
native: "Native" native: "Native"
menuStyle: "Gaya menu"
style: "Gaya"
showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk"
showReactionsCount: "Lihat jumlah reaksi dalam catatan" showReactionsCount: "Lihat jumlah reaksi dalam catatan"
noHistory: "Tidak ada riwayat" noHistory: "Tidak ada riwayat"
@ -927,6 +932,9 @@ oneHour: "1 Jam"
oneDay: "1 Hari" oneDay: "1 Hari"
oneWeek: "1 Bulan" oneWeek: "1 Bulan"
oneMonth: "satu bulan" oneMonth: "satu bulan"
threeMonths: "3 bulan"
oneYear: "1 tahun"
threeDays: "3 hari"
reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan." reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan."
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
rateLimitExceeded: "Batas sudah terlampaui" rateLimitExceeded: "Batas sudah terlampaui"
@ -1101,6 +1109,7 @@ preservedUsernames: "Nama pengguna tercadangkan"
preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh." preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh."
createNoteFromTheFile: "Buat catatan dari berkas ini" createNoteFromTheFile: "Buat catatan dari berkas ini"
archive: "Arsipkan" archive: "Arsipkan"
archived: "Diarsipkan"
channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?" channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?"
channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi." channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi."
thisChannelArchived: "Kanal ini telah diarsipkan." thisChannelArchived: "Kanal ini telah diarsipkan."
@ -1111,6 +1120,7 @@ preventAiLearning: "Tolak penggunaan Pembelajaran Mesin (AI Generatif)"
preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja." preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja."
options: "Opsi peran" options: "Opsi peran"
specifyUser: "Pengguna spesifik" specifyUser: "Pengguna spesifik"
openTagPageConfirm: "Apakah ingin membuka laman tagar?"
failedToPreviewUrl: "Tidak dapat dipratinjau" failedToPreviewUrl: "Tidak dapat dipratinjau"
update: "Perbarui" update: "Perbarui"
rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi" rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi"
@ -1243,6 +1253,18 @@ noDescription: "Tidak ada deskripsi"
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti" alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
inquiry: "Hubungi kami" inquiry: "Hubungi kami"
tryAgain: "Silahkan coba lagi." tryAgain: "Silahkan coba lagi."
createdLists: "Senarai yang dibuat"
createdAntennas: "Antena yang dibuat"
fromX: "Dari {x}"
noteOfThisUser: "Catatan oleh pengguna ini"
clipNoteLimitExceeded: "Klip ini tak bisa ditambahi lagi catatan."
performance: "Kinerja"
modified: "Diubah"
thereAreNChanges: "Ada {n} perubahan"
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
_abuseUserReport:
accept: "Setuju"
reject: "Tolak"
_delivery: _delivery:
status: "Status pengiriman" status: "Status pengiriman"
stop: "Ditangguhkan" stop: "Ditangguhkan"
@ -1707,6 +1729,8 @@ _role:
canSearchNotes: "Penggunaan pencarian catatan" canSearchNotes: "Penggunaan pencarian catatan"
canUseTranslator: "Penggunaan penerjemah" canUseTranslator: "Penggunaan penerjemah"
avatarDecorationLimit: "Jumlah maksimum dekorasi avatar yang dapat diterapkan" avatarDecorationLimit: "Jumlah maksimum dekorasi avatar yang dapat diterapkan"
canImportAntennas: "Izinkan mengimpor antena"
canImportUserLists: "Izinkan mengimpor senarai"
_condition: _condition:
roleAssignedTo: "Ditugaskan ke peran manual" roleAssignedTo: "Ditugaskan ke peran manual"
isLocal: "Pengguna lokal" isLocal: "Pengguna lokal"
@ -1943,6 +1967,7 @@ _soundSettings:
driveFileTypeWarnDescription: "Pilih berkas audio" driveFileTypeWarnDescription: "Pilih berkas audio"
driveFileDurationWarn: "Audio ini terlalu panjang" driveFileDurationWarn: "Audio ini terlalu panjang"
driveFileDurationWarnDescription: "Audio panjang dapat mengganggu penggunaan Misskey. Masih ingin melanjutkan?" driveFileDurationWarnDescription: "Audio panjang dapat mengganggu penggunaan Misskey. Masih ingin melanjutkan?"
driveFileError: "Tak bisa memuat audio. Mohon ubah pengaturan"
_ago: _ago:
future: "Masa depan" future: "Masa depan"
justNow: "Baru saja" justNow: "Baru saja"
@ -2415,6 +2440,8 @@ _abuseReport:
_notificationRecipient: _notificationRecipient:
_recipientType: _recipientType:
mail: "Surel" mail: "Surel"
webhook: "Webhook"
keywords: "Kata kunci"
_moderationLogTypes: _moderationLogTypes:
createRole: "Peran telah dibuat" createRole: "Peran telah dibuat"
deleteRole: "Peran telah dihapus" deleteRole: "Peran telah dihapus"
@ -2452,6 +2479,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "Hapus dekorasi avatar" deleteAvatarDecoration: "Hapus dekorasi avatar"
unsetUserAvatar: "Hapus avatar pengguna" unsetUserAvatar: "Hapus avatar pengguna"
unsetUserBanner: "Hapus banner pengguna" unsetUserBanner: "Hapus banner pengguna"
deleteAccount: "Akun dihapus"
_fileViewer: _fileViewer:
title: "Rincian berkas" title: "Rincian berkas"
type: "Jenis berkas" type: "Jenis berkas"

150
locales/index.d.ts vendored
View file

@ -3806,6 +3806,18 @@ export interface Locale extends ILocale {
* 1 * 1
*/ */
"oneMonth": string; "oneMonth": string;
/**
* 3
*/
"threeMonths": string;
/**
* 1
*/
"oneYear": string;
/**
* 3
*/
"threeDays": string;
/** /**
* *
*/ */
@ -4366,6 +4378,10 @@ export interface Locale extends ILocale {
* *
*/ */
"enableChartsForFederatedInstances": string; "enableChartsForFederatedInstances": string;
/**
*
*/
"enableStatsForFederatedInstances": string;
/** /**
* *
*/ */
@ -5166,6 +5182,88 @@ export interface Locale extends ILocale {
* *
*/ */
"target": string; "target": string;
/**
* CAPTCHAのテストを目的とした機能です<strong>使</strong>
*/
"testCaptchaWarning": string;
/**
*
*/
"prohibitedWordsForNameOfUser": string;
/**
*
*/
"prohibitedWordsForNameOfUserDescription": string;
/**
*
*/
"yourNameContainsProhibitedWords": string;
/**
* 使
*/
"yourNameContainsProhibitedWordsDescription": string;
/**
* 稿
*/
"thisContentsAreMarkedAsSigninRequiredByAuthor": string;
/**
*
*/
"lockdown": string;
/**
*
*/
"pleaseSelectAccount": string;
/**
*
*/
"availableRoles": string;
"_accountSettings": {
/**
*
*/
"requireSigninToViewContents": string;
/**
*
*/
"requireSigninToViewContentsDescription1": string;
/**
* URLプレビュー(OGP)Webページへの埋め込み
*/
"requireSigninToViewContentsDescription2": string;
/**
*
*/
"requireSigninToViewContentsDescription3": string;
/**
*
*/
"makeNotesFollowersOnlyBefore": string;
/**
*
*/
"makeNotesFollowersOnlyBeforeDescription": string;
/**
*
*/
"makeNotesHiddenBefore": string;
/**
* ()
*/
"makeNotesHiddenBeforeDescription": string;
/**
*
*/
"mayNotEffectForFederatedNotes": string;
/**
*
*/
"notesHavePassedSpecifiedPeriod": string;
/**
*
*/
"notesOlderThanSpecifiedDateAndTime": string;
};
"_abuseUserReport": { "_abuseUserReport": {
/** /**
* *
@ -5696,6 +5794,10 @@ export interface Locale extends ILocale {
* URLやWebページのURLを指定します * URLやWebページのURLを指定します
*/ */
"inquiryUrlDescription": string; "inquiryUrlDescription": string;
/**
*
*/
"thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
}; };
"_accountMigration": { "_accountMigration": {
/** /**
@ -8354,14 +8456,26 @@ export interface Locale extends ILocale {
* *
*/ */
"callback": string; "callback": string;
/**
*
*/
"accepted": string;
/** /**
* *
*/ */
"denied": string; "denied": string;
/**
*
*/
"scopeUser": string;
/** /**
* *
*/ */
"pleaseLogin": string; "pleaseLogin": string;
/**
* URLに遷移します
*/
"byClickingYouWillBeRedirectedToThisUrl": string;
}; };
"_antennaSources": { "_antennaSources": {
/** /**
@ -9243,7 +9357,7 @@ export interface Locale extends ILocale {
*/ */
"youGotQuote": ParameterizedString<"name">; "youGotQuote": ParameterizedString<"name">;
/** /**
* {name}Renoteしまし * {name}
*/ */
"youRenoted": ParameterizedString<"name">; "youRenoted": ParameterizedString<"name">;
/** /**
@ -9348,7 +9462,7 @@ export interface Locale extends ILocale {
*/ */
"reply": string; "reply": string;
/** /**
* Renote *
*/ */
"renote": string; "renote": string;
/** /**
@ -9406,7 +9520,7 @@ export interface Locale extends ILocale {
*/ */
"reply": string; "reply": string;
/** /**
* Renote *
*/ */
"renote": string; "renote": string;
}; };
@ -9633,6 +9747,14 @@ export interface Locale extends ILocale {
* *
*/ */
"userCreated": string; "userCreated": string;
/**
*
*/
"inactiveModeratorsWarning": string;
/**
*
*/
"inactiveModeratorsInvitationOnlyChanged": string;
}; };
/** /**
* Webhookを削除しますか * Webhookを削除しますか
@ -10435,6 +10557,28 @@ export interface Locale extends ILocale {
*/ */
"codeGeneratedDescription": string; "codeGeneratedDescription": string;
}; };
"_selfXssPrevention": {
/**
*
*/
"warning": string;
/**
*
*/
"title": string;
/**
*
*/
"description1": string;
/**
* %c今すぐ作業を中止してこのウィンドウを閉じてください
*/
"description2": string;
/**
* {link}
*/
"description3": ParameterizedString<"link">;
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View file

@ -15,6 +15,7 @@ const merge = (...args) => args.reduce((a, c) => ({
const languages = [ const languages = [
'ar-SA', 'ar-SA',
'ca-ES',
'cs-CZ', 'cs-CZ',
'da-DK', 'da-DK',
'de-DE', 'de-DE',

View file

@ -68,7 +68,7 @@ reply: "Rispondi"
loadMore: "Mostra di più" loadMore: "Mostra di più"
showMore: "Espandi" showMore: "Espandi"
showLess: "Comprimi" showLess: "Comprimi"
youGotNewFollower: "Adesso ti segue" youGotNewFollower: "Hai un nuovo Follower"
receiveFollowRequest: "Hai ricevuto una richiesta di follow" receiveFollowRequest: "Hai ricevuto una richiesta di follow"
followRequestAccepted: "Ha accettato la tua richiesta di follow" followRequestAccepted: "Ha accettato la tua richiesta di follow"
mention: "Menzioni" mention: "Menzioni"
@ -80,14 +80,14 @@ export: "Esporta"
files: "Allegati" files: "Allegati"
download: "Scarica" download: "Scarica"
driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?" driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
unfollowConfirm: "Vuoi davvero smettere di seguire {name}?" unfollowConfirm: "Vuoi davvero togliere il Following a {name}?"
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo." importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo."
lists: "Liste" lists: "Liste"
noLists: "Nessuna lista" noLists: "Nessuna lista"
note: "Nota" note: "Nota"
notes: "Note" notes: "Note"
following: "Follow" following: "Following"
followers: "Follower" followers: "Follower"
followsYou: "Follower" followsYou: "Follower"
createList: "Aggiungi una nuova lista" createList: "Aggiungi una nuova lista"
@ -106,7 +106,7 @@ defaultNoteVisibility: "Privacy predefinita delle note"
follow: "Segui" follow: "Segui"
followRequest: "Richiesta di follow" followRequest: "Richiesta di follow"
followRequests: "Richieste di follow" followRequests: "Richieste di follow"
unfollow: "Smetti di seguire" unfollow: "Togli Following"
followRequestPending: "Richiesta in approvazione" followRequestPending: "Richiesta in approvazione"
enterEmoji: "Inserisci emoji" enterEmoji: "Inserisci emoji"
renote: "Rinota" renote: "Rinota"
@ -195,7 +195,7 @@ setWallpaper: "Imposta sfondo"
removeWallpaper: "Elimina lo sfondo" removeWallpaper: "Elimina lo sfondo"
searchWith: "Cerca: {q}" searchWith: "Cerca: {q}"
youHaveNoLists: "Non hai ancora creato nessuna lista" youHaveNoLists: "Non hai ancora creato nessuna lista"
followConfirm: "Vuoi seguire {name}?" followConfirm: "Confermi il Following a {name}?"
proxyAccount: "Profilo proxy" proxyAccount: "Profilo proxy"
proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti." proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti."
host: "Host" host: "Host"
@ -263,7 +263,7 @@ all: "Tutte"
subscribing: "Iscrizione" subscribing: "Iscrizione"
publishing: "Pubblicazione" publishing: "Pubblicazione"
notResponding: "Nessuna risposta" notResponding: "Nessuna risposta"
instanceFollowing: "Seguiti dall'istanza" instanceFollowing: "Istanza Following"
instanceFollowers: "Follower dell'istanza" instanceFollowers: "Follower dell'istanza"
instanceUsers: "Profili nell'istanza" instanceUsers: "Profili nell'istanza"
changePassword: "Aggiorna Password" changePassword: "Aggiorna Password"
@ -454,6 +454,7 @@ totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App d
moderator: "Moderatore" moderator: "Moderatore"
moderation: "moderazione" moderation: "moderazione"
moderationNote: "Promemoria di moderazione" moderationNote: "Promemoria di moderazione"
moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori."
addModerationNote: "Aggiungi promemoria di moderazione" addModerationNote: "Aggiungi promemoria di moderazione"
moderationLogs: "Cronologia di moderazione" moderationLogs: "Cronologia di moderazione"
nUsersMentioned: "{n} profili ne parlano" nUsersMentioned: "{n} profili ne parlano"
@ -614,7 +615,7 @@ unsetUserBannerConfirm: "Vuoi davvero rimuovere l'intestazione dal profilo?"
deleteAllFiles: "Elimina tutti i file" deleteAllFiles: "Elimina tutti i file"
deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?"
removeAllFollowing: "Annulla tutti i follow" removeAllFollowing: "Annulla tutti i follow"
removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più." removeAllFollowingDescription: "Togli il Following a tutti i profili su {host}. Utile, ad esempio, quando l'istanza non esiste più."
userSuspended: "L'utente è in sospensione" userSuspended: "L'utente è in sospensione"
userSilenced: "Profilo silenziato" userSilenced: "Profilo silenziato"
yourAccountSuspendedTitle: "Questo profilo è sospeso" yourAccountSuspendedTitle: "Questo profilo è sospeso"
@ -687,7 +688,7 @@ hardWordMute: "Filtro parole forte"
regexpError: "errore regex" regexpError: "errore regex"
regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:"
instanceMute: "Silenziare l'istanza" instanceMute: "Silenziare l'istanza"
userSaysSomething: "{name} ha parlato" userSaysSomething: "{name} ha detto qualcosa"
makeActive: "Attiva" makeActive: "Attiva"
display: "Visualizza" display: "Visualizza"
copy: "Copia" copy: "Copia"
@ -702,7 +703,7 @@ notificationSetting: "Impostazioni notifiche"
notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare." notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare."
useGlobalSetting: "Usa impostazioni generali" useGlobalSetting: "Usa impostazioni generali"
useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate." useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate."
other: "Ulteriori" other: "Eccetera"
regenerateLoginToken: "Genera di nuovo un token di connessione" regenerateLoginToken: "Genera di nuovo un token di connessione"
regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi." regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate" theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate"
@ -746,7 +747,7 @@ repliesCount: "Numero di risposte inviate"
renotesCount: "Numero di note che hai ricondiviso" renotesCount: "Numero di note che hai ricondiviso"
repliedCount: "Numero di risposte ricevute" repliedCount: "Numero di risposte ricevute"
renotedCount: "Numero delle tue note ricondivise" renotedCount: "Numero delle tue note ricondivise"
followingCount: "Numero di profili seguiti" followingCount: "Numero di Following"
followersCount: "Numero di profili che ti seguono" followersCount: "Numero di profili che ti seguono"
sentReactionsCount: "Numero di reazioni inviate" sentReactionsCount: "Numero di reazioni inviate"
receivedReactionsCount: "Numero di reazioni ricevute" receivedReactionsCount: "Numero di reazioni ricevute"
@ -841,7 +842,7 @@ onlineStatus: "Stato di connessione"
hideOnlineStatus: "Modalità invisibile" hideOnlineStatus: "Modalità invisibile"
hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca." hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca."
online: "Online" online: "Online"
active: "Attività" active: "Attivo"
offline: "Offline" offline: "Offline"
notRecommended: "Sconsigliato" notRecommended: "Sconsigliato"
botProtection: "Protezione contro i bot" botProtection: "Protezione contro i bot"
@ -900,8 +901,8 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente" lastCommunication: "La comunicazione più recente"
resolved: "Risolto" resolved: "Risolto"
unresolved: "Non risolto" unresolved: "Non risolto"
breakFollow: "Impedire di seguirmi" breakFollow: "Rimuovi Follower"
breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?" breakFollowConfirm: "Vuoi davvero togliere questo Follower?"
itsOn: "Abilitato" itsOn: "Abilitato"
itsOff: "Disabilitato" itsOff: "Disabilitato"
on: "Acceso" on: "Acceso"
@ -916,7 +917,7 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
classic: "Classico" classic: "Classico"
muteThread: "Silenziare conversazione" muteThread: "Silenziare conversazione"
unmuteThread: "Riattiva la conversazione" unmuteThread: "Riattiva la conversazione"
followingVisibility: "Visibilità dei profili seguiti" followingVisibility: "Visibilità dei Following"
followersVisibility: "Visibilità dei profili che ti seguono" followersVisibility: "Visibilità dei profili che ti seguono"
continueThread: "Altre conversazioni" continueThread: "Altre conversazioni"
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
@ -946,6 +947,9 @@ oneHour: "1 ora"
oneDay: "1 giorno" oneDay: "1 giorno"
oneWeek: "1 settimana" oneWeek: "1 settimana"
oneMonth: "Un mese" oneMonth: "Un mese"
threeMonths: "3 mesi"
oneYear: "1 anno"
threeDays: "3 giorni"
reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto." reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto."
failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo" failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo"
rateLimitExceeded: "Superato il limite di richieste." rateLimitExceeded: "Superato il limite di richieste."
@ -964,7 +968,7 @@ driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo"
driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato." driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato."
requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore." requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore."
isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema" isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema"
typeToConfirm: "Per eseguire questa operazione, digitare {x}" typeToConfirm: "Digita {x} per continuare"
deleteAccount: "Eliminazione profilo" deleteAccount: "Eliminazione profilo"
document: "Documento" document: "Documento"
numberOfPageCache: "Numero di pagine cache" numberOfPageCache: "Numero di pagine cache"
@ -1019,7 +1023,7 @@ neverShow: "Non mostrare più"
remindMeLater: "Rimanda" remindMeLater: "Rimanda"
didYouLikeMisskey: "Ti piace Misskey?" didYouLikeMisskey: "Ti piace Misskey?"
pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
correspondingSourceIsAvailable: "" correspondingSourceIsAvailable: "Il codice sorgente corrispondente è disponibile su {anchor}."
roles: "Ruoli" roles: "Ruoli"
role: "Ruolo" role: "Ruolo"
noRole: "Ruolo non trovato" noRole: "Ruolo non trovato"
@ -1086,6 +1090,7 @@ retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
enableChartsForRemoteUser: "Abilita i grafici per i profili remoti" enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
enableStatsForFederatedInstances: "Informazioni statistiche sui server federati"
showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
reactionsDisplaySize: "Grandezza delle reazioni" reactionsDisplaySize: "Grandezza delle reazioni"
limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale" limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale"
@ -1128,7 +1133,7 @@ channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco
thisChannelArchived: "Questo canale è stato archiviato." thisChannelArchived: "Questo canale è stato archiviato."
displayOfNote: "Visualizzazione delle Note" displayOfNote: "Visualizzazione delle Note"
initialAccountSetting: "Impostazioni iniziali del profilo" initialAccountSetting: "Impostazioni iniziali del profilo"
youFollowing: "Seguiti" youFollowing: "Following"
preventAiLearning: "Impedisci l'apprendimento della IA" preventAiLearning: "Impedisci l'apprendimento della IA"
preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata." preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata."
options: "Opzioni del ruolo" options: "Opzioni del ruolo"
@ -1285,6 +1290,34 @@ unknownWebAuthnKey: "Questa è una passkey sconosciuta."
passkeyVerificationFailed: "La verifica della passkey non è riuscita." passkeyVerificationFailed: "La verifica della passkey non è riuscita."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato." passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato."
messageToFollower: "Messaggio ai follower" messageToFollower: "Messaggio ai follower"
target: "Riferimento"
testCaptchaWarning: "Questa funzione è destinata al test CAPTCHA. <strong>Da non utilizzare in ambiente di produzione.</strong>"
prohibitedWordsForNameOfUser: "Parole proibite (nome utente)"
prohibitedWordsForNameOfUserDescription: "Il sistema rifiuta di rinominare un utente, se il nome contiene qualsiasi parola nell'elenco. Sono esenti i profili con privilegi di moderazione."
yourNameContainsProhibitedWords: "Il nome che hai scelto contiene una o più parole vietate"
yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare questo nome, contatta l''amministrazione."
thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autore richiede di iscriversi per vedere il contenuto"
lockdown: "Isolamento"
pleaseSelectAccount: "Per favore, seleziona un profilo"
_accountSettings:
requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione"
requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler."
requireSigninToViewContentsDescription2: "La visualizzazione verrà disabilitata a server che non supportano l'anteprima URL (OGP), all'incorporamento nelle pagine Web e alla citazione delle Note."
requireSigninToViewContentsDescription3: "Queste restrizioni potrebbero non applicarsi al contenuto federato su server remoti."
makeNotesFollowersOnlyBefore: "Rendi visibili solo ai Follower le Note pubblicate in precedenza"
makeNotesFollowersOnlyBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili solo ai profili Follower. Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota."
makeNotesHiddenBefore: "Nascondi le Note pubblicate in precedenza"
makeNotesHiddenBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili soltanto a te (private). Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota."
mayNotEffectForFederatedNotes: "Le Note già federate su server remoti potrebbero non essere modificate."
notesHavePassedSpecifiedPeriod: "Note antecedenti al periodo specificato"
notesOlderThanSpecifiedDateAndTime: "Note antecedenti al momento specificato"
_abuseUserReport:
forward: "Inoltra"
forwardDescription: "Inoltra il report al server remoto, per mezzo di account di sistema, anonimo."
resolve: "Risolvi"
accept: "Approva"
reject: "Rifiuta"
resolveTutorial: "Se moderi una segnalazione legittima, scegli \"Approva\" per risolvere positivamente.\nSe la segnalazione non è legittima, seleziona \"Rifiuta\" per risolvere negativamente."
_delivery: _delivery:
status: "Stato della consegna" status: "Stato della consegna"
stop: "Sospensione" stop: "Sospensione"
@ -1312,16 +1345,16 @@ _bubbleGame:
_announcement: _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
needConfirmationToRead: "Richiede la conferma di lettura" needConfirmationToRead: "Conferma di lettura obbligatoria"
needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce." needConfirmationToReadDescription: "I profili riceveranno una finestra di dialogo che richiede di accettare obbligatoriamente per procedere. Tale richiesta è esente da \"conferma tutte\"."
end: "Archivia l'annuncio" end: "Archivia l'annuncio"
tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi."
readConfirmTitle: "Segnare come già letto?" readConfirmTitle: "Segnare come già letto?"
readConfirmText: "Hai già letto \"{title}˝?" readConfirmText: "Hai già letto \"{title}˝?"
shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte." shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte."
dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte." dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte."
silence: "Silenziare gli annunci" silence: "Annuncio silenzioso"
silenceDescription: "Se attivi questa opzione, non riceverai notifiche sugli annunci, evitando di contrassegnarle come già lette." silenceDescription: "Attivando questa opzione, non invierai la notifica, evitando che debba essere contrassegnata come già letta."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Il tuo profilo è stato creato!" accountCreated: "Il tuo profilo è stato creato!"
letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
@ -1363,7 +1396,7 @@ _initialTutorial:
_timeline: _timeline:
title: "Come funziona la Timeline" title: "Come funziona la Timeline"
description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori." description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori."
home: "le Note provenienti dai profili che segui (follow)." home: "le Note provenienti dai profili che segui (Following)."
local: "tutte le Note pubblicate dai profili di questa istanza." local: "tutte le Note pubblicate dai profili di questa istanza."
social: "sia le Note della Timeline Home che quelle della Timeline Locale, insieme!" social: "sia le Note della Timeline Home che quelle della Timeline Locale, insieme!"
global: "le Note da pubblicate da tutte le altre istanze federate con la nostra." global: "le Note da pubblicate da tutte le altre istanze federate con la nostra."
@ -1401,7 +1434,7 @@ _initialTutorial:
title: "Il tutorial è finito! 🎉" title: "Il tutorial è finito! 🎉"
description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}." description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}."
_timelineDescription: _timelineDescription:
home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (follow)." home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (Following)."
local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server." local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server."
social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale." social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale."
global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo." global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo."
@ -1422,11 +1455,12 @@ _serverSettings:
reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis." reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis."
inquiryUrl: "URL di contatto" inquiryUrl: "URL di contatto"
inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo."
_accountMigration: _accountMigration:
moveFrom: "Migra un altro profilo dentro a questo" moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto" moveFromSub: "Crea un alias verso un altro profilo remoto"
moveFromLabel: "Profilo da cui migrare #{n}" moveFromLabel: "Profilo da cui migrare #{n}"
moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it" moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
moveTo: "Migrare questo profilo verso un un altro" moveTo: "Migrare questo profilo verso un un altro"
moveToLabel: "Profilo verso cui migrare" moveToLabel: "Profilo verso cui migrare"
moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata." moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata."
@ -1435,7 +1469,7 @@ _accountMigration:
startMigration: "Avvia la migrazione" startMigration: "Avvia la migrazione"
migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo." migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo."
movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata." movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata."
postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Follow che i Follower scenderanno a zero. I tuoi follower saranno comunque in grado di vedere le Note per soli follower, poiché non smetteranno di seguirti." postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Following che i Follower scenderanno a zero. I tuoi Follower saranno comunque in grado di vedere le Note per soli Follower, poiché non smetteranno di seguirti."
movedTo: "Profilo verso cui migrare" movedTo: "Profilo verso cui migrare"
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
@ -1828,7 +1862,7 @@ _gallery:
unlike: "Non mi piace più" unlike: "Non mi piace più"
_email: _email:
_follow: _follow:
title: "Adesso ti segue" title: "Follower aggiuntivo"
_receiveFollowRequest: _receiveFollowRequest:
title: "Hai ricevuto una richiesta di follow" title: "Hai ricevuto una richiesta di follow"
_plugin: _plugin:
@ -1892,7 +1926,7 @@ _channel:
removeBanner: "Rimuovi intestazione" removeBanner: "Rimuovi intestazione"
featured: "Di tendenza" featured: "Di tendenza"
owned: "I miei canali" owned: "I miei canali"
following: "Seguiti" following: "Following"
usersCount: "{n} partecipanti" usersCount: "{n} partecipanti"
notesCount: "{n} note" notesCount: "{n} note"
nameAndDescription: "Nome e descrizione" nameAndDescription: "Nome e descrizione"
@ -2058,7 +2092,7 @@ _permissions:
"read:favorites": "Visualizza i tuoi preferiti" "read:favorites": "Visualizza i tuoi preferiti"
"write:favorites": "Gestisci i tuoi preferiti" "write:favorites": "Gestisci i tuoi preferiti"
"read:following": "Vedi le informazioni di follow" "read:following": "Vedi le informazioni di follow"
"write:following": "Following di altri profili" "write:following": "Aggiungere e togliere Following"
"read:messaging": "Visualizzare la chat" "read:messaging": "Visualizzare la chat"
"write:messaging": "Gestire la chat" "write:messaging": "Gestire la chat"
"read:mutes": "Vedi i profili silenziati" "read:mutes": "Vedi i profili silenziati"
@ -2141,11 +2175,14 @@ _auth:
permissionAsk: "Questa app richiede le seguenti autorizzazioni:" permissionAsk: "Questa app richiede le seguenti autorizzazioni:"
pleaseGoBack: "Si prega di ritornare sulla app" pleaseGoBack: "Si prega di ritornare sulla app"
callback: "Ritornando sulla app" callback: "Ritornando sulla app"
accepted: "Accesso concesso"
denied: "Accesso negato" denied: "Accesso negato"
scopeUser: "Sto funzionando per il seguente profilo"
pleaseLogin: "Per favore accedi al tuo account per cambiare i permessi dell'applicazione" pleaseLogin: "Per favore accedi al tuo account per cambiare i permessi dell'applicazione"
byClickingYouWillBeRedirectedToThisUrl: "Consentendo l'accesso, si verrà reindirizzati presso questo indirizzo URL"
_antennaSources: _antennaSources:
all: "Tutte le note" all: "Tutte le note"
homeTimeline: "Note dagli utenti che segui" homeTimeline: "Note dai tuoi Following"
users: "Note dagli utenti selezionati" users: "Note dagli utenti selezionati"
userList: "Note dagli utenti della lista selezionata" userList: "Note dagli utenti della lista selezionata"
userBlacklist: "Tutte le Note tranne quelle di uno o più profili specificati" userBlacklist: "Tutte le Note tranne quelle di uno o più profili specificati"
@ -2187,7 +2224,7 @@ _widgets:
_userList: _userList:
chooseList: "Seleziona una lista" chooseList: "Seleziona una lista"
clicker: "Cliccaggio" clicker: "Cliccaggio"
birthdayFollowings: "Chi nacque oggi" birthdayFollowings: "Compleanni del giorno"
_cw: _cw:
hide: "Nascondere" hide: "Nascondere"
show: "Continua la lettura..." show: "Continua la lettura..."
@ -2258,7 +2295,7 @@ _exportOrImport:
allNotes: "Tutte le note" allNotes: "Tutte le note"
favoritedNotes: "Note preferite" favoritedNotes: "Note preferite"
clips: "Clip" clips: "Clip"
followingList: "Follow" followingList: "Following"
muteList: "Elenco profili silenziati" muteList: "Elenco profili silenziati"
blockingList: "Elenco profili bloccati" blockingList: "Elenco profili bloccati"
userLists: "Liste" userLists: "Liste"
@ -2374,7 +2411,7 @@ _notification:
youGotReply: "{name} ti ha risposto" youGotReply: "{name} ti ha risposto"
youGotQuote: "{name} ha citato la tua Nota e ha detto" youGotQuote: "{name} ha citato la tua Nota e ha detto"
youRenoted: "{name} ha rinotato" youRenoted: "{name} ha rinotato"
youWereFollowed: "Adesso ti segue" youWereFollowed: "Follower aggiuntivo"
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata"
pollEnded: "Risultati del sondaggio." pollEnded: "Risultati del sondaggio."
@ -2397,7 +2434,7 @@ _notification:
_types: _types:
all: "Tutto" all: "Tutto"
note: "Nuove Note" note: "Nuove Note"
follow: "Nuovi profili follower" follow: "Follower"
mention: "Menzioni" mention: "Menzioni"
reply: "Risposte" reply: "Risposte"
renote: "Rinota" renote: "Rinota"
@ -2413,7 +2450,7 @@ _notification:
test: "Prova la notifica" test: "Prova la notifica"
app: "Notifiche da applicazioni" app: "Notifiche da applicazioni"
_actions: _actions:
followBack: "Segui" followBack: "Following ricambiato"
reply: "Rispondi" reply: "Rispondi"
renote: "Rinota" renote: "Rinota"
_deck: _deck:
@ -2465,7 +2502,7 @@ _webhookSettings:
trigger: "Trigger" trigger: "Trigger"
active: "Attivo" active: "Attivo"
_events: _events:
follow: "Quando segui un profilo" follow: "Quando aggiungi Following"
followed: "Quando ti segue un profilo" followed: "Quando ti segue un profilo"
note: "Quando pubblichi una Nota" note: "Quando pubblichi una Nota"
reply: "Quando rispondono ad una Nota" reply: "Quando rispondono ad una Nota"
@ -2476,6 +2513,8 @@ _webhookSettings:
abuseReport: "Quando arriva una segnalazione" abuseReport: "Quando arriva una segnalazione"
abuseReportResolved: "Quando una segnalazione è risolta" abuseReportResolved: "Quando una segnalazione è risolta"
userCreated: "Quando viene creato un profilo" userCreated: "Quando viene creato un profilo"
inactiveModeratorsWarning: "Quando un profilo moderatore rimane inattivo per un determinato periodo"
inactiveModeratorsInvitationOnlyChanged: "Quando la moderazione è rimasta inattiva per un determinato periodo e il sistema è cambiato in modalità \"solo inviti\""
deleteConfirm: "Vuoi davvero eliminare il Webhook?" deleteConfirm: "Vuoi davvero eliminare il Webhook?"
testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi." testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi."
_abuseReport: _abuseReport:
@ -2521,6 +2560,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "File nel Drive segnato come esplicito" markSensitiveDriveFile: "File nel Drive segnato come esplicito"
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
resolveAbuseReport: "Segnalazione risolta" resolveAbuseReport: "Segnalazione risolta"
forwardAbuseReport: "Segnalazione inoltrata"
updateAbuseReportNote: "Ha aggiornato la segnalazione"
createInvitation: "Genera codice di invito" createInvitation: "Genera codice di invito"
createAd: "Banner creato" createAd: "Banner creato"
deleteAd: "Banner eliminato" deleteAd: "Banner eliminato"
@ -2690,3 +2731,9 @@ _embedCodeGen:
generateCode: "Crea il codice di incorporamento" generateCode: "Crea il codice di incorporamento"
codeGenerated: "Codice generato" codeGenerated: "Codice generato"
codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web." codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web."
_selfXssPrevention:
warning: "Avviso"
title: "\"Incolla qualcosa su questa schermata\" è tutta una truffa."
description1: "Incollando qualcosa qui, malintenzionati potrebbero prendere il controllo del tuo profilo o rubare i tuoi dati personali."
description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra."
description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}"

View file

@ -947,6 +947,9 @@ oneHour: "1時間"
oneDay: "1日" oneDay: "1日"
oneWeek: "1週間" oneWeek: "1週間"
oneMonth: "1ヶ月" oneMonth: "1ヶ月"
threeMonths: "3ヶ月"
oneYear: "1年"
threeDays: "3日"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
rateLimitExceeded: "レート制限を超えました" rateLimitExceeded: "レート制限を超えました"
@ -1087,6 +1090,7 @@ retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。" retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
enableStatsForFederatedInstances: "リモートサーバーの情報を取得"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
reactionsDisplaySize: "リアクションの表示サイズ" reactionsDisplaySize: "リアクションの表示サイズ"
limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する" limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する"
@ -1287,6 +1291,28 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ" messageToFollower: "フォロワーへのメッセージ"
target: "対象" target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
lockdown: "ロックダウン"
pleaseSelectAccount: "アカウントを選択してください"
availableRoles: "利用可能なロール"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。"
requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ートの引用に対応していないサーバーからの表示も不可になります。"
requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。"
makeNotesFollowersOnlyBefore: "過去のノートをフォロワーのみ表示可能にする"
makeNotesFollowersOnlyBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。"
makeNotesHiddenBefore: "過去のノートを非公開化する"
makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
_abuseUserReport: _abuseUserReport:
forward: "転送" forward: "転送"
@ -1440,6 +1466,7 @@ _serverSettings:
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
inquiryUrl: "問い合わせ先URL" inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
_accountMigration: _accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行" moveFrom: "別のアカウントからこのアカウントに移行"
@ -2192,8 +2219,11 @@ _auth:
permissionAsk: "このアプリは次の権限を要求しています" permissionAsk: "このアプリは次の権限を要求しています"
pleaseGoBack: "アプリケーションに戻ってやっていってください" pleaseGoBack: "アプリケーションに戻ってやっていってください"
callback: "アプリケーションに戻っています" callback: "アプリケーションに戻っています"
accepted: "アクセスを許可しました"
denied: "アクセスを拒否しました" denied: "アクセスを拒否しました"
scopeUser: "以下のユーザーとして操作しています"
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。" pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
byClickingYouWillBeRedirectedToThisUrl: "アクセスを許可すると、自動で以下のURLに遷移します"
_antennaSources: _antennaSources:
all: "全てのノート" all: "全てのノート"
@ -2441,7 +2471,7 @@ _notification:
youGotMention: "{name}からのメンション" youGotMention: "{name}からのメンション"
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました" youRenoted: "{name}がリノートしました"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
@ -2469,7 +2499,7 @@ _notification:
follow: "フォロー" follow: "フォロー"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
renote: "Renote" renote: "リノート"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
@ -2485,7 +2515,7 @@ _notification:
_actions: _actions:
followBack: "フォローバック" followBack: "フォローバック"
reply: "返信" reply: "返信"
renote: "Renote" renote: "リノート"
_deck: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"
@ -2552,6 +2582,8 @@ _webhookSettings:
abuseReport: "ユーザーから通報があったとき" abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき" abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき" userCreated: "ユーザーが作成されたとき"
inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき"
inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき"
deleteConfirm: "Webhookを削除しますか" deleteConfirm: "Webhookを削除しますか"
testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。" testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
@ -2780,3 +2812,10 @@ _embedCodeGen:
generateCode: "埋め込みコードを作成" generateCode: "埋め込みコードを作成"
codeGenerated: "コードが生成されました" codeGenerated: "コードが生成されました"
codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。" codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"
_selfXssPrevention:
warning: "警告"
title: "「この画面に何か貼り付けろ」はすべて詐欺です。"
description1: "ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。"
description2: "貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。"
description3: "詳しくはこちらをご確認ください。 {link}"

View file

@ -8,6 +8,9 @@ search: "探す"
notifications: "通知" notifications: "通知"
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"
initialPasswordForSetup: "初期設定開始用パスワード"
initialPasswordIsIncorrect: "初期設定開始用のパスワードがちゃうで。"
initialPasswordForSetupDescription: "Miskkeyを自分でインストールしたんやったら、設定ファイルに入れたパスワードを使ってや。\nホスティングサービスを使っとるんやったら、サービスから言われたやつを使うんやで。\n別に何も設定しとらんのやったら、何も入れずに空けといてな。"
forgotPassword: "パスワード忘れたん?" forgotPassword: "パスワード忘れたん?"
fetchingAsApObject: "今ちと連合に照会しとるで" fetchingAsApObject: "今ちと連合に照会しとるで"
ok: "ええで" ok: "ええで"
@ -236,6 +239,8 @@ silencedInstances: "サーバーサイレンスされてんねん"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
mediaSilencedInstances: "メディアサイレンスしたサーバー" mediaSilencedInstances: "メディアサイレンスしたサーバー"
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。" mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。"
federationAllowedHosts: "連合を許すサーバー"
federationAllowedHostsDescription: "連合してもいいサーバーのホストを行ごとに区切って設定してや。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしとるユーザー" mutedUsers: "ミュートしとるユーザー"
blockedUsers: "ブロックしとるユーザー" blockedUsers: "ブロックしとるユーザー"
@ -334,6 +339,7 @@ renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーをほかす" deleteFolder: "フォルダーをほかす"
folder: "フォルダー" folder: "フォルダー"
addFile: "ファイルを追加" addFile: "ファイルを追加"
showFile: "ファイル出す"
emptyDrive: "ドライブは空っぽや" emptyDrive: "ドライブは空っぽや"
emptyFolder: "このフォルダーは空や" emptyFolder: "このフォルダーは空や"
unableToDelete: "消せんかったわ" unableToDelete: "消せんかったわ"
@ -448,6 +454,7 @@ totpDescription: "認証アプリ使うてワンタイムパスワードを入
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
moderationNote: "モデレーションノート" moderationNote: "モデレーションノート"
moderationNoteDescription: "モデレーターの中だけで共有するメモを入れれるで。"
addModerationNote: "モデレーションノートを追加するで" addModerationNote: "モデレーションノートを追加するで"
moderationLogs: "モデログ" moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
@ -509,6 +516,10 @@ uiLanguage: "UIの表示言語"
aboutX: "{x}について" aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
menuStyle: "メニューのスタイル"
style: "スタイル"
drawer: "ドロワー"
popup: "ポップアップ"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで"
showReactionsCount: "ノートのリアクション数を表示する" showReactionsCount: "ノートのリアクション数を表示する"
noHistory: "履歴はないわ。" noHistory: "履歴はないわ。"
@ -591,6 +602,8 @@ ascendingOrder: "小さい順"
descendingOrder: "大きい順" descendingOrder: "大きい順"
scratchpad: "スクラッチパッド" scratchpad: "スクラッチパッド"
scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Misskeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。" scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。Misskeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
uiInspector: "UIインスペクター"
uiInspectorDescription: "メモリ上にあるUIコンポーネントのインスタンス一覧を見れるで。UIコンポーネントはUi:C:系関数で生成されるで。"
output: "出力" output: "出力"
script: "スクリプト" script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にしてや" disablePagesScript: "Pagesのスクリプトを無効にしてや"
@ -909,6 +922,7 @@ followersVisibility: "フォロワーの公開範囲"
continueThread: "さらにスレッドを見るで" continueThread: "さらにスレッドを見るで"
deleteAccountConfirm: "アカウントを消すで?ええんか?" deleteAccountConfirm: "アカウントを消すで?ええんか?"
incorrectPassword: "パスワードがちゃうわ。" incorrectPassword: "パスワードがちゃうわ。"
incorrectTotp: "ワンタイムパスワードが間違っとるか、期限が切れとるみたいやな。"
voteConfirm: "「{choice}」に投票するんか?" voteConfirm: "「{choice}」に投票するんか?"
hide: "隠す" hide: "隠す"
useDrawerReactionPickerForMobile: "ケータイとかのときドロワーで表示するで" useDrawerReactionPickerForMobile: "ケータイとかのときドロワーで表示するで"
@ -1073,6 +1087,7 @@ retryAllQueuesConfirmTitle: "もっかいやってみるか?"
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。" retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
enableChartsForRemoteUser: "リモートユーザーのチャートを作る" enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
enableStatsForFederatedInstances: "リモートサーバの情報を取得"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
reactionsDisplaySize: "ツッコミの表示のでかさ" reactionsDisplaySize: "ツッコミの表示のでかさ"
limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく表示するで" limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく表示するで"
@ -1259,6 +1274,32 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示
sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?" sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?"
createdLists: "作成したリスト" createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ" createdAntennas: "作成したアンテナ"
fromX: "{x}から"
genEmbedCode: "埋め込みコードを作る"
noteOfThisUser: "このユーザーのノート全部"
clipNoteLimitExceeded: "これ以上このクリップにノート追加でけへんわ。"
performance: "パフォーマンス"
modified: "変更あり"
discard: "やめる"
thereAreNChanges: "{n}個の変更があるみたいや"
signinWithPasskey: "パスキーでログイン"
unknownWebAuthnKey: "登録されてへんパスキーやな。"
passkeyVerificationFailed: "パスキーの検証に失敗したで。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証は成功したんやけど、パスワードレスログインが無効になっとるわ。"
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的としてるで。<strong>絶対に本番環境で使わんといてな。絶対やで。</strong>"
prohibitedWordsForNameOfUser: "禁止ワード(ユーザー名)"
prohibitedWordsForNameOfUserDescription: "このリストの中にある文字列がユーザー名に入っとったら、その名前に変更できひんようになるで。モデレーター権限があるユーザーは除外や。"
yourNameContainsProhibitedWords: "その名前は禁止した文字列が含まれとるで"
yourNameContainsProhibitedWordsDescription: "その名前は禁止した文字列が含まれとるわ。どうしてもって言うなら、サーバー管理者に言うしかないで。"
_abuseUserReport:
forward: "転送"
forwardDescription: "匿名のシステムアカウントってことにして、リモートサーバーに通報を転送するで。"
resolve: "解決"
accept: "ええよ"
reject: "あかんよ"
resolveTutorial: "内容がええなら「ええよ」を選ぶんや。肯定的に解決されたことにして記録するで。\n逆に、内容がだめなら「あかんよ」を選びいや。否定的に解決されたって記録しとくで。"
_delivery: _delivery:
status: "配信状態" status: "配信状態"
stop: "配信せぇへん" stop: "配信せぇへん"
@ -1393,8 +1434,10 @@ _serverSettings:
fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。" fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。"
fanoutTimelineDbFallback: "データベースにフォールバックする" fanoutTimelineDbFallback: "データベースにフォールバックする"
fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。" fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。"
reactionsBufferingDescription: "有効にしたら、リアクション作るときのパフォーマンスがすっごい上がって、データベースへの負荷が減るで。代わりに、Redisのメモリ使用は増えるで。"
inquiryUrl: "問い合わせ先URL" inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターがおらんかったら、スパムを防ぐためにこの設定は勝手に切られるで。"
_accountMigration: _accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromSub: "別のアカウントへエイリアスを作る" moveFromSub: "別のアカウントへエイリアスを作る"
@ -1726,6 +1769,11 @@ _role:
canSearchNotes: "ノート探せるかどうか" canSearchNotes: "ノート探せるかどうか"
canUseTranslator: "翻訳使えるかどうか" canUseTranslator: "翻訳使えるかどうか"
avatarDecorationLimit: "アイコンデコのいっちばんつけれる数" avatarDecorationLimit: "アイコンデコのいっちばんつけれる数"
canImportAntennas: "アンテナのインポートを許す"
canImportBlocking: "ブロックのインポートを許す"
canImportFollowing: "フォローのインポートを許す"
canImportMuting: "ミュートのインポートを許す"
canImportUserLists: "リストのインポートを許す"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
@ -2219,6 +2267,9 @@ _profile:
changeBanner: "バナー画像を変更するで" changeBanner: "バナー画像を変更するで"
verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。" verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。"
avatarDecorationMax: "最大{max}つまでデコつけれんで" avatarDecorationMax: "最大{max}つまでデコつけれんで"
followedMessage: "フォローされたら返すメッセージ"
followedMessageDescription: "フォローされたときに相手に返す短めのメッセージを決めれるで。"
followedMessageDescriptionForLockedAccount: "フォローが承認制なら、フォローリクエストをOKしたときに見せるで。"
_exportOrImport: _exportOrImport:
allNotes: "全てのノート" allNotes: "全てのノート"
favoritedNotes: "お気に入りにしたノート" favoritedNotes: "お気に入りにしたノート"
@ -2311,6 +2362,7 @@ _pages:
eyeCatchingImageSet: "アイキャッチ画像を設定" eyeCatchingImageSet: "アイキャッチ画像を設定"
eyeCatchingImageRemove: "アイキャッチ画像を削除" eyeCatchingImageRemove: "アイキャッチ画像を削除"
chooseBlock: "ブロックを追加" chooseBlock: "ブロックを追加"
enterSectionTitle: "セクションタイトルを入れる"
selectType: "種類を選択" selectType: "種類を選択"
contentBlocks: "コンテンツ" contentBlocks: "コンテンツ"
inputBlocks: "入力" inputBlocks: "入力"
@ -2356,13 +2408,15 @@ _notification:
renotedBySomeUsers: "{n}人がリノートしたで" renotedBySomeUsers: "{n}人がリノートしたで"
followedBySomeUsers: "{n}人にフォローされたで" followedBySomeUsers: "{n}人にフォローされたで"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが終わったわ"
login: "ログインしとったで"
_types: _types:
all: "すべて" all: "すべて"
note: "あんたらの新規投稿" note: "あんたらの新規投稿"
follow: "フォロー" follow: "フォロー"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
renote: "Renote" renote: "リノート"
quote: "引用" quote: "引用"
reaction: "ツッコミ" reaction: "ツッコミ"
pollEnded: "アンケートが終了したで" pollEnded: "アンケートが終了したで"
@ -2370,12 +2424,14 @@ _notification:
followRequestAccepted: "フォローが受理されたで" followRequestAccepted: "フォローが受理されたで"
roleAssigned: "ロールが付与された" roleAssigned: "ロールが付与された"
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
exportCompleted: "エクスポート終わった"
login: "ログイン" login: "ログイン"
test: "通知テスト"
app: "連携アプリからの通知や" app: "連携アプリからの通知や"
_actions: _actions:
followBack: "フォローバック" followBack: "フォローバック"
reply: "返事" reply: "返事"
renote: "Renote" renote: "リノート"
_deck: _deck:
alwaysShowMainColumn: "いつもメインカラムを表示" alwaysShowMainColumn: "いつもメインカラムを表示"
columnAlign: "カラムの寄せ" columnAlign: "カラムの寄せ"
@ -2436,7 +2492,10 @@ _webhookSettings:
abuseReport: "ユーザーから通報があったとき" abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき" abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき" userCreated: "ユーザーが作成されたとき"
inactiveModeratorsWarning: "モデレーターがしばらくおらんかったとき"
inactiveModeratorsInvitationOnlyChanged: "モデレーターがしばらくおらんかったから、システムが招待制に変えたとき"
deleteConfirm: "ほんまにWebhookをほかしてもええんか" deleteConfirm: "ほんまにWebhookをほかしてもええんか"
testRemarks: "スイッチ右のボタンを押すとダミーデータを使ったテスト用Webhookを送れるで。"
_abuseReport: _abuseReport:
_notificationRecipient: _notificationRecipient:
createRecipient: "通報の通知先を追加" createRecipient: "通報の通知先を追加"
@ -2480,6 +2539,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "苦情を解決" resolveAbuseReport: "苦情を解決"
forwardAbuseReport: "通報を転送"
updateAbuseReportNote: "通報のモデレーションノート更新"
createInvitation: "招待コード作る" createInvitation: "招待コード作る"
createAd: "広告を作んで" createAd: "広告を作んで"
deleteAd: "広告ほかす" deleteAd: "広告ほかす"
@ -2491,6 +2552,14 @@ _moderationLogTypes:
unsetUserBanner: "この子のバナー元に戻す" unsetUserBanner: "この子のバナー元に戻す"
createSystemWebhook: "SystemWebhookを作成" createSystemWebhook: "SystemWebhookを作成"
updateSystemWebhook: "SystemWebhookを更新" updateSystemWebhook: "SystemWebhookを更新"
deleteSystemWebhook: "SystemWebhookを削除"
createAbuseReportNotificationRecipient: "通報の通知先作る"
updateAbuseReportNotificationRecipient: "通報の通知先更新"
deleteAbuseReportNotificationRecipient: "通報の通知先消す"
deleteAccount: "アカウント消す"
deletePage: "ページ消す"
deleteFlash: "Playをほかす"
deleteGalleryPost: "ギャラリーの投稿をほかす"
_fileViewer: _fileViewer:
title: "ファイルの詳しい情報" title: "ファイルの詳しい情報"
type: "ファイルの種類" type: "ファイルの種類"
@ -2622,3 +2691,22 @@ _mediaControls:
pip: "ピクチャインピクチャ" pip: "ピクチャインピクチャ"
playbackRate: "再生速度" playbackRate: "再生速度"
loop: "ループ再生" loop: "ループ再生"
_contextMenu:
title: "コンテキストメニュー"
app: "アプリ"
appWithShift: "Shiftキーでアプリ"
native: "ブラウザのUI"
_embedCodeGen:
title: "埋め込みコードをカスタム"
header: "ヘッダー出す"
autoload: "勝手に続きを読み込む(非推奨)"
maxHeight: "高さの最大値"
maxHeightDescription: "0は最大値を指定せえへんけど、ウィジェットが伸び続けるから絶対1以上にしといてや。"
maxHeightWarn: "高さの最大値が無効になっとるで。意図してへん変更なら、普通の値に戻してや。"
previewIsNotActual: "プレビュー画面で出せる範囲をはみ出したから、ホンマの表示とはちゃうとおもうで。"
rounded: "角丸める"
border: "外枠に枠線つける"
applyToPreview: "プレビューに反映"
generateCode: "埋め込みコード作る"
codeGenerated: "コード作ったで"
codeGeneratedDescription: "作ったコードはウェブサイトに貼っつけて使ってや。"

View file

@ -468,7 +468,7 @@ tooShort: "억수로 짜립니다"
tooLong: "억수로 집니다" tooLong: "억수로 집니다"
passwordMatched: "맞십니다" passwordMatched: "맞십니다"
passwordNotMatched: "안 맞십니다" passwordNotMatched: "안 맞십니다"
signinWith: "{n}서 로그인" signinWith: "{x} 서 로그인"
signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소." signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소."
or: "아니면" or: "아니면"
language: "언어" language: "언어"
@ -809,11 +809,13 @@ _notification:
_types: _types:
follow: "팔로잉" follow: "팔로잉"
mention: "멘션" mention: "멘션"
renote: "리노트"
quote: "따오기" quote: "따오기"
reaction: "반엉" reaction: "반엉"
login: "로그인" login: "로그인"
_actions: _actions:
reply: "답하기" reply: "답하기"
renote: "리노트"
_deck: _deck:
_columns: _columns:
notifications: "알림" notifications: "알림"

View file

@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "지금 다시 시도하시겠습니까?"
retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 있습니다." retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 있습니다."
enableChartsForRemoteUser: "리모트 유저의 차트를 생성" enableChartsForRemoteUser: "리모트 유저의 차트를 생성"
enableChartsForFederatedInstances: "리모트 서버의 차트를 생성" enableChartsForFederatedInstances: "리모트 서버의 차트를 생성"
enableStatsForFederatedInstances: "리모트 서버 정보 받아오기"
showClipButtonInNoteFooter: "노트 동작에 클립을 추가" showClipButtonInNoteFooter: "노트 동작에 클립을 추가"
reactionsDisplaySize: "리액션 표시 크기" reactionsDisplaySize: "리액션 표시 크기"
limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기" limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기"
@ -1287,6 +1288,11 @@ passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다." passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
messageToFollower: "팔로워에 보낼 메시지" messageToFollower: "팔로워에 보낼 메시지"
target: "대상" target: "대상"
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
_abuseUserReport: _abuseUserReport:
forward: "전달" forward: "전달"
forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다." forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
@ -1431,6 +1437,7 @@ _serverSettings:
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다." reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
inquiryUrl: "문의처 URL" inquiryUrl: "문의처 URL"
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다." inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
_accountMigration: _accountMigration:
moveFrom: "다른 계정에서 이 계정으로 이사" moveFrom: "다른 계정에서 이 계정으로 이사"
moveFromSub: "다른 계정에 대한 별칭을 생성" moveFromSub: "다른 계정에 대한 별칭을 생성"
@ -2485,6 +2492,8 @@ _webhookSettings:
abuseReport: "유저롭" abuseReport: "유저롭"
abuseReportResolved: "받은 신고를 처리했을 때" abuseReportResolved: "받은 신고를 처리했을 때"
userCreated: "유저가 생성되었을 때" userCreated: "유저가 생성되었을 때"
inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우"
inactiveModeratorsInvitationOnlyChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 초대제로 바뀐 경우"
deleteConfirm: "Webhook을 삭제할까요?" deleteConfirm: "Webhook을 삭제할까요?"
testRemarks: "스위치 오른쪽에 있는 버튼을 클릭하여 더미 데이터를 사용한 테스트용 웹 훅을 보낼 수 있습니다." testRemarks: "스위치 오른쪽에 있는 버튼을 클릭하여 더미 데이터를 사용한 테스트용 웹 훅을 보낼 수 있습니다."
_abuseReport: _abuseReport:

View file

@ -8,6 +8,9 @@ search: "Поиск"
notifications: "Уведомления" notifications: "Уведомления"
username: "Имя пользователя" username: "Имя пользователя"
password: "Пароль" password: "Пароль"
initialPasswordForSetup: "Пароль для начала настройки"
initialPasswordIsIncorrect: "Пароль для запуска настройки неверен"
initialPasswordForSetupDescription: "Если вы установили Misskey самостоятельно, используйте пароль, который вы указали в файле конфигурации.\nЕсли вы используете что-то вроде хостинга Misskey, используйте предоставленный пароль.\nЕсли вы не установили пароль, оставьте его пустым и продолжайте."
forgotPassword: "Забыли пароль?" forgotPassword: "Забыли пароль?"
fetchingAsApObject: "Приём с других сайтов" fetchingAsApObject: "Приём с других сайтов"
ok: "Подтвердить" ok: "Подтвердить"
@ -232,6 +235,7 @@ clearCachedFilesConfirm: "Удалить все закэшированные ф
blockedInstances: "Заблокированные инстансы" blockedInstances: "Заблокированные инстансы"
blockedInstancesDescription: "Введите список инстансов, которые хотите заблокировать. Они больше не смогут обмениваться с вашим инстансом." blockedInstancesDescription: "Введите список инстансов, которые хотите заблокировать. Они больше не смогут обмениваться с вашим инстансом."
silencedInstances: "Заглушённые инстансы" silencedInstances: "Заглушённые инстансы"
federationAllowedHosts: "Серверы, поддерживающие федерацию"
muteAndBlock: "Скрытие и блокировка" muteAndBlock: "Скрытие и блокировка"
mutedUsers: "Скрытые пользователи" mutedUsers: "Скрытые пользователи"
blockedUsers: "Заблокированные пользователи" blockedUsers: "Заблокированные пользователи"
@ -330,6 +334,7 @@ renameFolder: "Переименовать папку"
deleteFolder: "Удалить папку" deleteFolder: "Удалить папку"
folder: "Папка" folder: "Папка"
addFile: "Добавить файл" addFile: "Добавить файл"
showFile: "Посмотреть файл"
emptyDrive: "Диск пуст" emptyDrive: "Диск пуст"
emptyFolder: "Папка пуста" emptyFolder: "Папка пуста"
unableToDelete: "Удаление невозможно" unableToDelete: "Удаление невозможно"
@ -443,6 +448,7 @@ totp: "Приложение-аутентификатор"
totpDescription: "Описание приложения-аутентификатора" totpDescription: "Описание приложения-аутентификатора"
moderator: "Модератор" moderator: "Модератор"
moderation: "Модерация" moderation: "Модерация"
moderationNote: "Примечания модератора"
moderationLogs: "Журнал модерации" moderationLogs: "Журнал модерации"
nUsersMentioned: "Упомянуло пользователей: {n}" nUsersMentioned: "Упомянуло пользователей: {n}"
securityKeyAndPasskey: "Ключ безопасности и парольная фраза" securityKeyAndPasskey: "Ключ безопасности и парольная фраза"
@ -503,6 +509,8 @@ uiLanguage: "Язык интерфейса"
aboutX: "Описание {x}" aboutX: "Описание {x}"
emojiStyle: "Стиль эмодзи" emojiStyle: "Стиль эмодзи"
native: "Системные" native: "Системные"
menuStyle: "Стиль меню"
style: "Стиль"
showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении" showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении"
showReactionsCount: "Видеть количество реакций на заметках" showReactionsCount: "Видеть количество реакций на заметках"
noHistory: "История пока пуста" noHistory: "История пока пуста"

View file

@ -8,6 +8,9 @@ search: "ค้นหา"
notifications: "เเจ้งเตือน" notifications: "เเจ้งเตือน"
username: "ชื่อผู้ใช้" username: "ชื่อผู้ใช้"
password: "รหัสผ่าน" password: "รหัสผ่าน"
initialPasswordForSetup: "รหัสผ่านเริ่มต้นสำหรับการตั้งค่า"
initialPasswordIsIncorrect: "รหัสผ่านเริ่มต้นสำหรับตั้งค่านั้นไม่ถูกต้องค่ะ"
initialPasswordForSetupDescription: "ถ้าหากคุณติดตั้ง Misskey เอง ให้ใช้รหัสผ่านที่คุณป้อนในไฟล์กำหนดค่า \nถ้าหากคุณกำลังใช้บริการโฮสต์ Misskey ให้ใช้รหัสผ่านที่ได้รับมา\nถ้ายังไม่มีรหัสผ่าน ให้ข้ามช่องรหัสผ่านไป แล้วกดต่อไป"
forgotPassword: "ลืมรหัสผ่าน" forgotPassword: "ลืมรหัสผ่าน"
fetchingAsApObject: "กำลังดึงข้อมูลจากสหพันธ์..." fetchingAsApObject: "กำลังดึงข้อมูลจากสหพันธ์..."
ok: "ตกลง" ok: "ตกลง"
@ -236,6 +239,8 @@ silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้
silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ" mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ"
mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
federationAllowedHosts: "เซิร์ฟเวอร์ที่เปิดให้บริการแบบเฟเดอเรชั่น"
federationAllowedHostsDescription: "ระบุชื่อโฮสต์ของเซิร์ฟเวอร์ที่คุณต้องการอนุญาตให้เชื่อมต่อแบบเฟเดอเรชั่น โดยต้องเว้นวรรคแต่ละบรรทัด"
muteAndBlock: "ปิดเสียงและบล็อก" muteAndBlock: "ปิดเสียงและบล็อก"
mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง" mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง"
blockedUsers: "ผู้ใช้ที่ถูกบล็อก" blockedUsers: "ผู้ใช้ที่ถูกบล็อก"
@ -334,6 +339,7 @@ renameFolder: "เปลี่ยนชื่อโฟลเดอร์"
deleteFolder: "ลบโฟลเดอร์" deleteFolder: "ลบโฟลเดอร์"
folder: "โฟลเดอร์" folder: "โฟลเดอร์"
addFile: "เพิ่มไฟล์" addFile: "เพิ่มไฟล์"
showFile: "แสดงไฟล์"
emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ" emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ"
emptyFolder: "โฟลเดอร์นี้ว่างเปล่า" emptyFolder: "โฟลเดอร์นี้ว่างเปล่า"
unableToDelete: "ไม่สามารถลบออกได้" unableToDelete: "ไม่สามารถลบออกได้"
@ -448,6 +454,7 @@ totpDescription: "ใช้แอปยืนยันตัวตนเพื
moderator: "ผู้ควบคุม" moderator: "ผู้ควบคุม"
moderation: "การกลั่นกรอง" moderation: "การกลั่นกรอง"
moderationNote: "โน้ตการกลั่นกรอง" moderationNote: "โน้ตการกลั่นกรอง"
moderationNoteDescription: "คุณสามารถใส่โน้ตส่วนตัวที่เฉพาะผู้ดูแลระบบเท่านั้นที่สามารถเข้าถึงได้"
addModerationNote: "เพิ่มโน้ตการกลั่นกรอง" addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
moderationLogs: "ปูมการควบคุมดูแล" moderationLogs: "ปูมการควบคุมดูแล"
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย" nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย"
@ -509,6 +516,10 @@ uiLanguage: "ภาษาอินเทอร์เฟซผู้ใช้ง
aboutX: "เกี่ยวกับ {x}" aboutX: "เกี่ยวกับ {x}"
emojiStyle: "สไตล์ของเอโมจิ" emojiStyle: "สไตล์ของเอโมจิ"
native: "ภาษาแม่" native: "ภาษาแม่"
menuStyle: "สไตล์เมนู"
style: "สไตล์"
drawer: "ตัววาด"
popup: "ป๊อปอัพ"
showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น" showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น"
showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต" showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต"
noHistory: "ไม่มีประวัติ" noHistory: "ไม่มีประวัติ"
@ -591,6 +602,8 @@ ascendingOrder: "เรียงลำดับขึ้น"
descendingOrder: "เรียงลำดับลง" descendingOrder: "เรียงลำดับลง"
scratchpad: "Scratchpad" scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad ให้สภาพแวดล้อมสำหรับการทดลอง AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินการ/ตรวจสอบผลลัพธ์ ของการโต้ตอบกับ Misskey ได้" scratchpadDescription: "Scratchpad ให้สภาพแวดล้อมสำหรับการทดลอง AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินการ/ตรวจสอบผลลัพธ์ ของการโต้ตอบกับ Misskey ได้"
uiInspector: "ตัวตรวจสอบ UI"
uiInspectorDescription: "คุณสามารถตรวจสอบรายชื่อเซิร์ฟเวอร์ที่เกี่ยวข้องกับส่วนประกอบอินเตอร์เฟซผู้ใช้ (UI) บนหน่วยความจำของระบบ ส่วนประกอบ UI เหล่านี้จะถูกสร้างขึ้นโดยฟังก์ชัน Ui:C:"
output: "เอาท์พุต" output: "เอาท์พุต"
script: "สคริปต์" script: "สคริปต์"
disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ" disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ"
@ -909,6 +922,7 @@ followersVisibility: "การมองเห็นผู้ที่กำล
continueThread: "ดูความต่อเนื่องเธรด" continueThread: "ดูความต่อเนื่องเธรด"
deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?" deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
incorrectPassword: "รหัสผ่านไม่ถูกต้อง" incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
incorrectTotp: "รหัสยืนยันตัวตนแบบใช้ครั้งเดียวที่ท่านได้ระบุมานั้น ไม่ถูกต้องหรือหมดอายุลงแล้วค่ะ"
voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?" voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?"
hide: "ซ่อน" hide: "ซ่อน"
useDrawerReactionPickerForMobile: "แสดง ตัวจิ้มรีแอคชั่น เป็นแบบลิ้นชัก เมื่อใช้บนมือถือ" useDrawerReactionPickerForMobile: "แสดง ตัวจิ้มรีแอคชั่น เป็นแบบลิ้นชัก เมื่อใช้บนมือถือ"
@ -1073,6 +1087,7 @@ retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริ
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ" retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล" enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล" enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล"
enableStatsForFederatedInstances: "ดึงข้อมูลสถิติจากเซิร์ฟเวอร์ที่อยู่ห่างไกล"
showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต" showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต"
reactionsDisplaySize: "ขนาดของรีแอคชั่น" reactionsDisplaySize: "ขนาดของรีแอคชั่น"
limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง" limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง"
@ -1259,6 +1274,32 @@ confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสด
sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?" sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?"
createdLists: "รายชื่อที่ถูกสร้าง" createdLists: "รายชื่อที่ถูกสร้าง"
createdAntennas: "เสาอากาศที่ถูกสร้าง" createdAntennas: "เสาอากาศที่ถูกสร้าง"
fromX: "จาก {x}"
genEmbedCode: "สร้างรหัสฝัง"
noteOfThisUser: "โน้ตโดยผู้ใช้นี้"
clipNoteLimitExceeded: "ไม่สามารถเพิ่มโน้ตเพิ่มเติมในคลิปนี้ได้อีกแล้ว"
performance: "ประสิทธิภาพ​"
modified: "แก้ไข"
discard: "ละทิ้ง"
thereAreNChanges: "มีอยู่ {n} เปลี่ยนแปลง(s)"
signinWithPasskey: "ลงชื่อเข้าใช้ด้วย Passkey"
unknownWebAuthnKey: "พาสคีย์ไม่ถูกต้องค่ะ"
passkeyVerificationFailed: "การยืนยันกุญแจดิจิทัลไม่สำเร็จค่ะ"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "การยืนยันพาสคีย์สำเร็จแล้ว แต่การลงชื่อเข้าใช้แบบไม่ต้องใส่รหัสผ่านถูกปิดใช้งานแล้ว"
messageToFollower: "ข้อความถึงผู้ติดตาม"
target: "เป้า"
testCaptchaWarning: "ฟังก์ชันนี้มีไว้สำหรับทดสอบ CAPTCHA เท่านั้น\n<strong>ห้ามนำไปใช้ในระบบจริงโดยเด็ดขาด</strong>"
prohibitedWordsForNameOfUser: "คำนี้ไม่สามารถใช้เป็นชื่อผู้ใช้ได้"
prohibitedWordsForNameOfUserDescription: "หากมีสตริงใดๆ ในรายการนี้ปรากฏอยู่ในชื่อของผู้ใช้ ชื่อนั้นจะถูกปฏิเสธ ผู้ใช้ที่มีสิทธิ์แต่ผู้ดูแลระบบนั้นจะไม่ได้รับผลกระทบใดๆจากข้อจำกัดนี้ค่ะ"
yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม"
yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ"
_abuseUserReport:
forward: "ส่ง​ต่อ"
forwardDescription: "ส่งรายงานไปยังเซิร์ฟเวอร์ระยะไกลโดยใช้บัญชีระบบที่ไม่ระบุตัวตน"
resolve: "แก้ไข"
accept: "ยอมรับ"
reject: "ปฏิเสธ"
resolveTutorial: "ถ้าหากรายงานนี้มีเนื้อหาถูกต้อง ให้เลือก \"ยอมรับ\" เพื่อปิดเคสกรณีนี้โดยถือว่าได้รับการแก้ไขแล้ว\nถ้าหากเนื้อหาในรายงานนี้นั้นไม่ถูกต้อง ให้เลือก \"ปฏิเสธ\" เพื่อปิดเคสกรณีนี้โดยถือว่าไม่ได้รับการแก้ไข"
_delivery: _delivery:
status: "สถานะการจัดส่ง" status: "สถานะการจัดส่ง"
stop: "ระงับการส่ง" stop: "ระงับการส่ง"
@ -1393,8 +1434,10 @@ _serverSettings:
fanoutTimelineDescription: "เพิ่มประสิทธิภาพการดึงข้อมูลไทม์ไลน์อย่างมาก และลดภาระในฐานข้อมูลเมื่อเปิดใช้งาน ในทางกลับกัน การใช้หน่วยความจำของ Redis จะเพิ่มขึ้น ลองปิดการใช้งานนี้ในกรณีที่หน่วยความจำเซิร์ฟเวอร์เหลือน้อยหรือเซิร์ฟเวอร์ไม่เสถียร" fanoutTimelineDescription: "เพิ่มประสิทธิภาพการดึงข้อมูลไทม์ไลน์อย่างมาก และลดภาระในฐานข้อมูลเมื่อเปิดใช้งาน ในทางกลับกัน การใช้หน่วยความจำของ Redis จะเพิ่มขึ้น ลองปิดการใช้งานนี้ในกรณีที่หน่วยความจำเซิร์ฟเวอร์เหลือน้อยหรือเซิร์ฟเวอร์ไม่เสถียร"
fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล" fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล"
fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้" fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้"
reactionsBufferingDescription: "เมื่อเปิดใช้งานฟังก์ชันนี้ก็จะช่วยลด latency ในการสร้างปฏิกิริยา แต่อาจจะส่งผลให้ memory footprint ของ Redis เพิ่มขึ้นนะ"
inquiryUrl: "URL สำหรับการติดต่อสอบถาม" inquiryUrl: "URL สำหรับการติดต่อสอบถาม"
inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์" inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "ถ้าหากไม่มีการตรวจสอบจากผู้ดูแลระบบหรือไม่มีความเคลื่อนไหวมาเป็นระยะเวลาหนึ่ง ระบบจะทำการปิดใช้งานฟังก์ชันนี้โดยอัตโนมัติ เพื่อลดความเสี่ยงในการถูกโจมตีด้วยสแปมและอื่นๆ"
_accountMigration: _accountMigration:
moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้" moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้"
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
@ -1726,6 +1769,11 @@ _role:
canSearchNotes: "การใช้การค้นหาโน้ต" canSearchNotes: "การใช้การค้นหาโน้ต"
canUseTranslator: "การใช้งานแปล" canUseTranslator: "การใช้งานแปล"
avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้" avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้"
canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ"
canImportBlocking: "อนุญาตให้นำเข้าการบล็อก"
canImportFollowing: "อนุญาตให้นำเข้ารายการต่อไปนี้"
canImportMuting: "อนุญาตให้นำเข้าการปิดกั้น"
canImportUserLists: "อนุญาตให้นำเข้ารายการ"
_condition: _condition:
roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ"
isLocal: "ผู้ใช้ท้องถิ่น" isLocal: "ผู้ใช้ท้องถิ่น"
@ -2219,6 +2267,9 @@ _profile:
changeBanner: "เปลี่ยนแบนเนอร์" changeBanner: "เปลี่ยนแบนเนอร์"
verifiedLinkDescription: "หากป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณ ไอคอนการยืนยันความเป็นเจ้าของจะแสดงถัดจากฟิลด์นั้น ๆ" verifiedLinkDescription: "หากป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณ ไอคอนการยืนยันความเป็นเจ้าของจะแสดงถัดจากฟิลด์นั้น ๆ"
avatarDecorationMax: "คุณสามารถเพิ่มการตกแต่งได้สูงสุด {max}" avatarDecorationMax: "คุณสามารถเพิ่มการตกแต่งได้สูงสุด {max}"
followedMessage: "ส่งข้อความเมื่อมีคนกดติดตาม"
followedMessageDescription: "ส่งข้อความเมื่อมีคนกดติดตามแล้ว"
followedMessageDescriptionForLockedAccount: "ถ้าหากคุณตั้งค่าให้คนอื่นต้องขออนุญาตก่อนที่จะติดตามคุณ ระบบจะขึ้นข้อความนี้ในตอนที่คุณอนุมัติให้เขาติดตาม"
_exportOrImport: _exportOrImport:
allNotes: "โน้ตทั้งหมด" allNotes: "โน้ตทั้งหมด"
favoritedNotes: "โน้ตที่ถูกใจไว้" favoritedNotes: "โน้ตที่ถูกใจไว้"
@ -2311,6 +2362,7 @@ _pages:
eyeCatchingImageSet: "ตั้งค่าภาพขนาดย่อ" eyeCatchingImageSet: "ตั้งค่าภาพขนาดย่อ"
eyeCatchingImageRemove: "ลบภาพขนาดย่อ" eyeCatchingImageRemove: "ลบภาพขนาดย่อ"
chooseBlock: "เพิ่มบล็อค" chooseBlock: "เพิ่มบล็อค"
enterSectionTitle: "ป้อนชื่อหัวข้อ"
selectType: "เลือกชนิด" selectType: "เลือกชนิด"
contentBlocks: "เนื้อหา" contentBlocks: "เนื้อหา"
inputBlocks: "ป้อนข้อมูล" inputBlocks: "ป้อนข้อมูล"
@ -2356,6 +2408,8 @@ _notification:
renotedBySomeUsers: "รีโน้ตจากผู้ใช้ {n} ราย" renotedBySomeUsers: "รีโน้ตจากผู้ใช้ {n} ราย"
followedBySomeUsers: "มีผู้ติดตาม {n} ราย" followedBySomeUsers: "มีผู้ติดตาม {n} ราย"
flushNotification: "ล้างประวัติการแจ้งเตือน" flushNotification: "ล้างประวัติการแจ้งเตือน"
exportOfXCompleted: "การดำเนินการส่งออก {x} ได้เสร็จสิ้นลงแล้ว"
login: "มีคนล็อกอิน"
_types: _types:
all: "ทั้งหมด" all: "ทั้งหมด"
note: "โน้ตใหม่" note: "โน้ตใหม่"
@ -2370,7 +2424,9 @@ _notification:
followRequestAccepted: "อนุมัติให้ติดตามแล้ว" followRequestAccepted: "อนุมัติให้ติดตามแล้ว"
roleAssigned: "ให้บทบาท" roleAssigned: "ให้บทบาท"
achievementEarned: "ปลดล็อกความสำเร็จแล้ว" achievementEarned: "ปลดล็อกความสำเร็จแล้ว"
exportCompleted: "กระบวนการส่งออกข้อมูลได้เสร็จสิ้นสมบูรณ์แล้ว"
login: "เข้าสู่ระบบ" login: "เข้าสู่ระบบ"
test: "ทดสอบระบบแจ้งเตือน"
app: "การแจ้งเตือนจากแอปที่มีลิงก์" app: "การแจ้งเตือนจากแอปที่มีลิงก์"
_actions: _actions:
followBack: "ติดตามกลับด้วย" followBack: "ติดตามกลับด้วย"
@ -2436,7 +2492,10 @@ _webhookSettings:
abuseReport: "เมื่อมีการรายงานจากผู้ใช้" abuseReport: "เมื่อมีการรายงานจากผู้ใช้"
abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้" abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้"
userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น" userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น"
inactiveModeratorsWarning: "เมื่อผู้ดูแลระบบไม่ได้ใช้งานมานานระยะหนึ่ง"
inactiveModeratorsInvitationOnlyChanged: "เมื่อผู้ดูแลระบบที่ไม่ได้ใช้งานมานาน และเซิร์ฟเวอร์เปลี่ยนเป็นแบบเชิญเข้าร่วมเท่านั้น"
deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?" deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?"
testRemarks: "คลิกปุ่มทางด้านขวาของสวิตช์เพื่อส่ง Webhook ทดสอบที่มีข้อมูลจำลอง"
_abuseReport: _abuseReport:
_notificationRecipient: _notificationRecipient:
createRecipient: "เพิ่มปลายทางการแจ้งเตือนการรายงาน" createRecipient: "เพิ่มปลายทางการแจ้งเตือนการรายงาน"
@ -2480,6 +2539,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน" markSensitiveDriveFile: "ทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน"
unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน" unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน"
resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว" resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
forwardAbuseReport: "ได้ส่งรายงานไปแล้ว"
updateAbuseReportNote: "โน้ตการกลั่นกรองที่รายงานไปนั้น ได้รับการอัปเดตแล้ว"
createInvitation: "สร้างรหัสเชิญ" createInvitation: "สร้างรหัสเชิญ"
createAd: "สร้างโฆษณาแล้ว" createAd: "สร้างโฆษณาแล้ว"
deleteAd: "ลบโฆษณาออกแล้ว" deleteAd: "ลบโฆษณาออกแล้ว"
@ -2495,6 +2556,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "สร้างปลายทางการแจ้งเตือนการรายงาน" createAbuseReportNotificationRecipient: "สร้างปลายทางการแจ้งเตือนการรายงาน"
updateAbuseReportNotificationRecipient: "อัปเดตปลายทางการแจ้งเตือนการรายงาน" updateAbuseReportNotificationRecipient: "อัปเดตปลายทางการแจ้งเตือนการรายงาน"
deleteAbuseReportNotificationRecipient: "ลบปลายทางการแจ้งเตือนการรายงาน" deleteAbuseReportNotificationRecipient: "ลบปลายทางการแจ้งเตือนการรายงาน"
deleteAccount: "บัญชีถูกลบไปแล้ว"
deletePage: "เพจถูกลบออกไปแล้ว"
deleteFlash: "Play ถูกลบออกไปแล้ว"
deleteGalleryPost: "โพสต์แกลเลอรี่ถูกลบออกแล้ว"
_fileViewer: _fileViewer:
title: "รายละเอียดไฟล์" title: "รายละเอียดไฟล์"
type: "ประเภทไฟล์" type: "ประเภทไฟล์"
@ -2631,3 +2696,17 @@ _contextMenu:
app: "แอปพลิเคชัน" app: "แอปพลิเคชัน"
appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)" appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)"
native: "UI ของเบราว์เซอร์" native: "UI ของเบราว์เซอร์"
_embedCodeGen:
title: "ปรับแต่งโค้ดฝัง"
header: "แสดงส่วนหัว"
autoload: "โหลดเพิ่มโดยอัตโนมัติ (เลิกใช้แล้ว)"
maxHeight: "ความสูงสุด"
maxHeightDescription: "หากถ้าตั้งค่าเป็น 0 จะทำให้ไม่มีการจำกัดความสูงของวิดเจ็ต แต่ควรตั้งค่าเป็นตัวเลขอื่นๆ เพื่อไม่ให้วิดเจ็ตยืดตัวลงไปเรื่อยๆ"
maxHeightWarn: "การจำกัดความสูงสูงสุดถูกปิดใช้งาน (0) หากไม่ได้ตั้งใจให้เป็นเช่นนี้ โปรดตั้งค่าความสูงสูงสุดให้เป็นค่าอื่นๆแทน"
previewIsNotActual: "การแสดงผลนั้นต่างจากการฝังจริงเพราะเกินขอบเขตที่แสดงบนหน้าจอตัวอย่างนะ"
rounded: "ทำให้มันกลม"
border: "เพิ่มขอบให้กับกรอบด้านนอก"
applyToPreview: "นำไปใช้กับการแสดงตัวอย่าง"
generateCode: "สร้างโค้ดสำหรับการฝัง"
codeGenerated: "รหัสถูกสร้างขึ้นแล้ว"
codeGeneratedDescription: "นำโค้ดที่สร้างแล้วไปวางในเว็บไซต์ของคุณเพื่อฝังเนื้อหา"

View file

@ -213,8 +213,8 @@ charts: "图表"
perHour: "每小时" perHour: "每小时"
perDay: "每天" perDay: "每天"
stopActivityDelivery: "停止发送活动" stopActivityDelivery: "停止发送活动"
blockThisInstance: "阻止此服务器向本服务器推流" blockThisInstance: "封锁此服务器"
silenceThisInstance: "使服务器静音" silenceThisInstance: "静音此服务器"
mediaSilenceThisInstance: "隐藏此服务器的媒体文件" mediaSilenceThisInstance: "隐藏此服务器的媒体文件"
operations: "操作" operations: "操作"
software: "软件" software: "软件"
@ -258,7 +258,7 @@ noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务" noJobs: "没有任务"
federating: "联合中" federating: "联合中"
blocked: "已拉黑" blocked: "已拉黑"
suspended: "停止推流" suspended: "停止投递"
all: "全部" all: "全部"
subscribing: "已订阅" subscribing: "已订阅"
publishing: "投递中" publishing: "投递中"
@ -706,7 +706,7 @@ useGlobalSettingDesc: "启用时,将使用账户通知设置。关闭时,则
other: "其他" other: "其他"
regenerateLoginToken: "重新生成登录令牌" regenerateLoginToken: "重新生成登录令牌"
regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。" regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。"
theKeywordWhenSearchingForCustomEmoji: "这将是搜自定义表情符号时的关键词。" theKeywordWhenSearchingForCustomEmoji: "这将是搜自定义表情符号时的关键词。"
setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。"
fileIdOrUrl: "文件 ID 或者 URL" fileIdOrUrl: "文件 ID 或者 URL"
behavior: "行为" behavior: "行为"
@ -947,6 +947,9 @@ oneHour: "1 小时"
oneDay: "1 天" oneDay: "1 天"
oneWeek: "1 周" oneWeek: "1 周"
oneMonth: "1 个月" oneMonth: "1 个月"
threeMonths: "3 个月"
oneYear: "1 年"
threeDays: "3 天"
reflectMayTakeTime: "可能需要一些时间才能体现出效果。" reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
failedToFetchAccountInformation: "获取账户信息失败" failedToFetchAccountInformation: "获取账户信息失败"
rateLimitExceeded: "已超过速率限制" rateLimitExceeded: "已超过速率限制"
@ -1070,7 +1073,7 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点
rolesAssignedToMe: "指派给自己的角色" rolesAssignedToMe: "指派给自己的角色"
resetPasswordConfirm: "确定重置密码?" resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词" sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。"
sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
prohibitedWords: "禁用词" prohibitedWords: "禁用词"
prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字" prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字"
@ -1087,6 +1090,7 @@ retryAllQueuesConfirmTitle: "要再尝试一次吗?"
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加" retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表" enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表" enableChartsForFederatedInstances: "生成远程服务器的图表"
enableStatsForFederatedInstances: "获取远程服务器的信息"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮" showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
reactionsDisplaySize: "回应显示大小" reactionsDisplaySize: "回应显示大小"
limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示" limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示"
@ -1287,6 +1291,26 @@ passkeyVerificationFailed: "验证通行密钥失败。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。"
messageToFollower: "给关注者的消息" messageToFollower: "给关注者的消息"
target: "对象" target: "对象"
testCaptchaWarning: "此功能为测试 CAPTCHA 用。<strong>请勿在正式环境中使用。</strong>"
prohibitedWordsForNameOfUser: "用户名中禁止的词"
prohibitedWordsForNameOfUserDescription: "更改用户名时,如果用户名中包含此列表里的词汇,用户的改名请求将被拒绝。持有管理员权限的用户不受此限制。"
yourNameContainsProhibitedWords: "目标用户名包含违禁词"
yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若想使用此用户名,请联系服务器管理员。"
thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示"
lockdown: "锁定"
pleaseSelectAccount: "请选择帐户"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
requireSigninToViewContentsDescription2: "没有 URL 预览OGP、内嵌网页、引用帖子的功能的服务器也将无法显示。"
requireSigninToViewContentsDescription3: "这些限制可能不适用于联合到远程服务器的内容。"
makeNotesFollowersOnlyBefore: "可将过去的帖子设为仅关注者可见"
makeNotesFollowersOnlyBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅关注者可见。关闭后帖子的公开状态将恢复成原本的设定。"
makeNotesHiddenBefore: "将过去的帖子设为私密"
makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。"
mayNotEffectForFederatedNotes: "与远程服务器联合的帖子在远端可能会没有效果。"
notesHavePassedSpecifiedPeriod: "超过指定时间的帖子"
notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子"
_abuseUserReport: _abuseUserReport:
forward: "转发" forward: "转发"
forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。" forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
@ -1431,6 +1455,7 @@ _serverSettings:
reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。" reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。"
inquiryUrl: "联络地址" inquiryUrl: "联络地址"
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。" inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
_accountMigration: _accountMigration:
moveFrom: "从别的账号迁移到此账户" moveFrom: "从别的账号迁移到此账户"
moveFromSub: "为另一个账户建立别名" moveFromSub: "为另一个账户建立别名"
@ -2150,8 +2175,11 @@ _auth:
permissionAsk: "这个应用程序需要以下权限" permissionAsk: "这个应用程序需要以下权限"
pleaseGoBack: "请返回到应用程序" pleaseGoBack: "请返回到应用程序"
callback: "回到应用程序" callback: "回到应用程序"
accepted: "已允许访问"
denied: "拒绝访问" denied: "拒绝访问"
scopeUser: "以下面的用户进行操作"
pleaseLogin: "在对应用进行授权许可之前,请先登录" pleaseLogin: "在对应用进行授权许可之前,请先登录"
byClickingYouWillBeRedirectedToThisUrl: "允许访问后将会自动重定向到以下 URL"
_antennaSources: _antennaSources:
all: "所有帖子" all: "所有帖子"
homeTimeline: "已关注用户的帖子" homeTimeline: "已关注用户的帖子"
@ -2262,7 +2290,7 @@ _profile:
avatarDecorationMax: "最多可添加 {max} 个挂件" avatarDecorationMax: "最多可添加 {max} 个挂件"
followedMessage: "被关注时显示的消息" followedMessage: "被关注时显示的消息"
followedMessageDescription: "可以设置被关注时向对方显示的短消息。" followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。" followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。"
_exportOrImport: _exportOrImport:
allNotes: "所有帖子" allNotes: "所有帖子"
favoritedNotes: "收藏的帖子" favoritedNotes: "收藏的帖子"
@ -2485,6 +2513,8 @@ _webhookSettings:
abuseReport: "当收到举报时" abuseReport: "当收到举报时"
abuseReportResolved: "当举报被处理时" abuseReportResolved: "当举报被处理时"
userCreated: "当用户被创建时" userCreated: "当用户被创建时"
inactiveModeratorsWarning: "当管理员在一段时间内不活跃时"
inactiveModeratorsInvitationOnlyChanged: "当因为管理员在一段时间内不活跃,导致服务器变为邀请制时"
deleteConfirm: "要删除 webhook 吗?" deleteConfirm: "要删除 webhook 吗?"
testRemarks: "点击开关右侧的按钮,可以发送使用假数据的测试 Webhook。" testRemarks: "点击开关右侧的按钮,可以发送使用假数据的测试 Webhook。"
_abuseReport: _abuseReport:
@ -2701,3 +2731,9 @@ _embedCodeGen:
generateCode: "生成嵌入代码" generateCode: "生成嵌入代码"
codeGenerated: "已生成代码" codeGenerated: "已生成代码"
codeGeneratedDescription: "将生成的代码贴到网站上来使用。" codeGeneratedDescription: "将生成的代码贴到网站上来使用。"
_selfXssPrevention:
warning: "警告"
title: "「在此处粘贴什么东西」是欺诈行为。"
description1: "如果在此处粘贴了什么,恶意用户可能会接管账户或者盗取个人资料。"
description2: "如果不能完全理解将要粘贴的内容,%c 请立即停止操作并关闭这个窗口。"
description3: "详情请看这里。{link}"

View file

@ -454,6 +454,7 @@ totpDescription: "以驗證應用程式輸入一次性密碼"
moderator: "審查員" moderator: "審查員"
moderation: "審查" moderation: "審查"
moderationNote: "管理筆記" moderationNote: "管理筆記"
moderationNoteDescription: "您可以編寫僅在審查員之間共用的註解。"
addModerationNote: "新增管理筆記" addModerationNote: "新增管理筆記"
moderationLogs: "管理日誌" moderationLogs: "管理日誌"
nUsersMentioned: "被 {n} 個人提及" nUsersMentioned: "被 {n} 個人提及"
@ -519,7 +520,7 @@ menuStyle: "選單風格"
style: "風格" style: "風格"
drawer: "側邊欄" drawer: "側邊欄"
popup: "彈出式視窗" popup: "彈出式視窗"
showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的"
showReactionsCount: "顯示貼文的反應數目" showReactionsCount: "顯示貼文的反應數目"
noHistory: "沒有歷史紀錄" noHistory: "沒有歷史紀錄"
signinHistory: "登入歷史" signinHistory: "登入歷史"
@ -946,6 +947,9 @@ oneHour: "一小時"
oneDay: "一天" oneDay: "一天"
oneWeek: "一週" oneWeek: "一週"
oneMonth: "一個月" oneMonth: "一個月"
threeMonths: "3 個月"
oneYear: "1 年"
threeDays: "3 日"
reflectMayTakeTime: "可能需要一些時間才會出現效果。" reflectMayTakeTime: "可能需要一些時間才會出現效果。"
failedToFetchAccountInformation: "取得帳戶資訊失敗" failedToFetchAccountInformation: "取得帳戶資訊失敗"
rateLimitExceeded: "已超過速率限制" rateLimitExceeded: "已超過速率限制"
@ -1018,7 +1022,7 @@ show: "檢視"
neverShow: "不再顯示" neverShow: "不再顯示"
remindMeLater: "以後再說" remindMeLater: "以後再說"
didYouLikeMisskey: "您喜歡 Misskey 嗎?" didYouLikeMisskey: "您喜歡 Misskey 嗎?"
pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!" pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發的工作能夠持續!"
correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。" correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。"
roles: "角色" roles: "角色"
role: "角色" role: "角色"
@ -1086,6 +1090,7 @@ retryAllQueuesConfirmTitle: "要現在重試嗎?"
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。" retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端使用者的圖表" enableChartsForRemoteUser: "生成遠端使用者的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表" enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
enableStatsForFederatedInstances: "取得遠端伺服器資訊"
showClipButtonInNoteFooter: "新增摘錄按鈕至貼文" showClipButtonInNoteFooter: "新增摘錄按鈕至貼文"
reactionsDisplaySize: "反應的顯示尺寸" reactionsDisplaySize: "反應的顯示尺寸"
limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。" limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。"
@ -1194,7 +1199,7 @@ showRenotes: "顯示其他人的轉發貼文"
edited: "已編輯" edited: "已編輯"
notificationRecieveConfig: "接受通知的設定" notificationRecieveConfig: "接受通知的設定"
mutualFollow: "互相追隨" mutualFollow: "互相追隨"
followingOrFollower: "追隨中或追隨者" followingOrFollower: "追隨中或追隨者"
fileAttachedOnly: "只顯示包含附件的貼文" fileAttachedOnly: "只顯示包含附件的貼文"
showRepliesToOthersInTimeline: "顯示給其他人的回覆" showRepliesToOthersInTimeline: "顯示給其他人的回覆"
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
@ -1265,7 +1270,7 @@ useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊"
keepOriginalFilename: "保留原始檔名" keepOriginalFilename: "保留原始檔名"
keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。" keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。"
noDescription: "沒有說明文字" noDescription: "沒有說明文字"
alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息" alwaysConfirmFollow: "跟隨時總是確認"
inquiry: "聯絡我們" inquiry: "聯絡我們"
tryAgain: "請再試一次。" tryAgain: "請再試一次。"
confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認" confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
@ -1285,6 +1290,34 @@ unknownWebAuthnKey: "未註冊的金鑰。"
passkeyVerificationFailed: "驗證金鑰失敗。" passkeyVerificationFailed: "驗證金鑰失敗。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。"
messageToFollower: "給追隨者的訊息" messageToFollower: "給追隨者的訊息"
target: "目標 "
testCaptchaWarning: "此功能用於 CAPTCHA 的測試。<strong>請勿在正式環境中使用。</strong>"
prohibitedWordsForNameOfUser: "禁止使用的字詞(使用者名稱)"
prohibitedWordsForNameOfUserDescription: "如果使用者名稱包含此清單中的任何字串,則拒絕重新命名使用者。 具有審查員權限的使用者不受此限制的影響。"
yourNameContainsProhibitedWords: "您嘗試更改的名稱包含禁止的字串"
yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字串。 如果您想使用此名稱,請聯絡您的伺服器管理員。"
thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。"
lockdown: "鎖定"
pleaseSelectAccount: "請選擇帳戶"
_accountSettings:
requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
requireSigninToViewContentsDescription2: "來自不支援 URL 預覽 (OGP)、 網頁嵌入和引用貼文的伺服器,也將停止顯示。"
requireSigninToViewContentsDescription3: "這些限制可能不適用於被聯邦發送至遠端伺服器的內容。"
makeNotesFollowersOnlyBefore: "讓過去的貼文僅對追隨者顯示"
makeNotesFollowersOnlyBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對追隨者顯示。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
makeNotesHiddenBefore: "隱藏過去的貼文"
makeNotesHiddenBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對自己顯示(私密化)。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
mayNotEffectForFederatedNotes: "聯邦發送至遠端伺服器的貼文可能會不受影響。"
notesHavePassedSpecifiedPeriod: "早於指定時間的貼文"
notesOlderThanSpecifiedDateAndTime: "指定時間和日期之前的貼文"
_abuseUserReport:
forward: "轉發"
forwardDescription: "以匿名系統帳戶將檢舉轉發至遠端伺服器。"
resolve: "解決"
accept: "接受"
reject: "拒絕"
resolveTutorial: "如果您已回覆正當的檢舉,請選擇「接受」以將案件標記為已解決。\n 如果檢舉的內容不正當,請選擇「拒絕」將案件標記為已解決。"
_delivery: _delivery:
status: "傳送狀態" status: "傳送狀態"
stop: "停止發送" stop: "停止發送"
@ -1422,6 +1455,7 @@ _serverSettings:
reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是Redis 記憶體使用量會增加。" reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是Redis 記憶體使用量會增加。"
inquiryUrl: "聯絡表單網址" inquiryUrl: "聯絡表單網址"
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。" inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam如果一段期間內沒有偵測到審查員的活動此設定將自動關閉。"
_accountMigration: _accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶" moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名" moveFromSub: "為另一個帳戶建立別名"
@ -1435,7 +1469,7 @@ _accountMigration:
startMigration: "遷移" startMigration: "遷移"
migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外請確認在要遷移到的帳戶已經建立了一個別名。" migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外請確認在要遷移到的帳戶已經建立了一個別名。"
movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。" movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。"
postMigrationNote: "在完成遷移的 24 小時後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為 0。由於不會解除追隨者你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。" postMigrationNote: "取消追蹤此帳戶將在遷移操作後 24 小時執行。\n 此帳戶有 0 個關注者/關注者。 您的關注者仍然可以看到此帳戶的關注者帖子,因為您不會被取消關注。"
movedTo: "要遷移到的帳戶:" movedTo: "要遷移到的帳戶:"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
@ -1555,7 +1589,7 @@ _achievements:
_markedAsCat: _markedAsCat:
title: "我是貓" title: "我是貓"
description: "已將帳戶設定為貓" description: "已將帳戶設定為貓"
flavor: "沒有名字。" flavor: "沒有名字。"
_following1: _following1:
title: "首次追隨" title: "首次追隨"
description: "首次追隨了" description: "首次追隨了"
@ -1569,7 +1603,7 @@ _achievements:
title: "一百位朋友" title: "一百位朋友"
description: "追隨超過100人了" description: "追隨超過100人了"
_following300: _following300:
title: "朋友多" title: "朋友多"
description: "追隨超過300人了" description: "追隨超過300人了"
_followers1: _followers1:
title: "第一個追隨者" title: "第一個追隨者"
@ -1895,7 +1929,7 @@ _channel:
following: "追隨中" following: "追隨中"
usersCount: "有 {n} 人參與" usersCount: "有 {n} 人參與"
notesCount: "有 {n} 篇貼文" notesCount: "有 {n} 篇貼文"
nameAndDescription: "名稱與說明" nameAndDescription: "名稱"
nameOnly: "僅名稱" nameOnly: "僅名稱"
allowRenoteToExternal: "允許在頻道外轉發和引用" allowRenoteToExternal: "允許在頻道外轉發和引用"
_menuDisplay: _menuDisplay:
@ -2141,8 +2175,11 @@ _auth:
permissionAsk: "此應用程式需要以下權限" permissionAsk: "此應用程式需要以下權限"
pleaseGoBack: "請返回至應用程式" pleaseGoBack: "請返回至應用程式"
callback: "回到應用程式" callback: "回到應用程式"
accepted: "已授予存取權限"
denied: "拒絕訪問" denied: "拒絕訪問"
scopeUser: "以下列使用者身分操作"
pleaseLogin: "必須登入以提供應用程式的存取權限。" pleaseLogin: "必須登入以提供應用程式的存取權限。"
byClickingYouWillBeRedirectedToThisUrl: "如果授予存取權限,就會自動導向到以下的網址"
_antennaSources: _antennaSources:
all: "全部貼文" all: "全部貼文"
homeTimeline: "來自已追隨使用者的貼文" homeTimeline: "來自已追隨使用者的貼文"
@ -2400,7 +2437,7 @@ _notification:
follow: "追隨中" follow: "追隨中"
mention: "提及" mention: "提及"
reply: "回覆" reply: "回覆"
renote: "轉發貼文" renote: "轉發"
quote: "引用" quote: "引用"
reaction: "反應" reaction: "反應"
pollEnded: "問卷調查結束" pollEnded: "問卷調查結束"
@ -2476,6 +2513,8 @@ _webhookSettings:
abuseReport: "當使用者檢舉時" abuseReport: "當使用者檢舉時"
abuseReportResolved: "當處理了使用者的檢舉時" abuseReportResolved: "當處理了使用者的檢舉時"
userCreated: "使用者被新增時" userCreated: "使用者被新增時"
inactiveModeratorsWarning: "當審查員在一段時間內沒有活動時"
inactiveModeratorsInvitationOnlyChanged: "當審查員在一段時間內不活動時,系統會將模式變更為邀請制"
deleteConfirm: "請問是否要刪除 Webhook" deleteConfirm: "請問是否要刪除 Webhook"
testRemarks: "按下切換開關右側的按鈕,就會將假資料發送至 Webhook。" testRemarks: "按下切換開關右側的按鈕,就會將假資料發送至 Webhook。"
_abuseReport: _abuseReport:
@ -2490,7 +2529,7 @@ _abuseReport:
mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)" mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)"
webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)" webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)"
keywords: "關鍵字" keywords: "關鍵字"
notifiedUser: "通知的使用者" notifiedUser: "通知的使用者"
notifiedWebhook: "使用的 Webhook" notifiedWebhook: "使用的 Webhook"
deleteConfirm: "確定要刪除通知對象嗎?" deleteConfirm: "確定要刪除通知對象嗎?"
_moderationLogTypes: _moderationLogTypes:
@ -2521,6 +2560,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "標記為敏感檔案" markSensitiveDriveFile: "標記為敏感檔案"
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案" unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
resolveAbuseReport: "解決檢舉" resolveAbuseReport: "解決檢舉"
forwardAbuseReport: "轉發檢舉"
updateAbuseReportNote: "更新檢舉的審查備註"
createInvitation: "建立邀請碼" createInvitation: "建立邀請碼"
createAd: "建立廣告" createAd: "建立廣告"
deleteAd: "刪除廣告" deleteAd: "刪除廣告"
@ -2690,3 +2731,9 @@ _embedCodeGen:
generateCode: "建立嵌入程式碼" generateCode: "建立嵌入程式碼"
codeGenerated: "已產生程式碼" codeGenerated: "已產生程式碼"
codeGeneratedDescription: "請將產生的程式碼貼到您的網站上。" codeGeneratedDescription: "請將產生的程式碼貼到您的網站上。"
_selfXssPrevention:
warning: "警告"
title: "「在此畫面貼上一些內容」完全是個騙局。"
description1: "如果您在此處貼上任何內容,惡意使用者可能會接管您的帳戶或竊取您的個人資訊。"
description2: "如果您不確切知道要貼上的內容,%c 請立即停止工作並關閉此視窗。"
description3: "細節請看這裡。{link}"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.10.1-beta.1", "version": "2024.11.0-alpha.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -11,7 +11,7 @@ export default [
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
parser: tsParser, parser: tsParser,
project: ['./tsconfig.json', './test/tsconfig.json'], project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'],
sourceType: 'module', sourceType: 'module',
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },

View file

@ -0,0 +1,13 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
const base = require('./jest.config.cjs');
module.exports = {
...base,
testMatch: [
'<rootDir>/test-federation/test/**/*.test.ts',
],
};

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class EnableStatsForFederatedInstances1727318020265 {
name = 'EnableStatsForFederatedInstances1727318020265'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableStatsForFederatedInstances" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableStatsForFederatedInstances"`);
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Testcaptcha1728550878802 {
name = 'Testcaptcha1728550878802'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
}
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ProhibitedWordsForNameOfUser1728634286056 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`);
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SigninRequiredForShowContents1729333924409 {
name = 'SigninRequiredForShowContents1729333924409'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "requireSigninToViewContents" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "requireSigninToViewContents"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MakeNotesHiddenBefore1729486255072 {
name = 'MakeNotesHiddenBefore1729486255072'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesFollowersOnlyBefore" integer`);
await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesHiddenBefore" integer`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesHiddenBefore"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesFollowersOnlyBefore"`);
}
}

View file

@ -19,16 +19,18 @@
"watch": "node ./scripts/watch.mjs", "watch": "node ./scripts/watch.mjs",
"restart": "pnpm build && pnpm start", "restart": "pnpm build && pnpm start",
"dev": "node ./scripts/dev.mjs", "dev": "node ./scripts/dev.mjs",
"typecheck": "tsc --noEmit && tsc -p test --noEmit", "typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"", "eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs", "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs", "jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs", "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs", "jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest", "test": "pnpm jest",
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test:fed": "pnpm jest:fed",
"test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
"generate-api-json": "node ./scripts/generate_api_json.js" "generate-api-json": "node ./scripts/generate_api_json.js"

View file

@ -5,11 +5,52 @@
import Redis from 'ioredis'; import Redis from 'ioredis';
import { loadConfig } from '../built/config.js'; import { loadConfig } from '../built/config.js';
import { createPostgresDataSource } from '../built/postgres.js';
const config = loadConfig(); const config = loadConfig();
const redis = new Redis(config.redis);
redis.on('connect', () => redis.disconnect()); async function connectToPostgres() {
redis.on('error', (e) => { const source = createPostgresDataSource(config);
throw e; await source.initialize();
await source.destroy();
}
async function connectToRedis(redisOptions) {
return await new Promise(async (resolve, reject) => {
const redis = new Redis({
...redisOptions,
lazyConnect: true,
reconnectOnError: false,
showFriendlyErrorStack: true,
}); });
redis.on('error', e => reject(e));
try {
await redis.connect();
resolve();
} catch (e) {
reject(e);
} finally {
redis.disconnect(false);
}
});
}
// If not all of these are defined, the default one gets reused.
// so we use a Set to only try connecting once to each **uniq** redis.
const promises = Array
.from(new Set([
config.redis,
config.redisForPubsub,
config.redisForJobQueue,
config.redisForTimelines,
config.redisForReactions,
]))
.map(connectToRedis)
.concat([
connectToPostgres()
]);
await Promise.allSettled(promises);

View file

@ -61,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return; return;
} }
const moderatorIds = await this.roleService.getModeratorIds(true, true); const moderatorIds = await this.roleService.getModeratorIds({
includeAdmins: true,
excludeExpire: true,
});
for (const moderatorId of moderatorIds) { for (const moderatorId of moderatorIds) {
for (const abuseReport of abuseReports) { for (const abuseReport of abuseReports) {
@ -285,8 +288,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.log(updater, 'createAbuseReportNotificationRecipient', { .log(updater, 'createAbuseReportNotificationRecipient', {
recipientId: id, recipientId: id,
recipient: created, recipient: created,
}) });
.then();
return created; return created;
} }
@ -324,8 +326,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
recipientId: params.id, recipientId: params.id,
before: beforeEntity, before: beforeEntity,
after: afterEntity, after: afterEntity,
}) });
.then();
return afterEntity; return afterEntity;
} }
@ -346,8 +347,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.log(updater, 'deleteAbuseReportNotificationRecipient', { .log(updater, 'deleteAbuseReportNotificationRecipient', {
recipientId: id, recipientId: id,
recipient: entity, recipient: entity,
}) });
.then();
} }
/** /**
@ -370,7 +370,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
} }
// モデレータ権限の有無で通知先設定を振り分ける // モデレータ権限の有無で通知先設定を振り分ける
const authorizedUserIds = await this.roleService.getModeratorIds(true, true); const authorizedUserIds = await this.roleService.getModeratorIds({
includeAdmins: true,
excludeExpire: true,
});
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>(); const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>(); const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
for (const recipient of userRecipients) { for (const recipient of userRecipients) {

View file

@ -110,8 +110,7 @@ export class AbuseReportService {
reportId: report.id, reportId: report.id,
report: report, report: report,
resolvedAs: ps.resolvedAs, resolvedAs: ps.resolvedAs,
}) });
.then();
} }
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
@ -148,8 +147,7 @@ export class AbuseReportService {
.log(moderator, 'forwardAbuseReport', { .log(moderator, 'forwardAbuseReport', {
reportId: report.id, reportId: report.id,
report: report, report: report,
}) });
.then();
} }
@bindThis @bindThis

View file

@ -274,14 +274,16 @@ export class AccountMoveService {
} }
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account. // Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
if (this.meta.enableStatsForFederatedInstances) {
if (this.userEntityService.isRemoteUser(oldAccount)) { if (this.userEntityService.isRemoteUser(oldAccount)) {
this.federatedInstanceService.fetch(oldAccount.host).then(async i => { this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false); this.instanceChart.updateFollowers(i.host, false);
} }
}); });
} }
}
// FIXME: expensive? // FIXME: expensive?
for (const followerId of localFollowerIds) { for (const followerId of localFollowerIds) {

View file

@ -209,6 +209,13 @@ export class AnnouncementService {
return; return;
} }
const announcement = await this.announcementsRepository.findOneBy({ id: announcementId });
if (announcement != null && announcement.userId === user.id) {
await this.announcementsRepository.update(announcementId, {
isActive: false,
});
}
if ((await this.getUnreadAnnouncements(user)).length === 0) { if ((await this.getUnreadAnnouncements(user)).length === 0) {
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements'); this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
} }

View file

@ -119,5 +119,18 @@ export class CaptchaService {
throw new Error(`turnstile-failed: ${errorCodes}`); throw new Error(`turnstile-failed: ${errorCodes}`);
} }
} }
@bindThis
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
if (response == null) {
throw new Error('testcaptcha-failed: no response provided');
}
const success = response === 'testcaptcha-passed';
if (!success) {
throw new Error('testcaptcha-failed');
}
}
} }

View file

@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async update(id: MiEmoji['id'], data: { public async update(data: (
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
) & {
driveFile?: MiDriveFile; driveFile?: MiDriveFile;
name?: string;
category?: string | null; category?: string | null;
aliases?: string[]; aliases?: string[];
license?: string | null; license?: string | null;
isSensitive?: boolean; isSensitive?: boolean;
localOnly?: boolean; localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}, moderator?: MiUser): Promise<void> { }, moderator?: MiUser): Promise<
const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); null
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); | 'NO_SUCH_EMOJI'
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); | 'SAME_NAME_EMOJI_EXISTS'
> {
const emoji = data.id
? await this.getEmojiById(data.id)
: await this.getEmojiByName(data.name!);
if (emoji === null) return 'NO_SUCH_EMOJI';
const id = emoji.id;
// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
if (doNameUpdate) {
const isDuplicate = await this.checkDuplicate(data.name!);
if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
}
await this.emojisRepository.update(emoji.id, { await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(), updatedAt: new Date(),
@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
const packed = await this.emojiEntityService.packDetailed(emoji.id); const packed = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) { if (!doNameUpdate) {
this.globalEventService.publishBroadcastStream('emojiUpdated', { this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [packed], emojis: [packed],
}); });
@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
after: updated, after: updated,
}); });
} }
return null;
} }
@bindThis @bindThis

View file

@ -47,7 +47,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async fetch(host: string): Promise<MiInstance> { public async fetchOrRegister(host: string): Promise<MiInstance> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = await this.federatedInstanceCache.get(host); const cached = await this.federatedInstanceCache.get(host);
@ -70,6 +70,24 @@ export class FederatedInstanceService implements OnApplicationShutdown {
} }
} }
@bindThis
public async fetch(host: string): Promise<MiInstance | null> {
host = this.utilityService.toPuny(host);
const cached = await this.federatedInstanceCache.get(host);
if (cached !== undefined) return cached;
const index = await this.instancesRepository.findOneBy({ host });
if (index == null) {
this.federatedInstanceCache.set(host, null);
return null;
} else {
this.federatedInstanceCache.set(host, index);
return index;
}
}
@bindThis @bindThis
public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> { public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> {
const result = await this.instancesRepository.createQueryBuilder().update() const result = await this.instancesRepository.createQueryBuilder().update()

View file

@ -82,7 +82,7 @@ export class FetchInstanceMetadataService {
try { try {
if (!force) { if (!force) {
const _instance = await this.federatedInstanceService.fetch(host); const _instance = await this.federatedInstanceService.fetchOrRegister(host);
const now = Date.now(); const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
// unlock at the finally caluse // unlock at the finally caluse

View file

@ -406,8 +406,10 @@ export class MfmService {
mention: (node) => { mention: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.setAttribute('href', remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
a.className = 'u-url mention'; a.className = 'u-url mention';
a.textContent = acct; a.textContent = acct;
return a; return a;

View file

@ -511,14 +511,16 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
// Register host // Register host
if (this.meta.enableStatsForFederatedInstances) {
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => { this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
this.updateNotesCountQueue.enqueue(i.id, 1); this.updateNotesCountQueue.enqueue(i.id, 1);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true); this.instanceChart.updateNote(i.host, note, true);
} }
}); });
} }
}
// ハッシュタグ更新 // ハッシュタグ更新
if (data.visibility === 'public' || data.visibility === 'home') { if (data.visibility === 'public' || data.visibility === 'home') {

View file

@ -106,8 +106,9 @@ export class NoteDeleteService {
this.perUserNotesChart.update(user, note, false); this.perUserNotesChart.update(user, note, false);
} }
if (this.meta.enableStatsForFederatedInstances) {
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => { this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false); this.instanceChart.updateNote(i.host, note, false);
@ -115,6 +116,7 @@ export class NoteDeleteService {
}); });
} }
} }
}
for (const cascadingNote of cascadingNotes) { for (const cascadingNote of cascadingNotes) {
this.searchService.unindexNote(cascadingNote); this.searchService.unindexNote(cascadingNote);

View file

@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -35,6 +35,7 @@ import type {
} from './QueueModule.js'; } from './QueueModule.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import { type UserWebhookPayload } from './UserWebhookService.js';
@Injectable() @Injectable()
export class QueueService { export class QueueService {
@ -93,6 +94,13 @@ export class QueueService {
repeat: { pattern: '0 0 * * *' }, repeat: { pattern: '0 0 * * *' },
removeOnComplete: true, removeOnComplete: true,
}); });
this.systemQueue.add('checkModeratorsActivity', {
}, {
// 毎時30分に起動
repeat: { pattern: '30 * * * *' },
removeOnComplete: true,
});
} }
@bindThis @bindThis
@ -461,10 +469,10 @@ export class QueueService {
* @see UserWebhookDeliverProcessorService * @see UserWebhookDeliverProcessorService
*/ */
@bindThis @bindThis
public userWebhookDeliver( public userWebhookDeliver<T extends WebhookEventTypes>(
webhook: MiWebhook, webhook: MiWebhook,
type: typeof webhookEventTypes[number], type: T,
content: unknown, content: UserWebhookPayload<T>,
opts?: { attempts?: number }, opts?: { attempts?: number },
) { ) {
const data: UserWebhookDeliverJobData = { const data: UserWebhookDeliverJobData = {

View file

@ -101,6 +101,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit { export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>; private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>; private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService; private notificationService: NotificationService;
@ -136,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
) { ) {
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
@ -423,22 +425,35 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return check.isExplorable; return check.isExplorable;
} }
/**
* ID一覧を取得する.
*
* @param opts.includeAdmins (デフォルト: true)
* @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
* @param opts.excludeExpire (デフォルト: false)
*/
@bindThis @bindThis
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> { public async getModeratorIds(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser['id'][]> {
const includeAdmins = opts?.includeAdmins ?? true;
const includeRoot = opts?.includeRoot ?? false;
const excludeExpire = opts?.excludeExpire ?? false;
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins const moderatorRoles = includeAdmins
? roles.filter(r => r.isModerator || r.isAdministrator) ? roles.filter(r => r.isModerator || r.isAdministrator)
: roles.filter(r => r.isModerator); : roles.filter(r => r.isModerator);
// TODO: isRootなアカウントも含める
const assigns = moderatorRoles.length > 0 const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: []; : [];
const now = Date.now();
const result = [
// Setを経由して重複を除去ユーザIDは重複する可能性があるので // Setを経由して重複を除去ユーザIDは重複する可能性があるので
...new Set( const now = Date.now();
const resultSet = new Set(
assigns assigns
.filter(it => .filter(it =>
(excludeExpire) (excludeExpire)
@ -446,19 +461,35 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
: true, : true,
) )
.map(a => a.userId), .map(a => a.userId),
), );
];
return result.sort((x, y) => x.localeCompare(y)); if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
}
return [...resultSet].sort((x, y) => x.localeCompare(y));
} }
@bindThis @bindThis
public async getModerators(includeAdmins = true): Promise<MiUser[]> { public async getModerators(opts?: {
const ids = await this.getModeratorIds(includeAdmins); includeAdmins?: boolean,
const users = ids.length > 0 ? await this.usersRepository.findBy({ includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser[]> {
const ids = await this.getModeratorIds(opts);
return ids.length > 0
? await this.usersRepository.findBy({
id: In(ids), id: In(ids),
}) : []; })
return users; : [];
} }
@bindThis @bindThis

View file

@ -150,8 +150,8 @@ export class SignupService {
})); }));
}); });
this.usersChart.update(account, true).then(); this.usersChart.update(account, true);
this.userService.notifySystemWebhook(account, 'userCreated').then(); this.userService.notifySystemWebhook(account, 'userCreated');
return { account, secret }; return { account, secret };
} }

View file

@ -101,8 +101,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
.log(updater, 'createSystemWebhook', { .log(updater, 'createSystemWebhook', {
systemWebhookId: webhook.id, systemWebhookId: webhook.id,
webhook: webhook, webhook: webhook,
}) });
.then();
return webhook; return webhook;
} }
@ -139,8 +138,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
systemWebhookId: beforeEntity.id, systemWebhookId: beforeEntity.id,
before: beforeEntity, before: beforeEntity,
after: afterEntity, after: afterEntity,
}) });
.then();
return afterEntity; return afterEntity;
} }
@ -158,8 +156,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
.log(updater, 'deleteSystemWebhook', { .log(updater, 'deleteSystemWebhook', {
systemWebhookId: webhook.id, systemWebhookId: webhook.id,
webhook, webhook,
}) });
.then();
} }
/** /**

View file

@ -305,21 +305,23 @@ export class UserFollowingService implements OnModuleInit {
//#endregion //#endregion
//#region Update instance stats //#region Update instance stats
if (this.meta.enableStatsForFederatedInstances) {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => { this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true); this.instanceChart.updateFollowing(i.host, true);
} }
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => { this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true); this.instanceChart.updateFollowers(i.host, true);
} }
}); });
} }
}
//#endregion //#endregion
this.perUserFollowingChart.update(follower, followee, true); this.perUserFollowingChart.update(follower, followee, true);
@ -437,21 +439,23 @@ export class UserFollowingService implements OnModuleInit {
//#endregion //#endregion
//#region Update instance stats //#region Update instance stats
if (this.meta.enableStatsForFederatedInstances) {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => { this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false); this.instanceChart.updateFollowing(i.host, false);
} }
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => { this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false); this.instanceChart.updateFollowers(i.host, false);
} }
}); });
} }
}
//#endregion //#endregion
this.perUserFollowingChart.update(follower, followee, false); this.perUserFollowingChart.update(follower, followee, false);

View file

@ -6,11 +6,23 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { type WebhooksRepository } from '@/models/_.js'; import { type WebhooksRepository } from '@/models/_.js';
import { MiWebhook } from '@/models/Webhook.js'; import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' |'mention' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {
user: Packed<'UserDetailedNotMe'>,
} :
T extends 'followed' ? {
user: Packed<'UserLite'>,
} : never;
@Injectable() @Injectable()
export class UserWebhookService implements OnApplicationShutdown { export class UserWebhookService implements OnApplicationShutdown {

View file

@ -10,8 +10,9 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js'; import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
const oneDayMillis = 24 * 60 * 60 * 1000; const oneDayMillis = 24 * 60 * 60 * 1000;
@ -82,6 +83,9 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isExplorable: true, isExplorable: true,
isHibernated: false, isHibernated: false,
isDeleted: false, isDeleted: false,
requireSigninToViewContents: false,
makeNotesFollowersOnlyBefore: null,
makeNotesHiddenBefore: null,
emojis: [], emojis: [],
score: 0, score: 0,
host: null, host: null,
@ -302,10 +306,10 @@ export class WebhookTestService {
* - on * - on
*/ */
@bindThis @bindThis
public async testUserWebhook( public async testUserWebhook<T extends WebhookEventTypes>(
params: { params: {
webhookId: MiWebhook['id'], webhookId: MiWebhook['id'],
type: WebhookEventTypes, type: T,
override?: Partial<Omit<MiWebhook, 'id'>>, override?: Partial<Omit<MiWebhook, 'id'>>,
}, },
sender: MiUser | null, sender: MiUser | null,
@ -317,7 +321,7 @@ export class WebhookTestService {
} }
const webhook = webhooks[0]; const webhook = webhooks[0];
const send = (contents: unknown) => { const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = { const merged = {
...webhook, ...webhook,
...params.override, ...params.override,
@ -325,7 +329,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図. // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ. // また、Jobの試行回数も1回だけ.
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 }); this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
}; };
const dummyNote1 = generateDummyNote({ const dummyNote1 = generateDummyNote({
@ -357,33 +361,40 @@ export class WebhookTestService {
switch (params.type) { switch (params.type) {
case 'note': { case 'note': {
send(toPackedNote(dummyNote1)); send('note', { note: toPackedNote(dummyNote1) });
break; break;
} }
case 'reply': { case 'reply': {
send(toPackedNote(dummyReply1)); send('reply', { note: toPackedNote(dummyReply1) });
break; break;
} }
case 'renote': { case 'renote': {
send(toPackedNote(dummyRenote1)); send('renote', { note: toPackedNote(dummyRenote1) });
break; break;
} }
case 'mention': { case 'mention': {
send(toPackedNote(dummyMention1)); send('mention', { note: toPackedNote(dummyMention1) });
break; break;
} }
case 'follow': { case 'follow': {
send(toPackedUserDetailedNotMe(dummyUser1)); send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break; break;
} }
case 'followed': { case 'followed': {
send(toPackedUserLite(dummyUser2)); send('followed', { user: toPackedUserLite(dummyUser2) });
break; break;
} }
case 'unfollow': { case 'unfollow': {
send(toPackedUserDetailedNotMe(dummyUser3)); send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
break; break;
} }
// まだ実装されていない (#9485)
case 'reaction': return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
} }
} }
@ -446,6 +457,22 @@ export class WebhookTestService {
send(toPackedUserLite(dummyUser1)); send(toPackedUserLite(dummyUser1));
break; break;
} }
case 'inactiveModeratorsWarning': {
const dummyTime: ModeratorInactivityRemainingTime = {
time: 100000,
asDays: 1,
asHours: 24,
};
send({
remainingTime: dummyTime,
});
break;
}
case 'inactiveModeratorsInvitationOnlyChanged': {
send({});
break;
}
} }
} }
} }

View file

@ -495,6 +495,9 @@ export class ApRendererService {
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description, _misskey_summary: profile.description,
_misskey_followedMessage: profile.followedMessage, _misskey_followedMessage: profile.followedMessage,
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
icon: avatar ? this.renderImage(avatar) : null, icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner) : null,
tag, tag,

View file

@ -555,6 +555,9 @@ const extension_context_definition = {
'_misskey_votes': 'misskey:_misskey_votes', '_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary', '_misskey_summary': 'misskey:_misskey_summary',
'_misskey_followedMessage': 'misskey:_misskey_followedMessage', '_misskey_followedMessage': 'misskey:_misskey_followedMessage',
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
'_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore',
'_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore',
'isCat': 'misskey:isCat', 'isCat': 'misskey:isCat',
// vcard // vcard
vcard: 'http://www.w3.org/2006/vcard/ns#', vcard: 'http://www.w3.org/2006/vcard/ns#',

View file

@ -232,6 +232,12 @@ export class ApPersonService implements OnModuleInit {
if (user == null) throw new Error('failed to create user: user is null'); if (user == null) throw new Error('failed to create user: user is null');
const [avatar, banner] = await Promise.all([icon, image].map(img => { const [avatar, banner] = await Promise.all([icon, image].map(img => {
// icon and image may be arrays
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
if (Array.isArray(img)) {
img = img.find(item => item && item.url) ?? null;
}
// if we have an explicitly missing image, return an // if we have an explicitly missing image, return an
// explicitly-null set of values // explicitly-null set of values
if ((img == null) || (typeof img === 'object' && img.url == null)) { if ((img == null) || (typeof img === 'object' && img.url == null)) {
@ -356,6 +362,9 @@ export class ApPersonService implements OnModuleInit {
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null,
makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null,
emojis, emojis,
})) as MiRemoteUser; })) as MiRemoteUser;
@ -408,13 +417,15 @@ export class ApPersonService implements OnModuleInit {
this.cacheService.uriPersonCache.set(user.uri, user); this.cacheService.uriPersonCache.set(user.uri, user);
// Register host // Register host
this.federatedInstanceService.fetch(host).then(i => { if (this.meta.enableStatsForFederatedInstances) {
this.federatedInstanceService.fetchOrRegister(host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host); this.instanceChart.newUser(i.host);
} }
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
}); });
}
this.usersChart.update(user, true); this.usersChart.update(user, true);

View file

@ -14,6 +14,9 @@ export interface IObject {
summary?: string; summary?: string;
_misskey_summary?: string; _misskey_summary?: string;
_misskey_followedMessage?: string | null; _misskey_followedMessage?: string | null;
_misskey_requireSigninToViewContents?: boolean;
_misskey_makeNotesFollowersOnlyBefore?: number | null;
_misskey_makeNotesHiddenBefore?: number | null;
published?: string; published?: string;
cc?: ApObject; cc?: ApObject;
to?: ApObject; to?: ApObject;

View file

@ -96,6 +96,7 @@ export class MetaEntityService {
recaptchaSiteKey: instance.recaptchaSiteKey, recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile, enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey, turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha,
swPublickey: instance.swPublicKey, swPublickey: instance.swPublicKey,
themeColor: instance.themeColor, themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',

View file

@ -22,6 +22,30 @@ import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
// is-renote.tsとよしなにリンク
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
return (
note.renote != null &&
note.reply == null &&
note.text == null &&
note.cw == null &&
(note.fileIds == null || note.fileIds.length === 0) &&
!note.hasPoll
);
}
function getAppearNoteIds(notes: MiNote[]): Set<string> {
const appearNoteIds = new Set<string>();
for (const note of notes) {
if (isPureRenote(note)) {
appearNoteIds.add(note.renoteId);
} else {
appearNoteIds.add(note.id);
}
}
return appearNoteIds;
}
@Injectable() @Injectable()
export class NoteEntityService implements OnModuleInit { export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
@ -78,34 +102,62 @@ export class NoteEntityService implements OnModuleInit {
} }
@bindThis @bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) { private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
// FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
if ((followersOnlyBefore != null)
&& (
(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
)
) {
packedNote.visibility = 'followers';
}
}
if (meId === packedNote.userId) return;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false; let hide = false;
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
}
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if ((hiddenBefore != null)
&& (
(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
)
) {
hide = true;
}
}
// visibility が specified かつ自分が指定されていなかったら非表示 // visibility が specified かつ自分が指定されていなかったら非表示
if (!hide) {
if (packedNote.visibility === 'specified') { if (packedNote.visibility === 'specified') {
if (meId == null) { if (meId == null) {
hide = true; hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else { } else {
// 指定されているかどうか // 指定されているかどうか
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id); const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (specified) { if (!specified) {
hide = false;
} else {
hide = true; hide = true;
} }
} }
} }
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (!hide) {
if (packedNote.visibility === 'followers') { if (packedNote.visibility === 'followers') {
if (meId == null) { if (meId == null) {
hide = true; hide = true;
} else if (meId === packedNote.userId) {
hide = false;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) { } else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ // 自分の投稿に対するリプライ
hide = false; hide = false;
@ -114,6 +166,7 @@ export class NoteEntityService implements OnModuleInit {
hide = false; hide = false;
} else { } else {
// フォロワーかどうか // フォロワーかどうか
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
const isFollowing = await this.followingsRepository.exists({ const isFollowing = await this.followingsRepository.exists({
where: { where: {
followeeId: packedNote.userId, followeeId: packedNote.userId,
@ -124,6 +177,7 @@ export class NoteEntityService implements OnModuleInit {
hide = !isFollowing; hide = !isFollowing;
} }
} }
}
if (hide) { if (hide) {
packedNote.visibleUserIds = undefined; packedNote.visibleUserIds = undefined;
@ -133,6 +187,7 @@ export class NoteEntityService implements OnModuleInit {
packedNote.poll = undefined; packedNote.poll = undefined;
packedNote.cw = null; packedNote.cw = null;
packedNote.isHidden = true; packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
} }
} }
@ -227,7 +282,7 @@ export class NoteEntityService implements OnModuleInit {
return true; return true;
} else { } else {
// 指定されているかどうか // 指定されているかどうか
return note.visibleUserIds.some((id: any) => meId === id); return note.visibleUserIds.some(id => meId === id);
} }
} }
@ -421,7 +476,7 @@ export class NoteEntityService implements OnModuleInit {
) { ) {
if (notes.length === 0) return []; if (notes.length === 0) return [];
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null; const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
const meId = me ? me.id : null; const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], string | null>(); const myReactionsMap = new Map<MiNote['id'], string | null>();
@ -432,7 +487,7 @@ export class NoteEntityService implements OnModuleInit {
const oldId = this.idService.gen(Date.now() - 2000); const oldId = this.idService.gen(Date.now() - 2000);
for (const note of notes) { for (const note of notes) {
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote if (isPureRenote(note)) {
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) { if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null); myReactionsMap.set(note.renote.id, null);

View file

@ -490,6 +490,9 @@ export class UserEntityService implements OnModuleInit {
}))) : [], }))) : [],
isBot: user.isBot, isBot: user.isBot,
isCat: user.isCat, isCat: user.isCat,
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name, name: instance.name,
softwareName: instance.softwareName, softwareName: instance.softwareName,

View file

@ -6,6 +6,8 @@
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
// NoteEntityService.isPureRenote とよしなにリンク
type Renote = type Renote =
MiNote & { MiNote & {
renoteId: NonNullable<MiNote['renoteId']> renoteId: NonNullable<MiNote['renoteId']>

View file

@ -4,5 +4,5 @@
*/ */
export function sqlLikeEscape(s: string) { export function sqlLikeEscape(s: string) {
return s.replace(/([%_])/g, '\\$1'); return s.replace(/([\\%_])/g, '\\$1');
} }

View file

@ -81,6 +81,11 @@ export class MiMeta {
}) })
public prohibitedWords: string[]; public prohibitedWords: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public prohibitedWordsForNameOfUser: string[];
@Column('varchar', { @Column('varchar', {
length: 1024, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })
@ -258,6 +263,11 @@ export class MiMeta {
}) })
public turnstileSecretKey: string | null; public turnstileSecretKey: string | null;
@Column('boolean', {
default: false,
})
public enableTestcaptcha: boolean;
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること // chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
@Column('enum', { @Column('enum', {
@ -519,6 +529,11 @@ export class MiMeta {
}) })
public enableChartsForFederatedInstances: boolean; public enableChartsForFederatedInstances: boolean;
@Column('boolean', {
default: true,
})
public enableStatsForFederatedInstances: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -14,6 +14,10 @@ export const systemWebhookEventTypes = [
'abuseReportResolved', 'abuseReportResolved',
// ユーザが作成された時 // ユーザが作成された時
'userCreated', 'userCreated',
// モデレータが一定期間不在である警告
'inactiveModeratorsWarning',
// モデレータが一定期間不在のためシステムにより招待制へと変更された
'inactiveModeratorsInvitationOnlyChanged',
] as const; ] as const;
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number]; export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];

View file

@ -202,6 +202,23 @@ export class MiUser {
}) })
public isHibernated: boolean; public isHibernated: boolean;
@Column('boolean', {
default: false,
})
public requireSigninToViewContents: boolean;
// in sec, マイナスで相対時間
@Column('integer', {
nullable: true,
})
public makeNotesFollowersOnlyBefore: number | null;
// in sec, マイナスで相対時間
@Column('integer', {
nullable: true,
})
public makeNotesHiddenBefore: number | null;
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', { @Column('boolean', {
default: false, default: false,

View file

@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
enableTestcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
swPublickey: { swPublickey: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,

View file

@ -115,6 +115,18 @@ export const packedUserLiteSchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: true, nullable: false, optional: true,
}, },
requireSigninToViewContents: {
type: 'boolean',
nullable: false, optional: true,
},
makeNotesFollowersOnlyBefore: {
type: 'number',
nullable: true, optional: true,
},
makeNotesHiddenBefore: {
type: 'number',
nullable: true, optional: true,
},
instance: { instance: {
type: 'object', type: 'object',
nullable: false, optional: true, nullable: false, optional: true,

View file

@ -6,6 +6,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js'; import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
@ -80,6 +81,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeliverProcessorService, DeliverProcessorService,
InboxProcessorService, InboxProcessorService,
AggregateRetentionProcessorService, AggregateRetentionProcessorService,
CheckExpiredMutingsProcessorService,
CheckModeratorsActivityProcessorService,
QueueProcessorService, QueueProcessorService,
], ],
exports: [ exports: [

View file

@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
@ -66,7 +67,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string {
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
const currentAttempts = job.attemptsMade + (increment ? 1 : 0); const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
const maxAttempts = job.opts ? job.opts.attempts : 0; const maxAttempts = job.opts.attempts ?? 0;
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
} }
@ -120,24 +121,35 @@ export class QueueProcessorService implements OnApplicationShutdown {
private aggregateRetentionProcessorService: AggregateRetentionProcessorService, private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
private cleanProcessorService: CleanProcessorService, private cleanProcessorService: CleanProcessorService,
) { ) {
this.logger = this.queueLoggerService.logger; this.logger = this.queueLoggerService.logger;
function renderError(e: Error): any { function renderError(e?: Error) {
if (e) { // 何故かeがundefinedで来ることがある // 何故かeがundefinedで来ることがある
if (!e) return '?';
if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
return `${e.name}: ${e.message}`;
}
return { return {
stack: e.stack, stack: e.stack,
message: e.message, message: e.message,
name: e.name, name: e.name,
}; };
} else {
return {
stack: '?',
message: '?',
name: '?',
};
} }
function renderJob(job?: Bull.Job) {
if (!job) return '?';
return {
name: job.name || undefined,
info: getJobInfo(job),
failedReason: job.failedReason || undefined,
data: job.data,
};
} }
//#region system //#region system
@ -150,6 +162,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
case 'clean': return this.cleanProcessorService.process(); case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`); default: throw new Error(`unrecognized job type ${job.name} for system`);
} }
@ -172,15 +185,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`)) .on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => { .on('failed', (job, err: Error) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, { Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -229,15 +242,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`)) .on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, { Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -269,15 +282,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Deliver: ${err.message}`, { Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -309,15 +322,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`)) .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }); logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, { Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -349,15 +362,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, { Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -389,15 +402,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, { Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -436,15 +449,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`)) .on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, { Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion
@ -477,15 +490,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`)) .on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => { .on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) { if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, { Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error', level: 'error',
extra: { job, err }, extra: { job, err },
}); });
} }
}) })
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
} }
//#endregion //#endregion

View file

@ -0,0 +1,292 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js';
import { EmailService } from '@/core/EmailService.js';
import { MiUser, type UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
// モデレーターが不在と判断する日付の閾値
const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
// 警告通知やログ出力を行う残日数の閾値
const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2;
// 期限から6時間ごとに通知を行う
const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6;
const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60;
const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24;
export type ModeratorInactivityEvaluationResult = {
isModeratorsInactive: boolean;
inactiveModerators: MiUser[];
remainingTime: ModeratorInactivityRemainingTime;
}
export type ModeratorInactivityRemainingTime = {
time: number;
asHours: number;
asDays: number;
};
function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) {
const subject = 'Moderator Inactivity Warning / モデレーター不在の通知';
const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`;
const message = [
'To Moderators,',
'',
`A moderator has been inactive for a period of time. If there are ${timeVariant} of inactivity left, it will switch to invitation only.`,
'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.',
'',
'---------------',
'',
'To モデレーター各位',
'',
`モデレーターが一定期間活動していないようです。あと${timeVariantJa}活動していない状態が続くと招待制に切り替わります。`,
'招待制に切り替わることを望まない場合は、Misskeyにログインして最終アクティブ日時を更新してください。',
'',
];
const html = message.join('<br>');
const text = message.join('\n');
return {
subject,
html,
text,
};
}
function generateInvitationOnlyChangedMail() {
const subject = 'Change to Invitation-Only / 招待制に変更されました';
const message = [
'To Moderators,',
'',
`Changed to invitation only because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`,
'To cancel the invitation only, you need to access the control panel.',
'',
'---------------',
'',
'To モデレーター各位',
'',
`モデレーターの活動が${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出されなかったため、招待制に変更されました。`,
'招待制を解除するには、コントロールパネルにアクセスする必要があります。',
'',
];
const html = message.join('<br>');
const text = message.join('\n');
return {
subject,
html,
text,
};
}
@Injectable()
export class CheckModeratorsActivityProcessorService {
private logger: Logger;
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private metaService: MetaService,
private roleService: RoleService,
private emailService: EmailService,
private announcementService: AnnouncementService,
private systemWebhookService: SystemWebhookService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
}
@bindThis
public async process(): Promise<void> {
this.logger.info('start.');
const meta = await this.metaService.fetch(false);
if (!meta.disableRegistration) {
await this.processImpl();
} else {
this.logger.info('is already invitation only.');
}
this.logger.succ('finish.');
}
@bindThis
private async processImpl() {
const evaluateResult = await this.evaluateModeratorsInactiveDays();
if (evaluateResult.isModeratorsInactive) {
this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
await this.changeToInvitationOnly();
await this.notifyChangeToInvitationOnly();
} else {
const remainingTime = evaluateResult.remainingTime;
if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) {
const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`);
if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) {
// ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する
// つまり、のこり2日を切ったら6時間ごとに通知が送られる
await this.notifyInactiveModeratorsWarning(remainingTime);
}
}
}
}
/**
* trueの場合はモデレーターが不在である
* isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に
* {@link MiUser.lastActiveDate}{@link MODERATOR_INACTIVITY_LIMIT_DAYS}
* {@link MiUser.lastActiveDate}nullの場合は
*
* -----
*
* ###
* - 実行日時: 2022-01-30 12:00:00
* - 判定基準: 2022-01-23 12:00:00{@link MODERATOR_INACTIVITY_LIMIT_DAYS}
*
* ####
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00
* - モデレータB: lastActiveDate = 2022-01-23 12:00:00 0
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 -1
* - モデレータD: lastActiveDate = null
*
* Bのアクティビティのみ判定基準日よりも古くないため
*
* ####
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00
* - モデレータB: lastActiveDate = 2022-01-22 12:00:00 -1
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 -1
* - モデレータD: lastActiveDate = null
*
* A, B, Cのアクティビティは判定基準日よりも古いため
*/
@bindThis
public async evaluateModeratorsInactiveDays(): Promise<ModeratorInactivityEvaluationResult> {
const today = new Date();
const inactivePeriod = new Date(today);
inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
const moderators = await this.fetchModerators()
.then(it => it.filter(it => it.lastActiveDate != null));
const inactiveModerators = moderators
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
const remainingTime = newestLastActiveDate.getTime() - inactivePeriod.getTime();
const remainingTimeAsDays = Math.floor(remainingTime / ONE_DAY_MILLI_SEC);
const remainingTimeAsHours = Math.floor((remainingTime / ONE_HOUR_MILLI_SEC));
return {
isModeratorsInactive: inactiveModerators.length === moderators.length,
inactiveModerators,
remainingTime: {
time: remainingTime,
asHours: remainingTimeAsHours,
asDays: remainingTimeAsDays,
},
};
}
@bindThis
private async changeToInvitationOnly() {
await this.metaService.update({ disableRegistration: true });
}
@bindThis
public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) {
// -- モデレータへのメール送信
const moderators = await this.fetchModerators();
const moderatorProfiles = await this.userProfilesRepository
.findBy({ userId: In(moderators.map(it => it.id)) })
.then(it => new Map(it.map(it => [it.userId, it])));
const mail = generateModeratorInactivityMail(remainingTime);
for (const moderator of moderators) {
const profile = moderatorProfiles.get(moderator.id);
if (profile && profile.email && profile.emailVerified) {
this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
}
}
// -- SystemWebhook
const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
.then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning')));
for (const systemWebhook of systemWebhooks) {
this.systemWebhookService.enqueueSystemWebhook(
systemWebhook,
'inactiveModeratorsWarning',
{ remainingTime: remainingTime },
);
}
}
@bindThis
public async notifyChangeToInvitationOnly() {
// -- モデレータへのメールとお知らせ(個人向け)送信
const moderators = await this.fetchModerators();
const moderatorProfiles = await this.userProfilesRepository
.findBy({ userId: In(moderators.map(it => it.id)) })
.then(it => new Map(it.map(it => [it.userId, it])));
const mail = generateInvitationOnlyChangedMail();
for (const moderator of moderators) {
this.announcementService.create({
title: mail.subject,
text: mail.text,
forExistingUsers: true,
needConfirmationToRead: true,
userId: moderator.id,
});
const profile = moderatorProfiles.get(moderator.id);
if (profile && profile.email && profile.emailVerified) {
this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
}
}
// -- SystemWebhook
const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
.then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged')));
for (const systemWebhook of systemWebhooks) {
this.systemWebhookService.enqueueSystemWebhook(
systemWebhook,
'inactiveModeratorsInvitationOnlyChanged',
{},
);
}
}
@bindThis
private async fetchModerators() {
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
return this.roleService.getModerators({
includeAdmins: true,
includeRoot: true,
excludeExpire: true,
});
}
}

View file

@ -74,8 +74,17 @@ export class DeliverProcessorService {
try { try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
// Update stats this.apRequestChart.deliverSucc();
this.federatedInstanceService.fetch(host).then(i => { this.federationChart.deliverd(host, true);
// Update instance stats
process.nextTick(async () => {
const i = await (this.meta.enableStatsForFederatedInstances
? this.federatedInstanceService.fetchOrRegister(host)
: this.federatedInstanceService.fetch(host));
if (i == null) return;
if (i.isNotResponding) { if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, { this.federatedInstanceService.update(i.id, {
isNotResponding: false, isNotResponding: false,
@ -83,9 +92,9 @@ export class DeliverProcessorService {
}); });
} }
if (this.meta.enableStatsForFederatedInstances) {
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
this.apRequestChart.deliverSucc(); }
this.federationChart.deliverd(i.host, true);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true); this.instanceChart.requestSent(i.host, true);
@ -94,8 +103,11 @@ export class DeliverProcessorService {
return 'Success'; return 'Success';
} catch (res) { } catch (res) {
// Update stats this.apRequestChart.deliverFail();
this.federatedInstanceService.fetch(host).then(i => { this.federationChart.deliverd(host, false);
// Update instance stats
this.federatedInstanceService.fetchOrRegister(host).then(i => {
if (!i.isNotResponding) { if (!i.isNotResponding) {
this.federatedInstanceService.update(i.id, { this.federatedInstanceService.update(i.id, {
isNotResponding: true, isNotResponding: true,
@ -116,9 +128,6 @@ export class DeliverProcessorService {
}); });
} }
this.apRequestChart.deliverFail();
this.federationChart.deliverd(i.host, false);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false); this.instanceChart.requestSent(i.host, false);
} }
@ -129,7 +138,7 @@ export class DeliverProcessorService {
if (!res.isRetryable) { if (!res.isRetryable) {
// 相手が閉鎖していることを明示しているため、配送停止する // 相手が閉鎖していることを明示しているため、配送停止する
if (job.data.isSharedInbox && res.statusCode === 410) { if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => { this.federatedInstanceService.fetchOrRegister(host).then(i => {
this.federatedInstanceService.update(i.id, { this.federatedInstanceService.update(i.id, {
suspensionState: 'goneSuspended', suspensionState: 'goneSuspended',
}); });

View file

@ -192,21 +192,27 @@ export class InboxProcessorService implements OnApplicationShutdown {
} }
} }
// Update stats this.apRequestChart.inbox();
this.federatedInstanceService.fetch(authUser.user.host).then(i => { this.federationChart.inbox(authUser.user.host);
// Update instance stats
process.nextTick(async () => {
const i = await (this.meta.enableStatsForFederatedInstances
? this.federatedInstanceService.fetchOrRegister(authUser.user.host)
: this.federatedInstanceService.fetch(authUser.user.host));
if (i == null) return;
this.updateInstanceQueue.enqueue(i.id, { this.updateInstanceQueue.enqueue(i.id, {
latestRequestReceivedAt: new Date(), latestRequestReceivedAt: new Date(),
shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding', shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
}); });
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
this.apRequestChart.inbox();
this.federationChart.inbox(i.host);
if (this.meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host); this.instanceChart.requestReceived(i.host);
} }
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
}); });
// アクティビティを処理 // アクティビティを処理

View file

@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js'; import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
@ -486,6 +487,16 @@ export class ActivityPubServerService {
return; return;
} }
// リモートだったらリダイレクト
if (user.host != null) {
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
reply.code(500);
return;
}
reply.redirect(user.uri, 301);
return;
}
reply.header('Cache-Control', 'public, max-age=180'); reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply); this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
@ -654,19 +665,20 @@ export class ActivityPubServerService {
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
id: userId, id: userId,
host: IsNull(),
isSuspended: false, isSuspended: false,
}); });
return await this.userInfo(request, reply, user); return await this.userInfo(request, reply, user);
}); });
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
usernameLower: request.params.user.toLowerCase(), usernameLower: acct.username,
host: IsNull(), host: acct.host ?? IsNull(),
isSuspended: false, isSuspended: false,
}); });

View file

@ -319,6 +319,12 @@ export class FileServerService {
); );
} }
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 // Create temp file
const file = await this.getStreamAndTypeFromUrl(url); const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') { if (file === '404') {

View file

@ -119,6 +119,7 @@ export class ApiServerService {
'g-recaptcha-response'?: string; 'g-recaptcha-response'?: string;
'turnstile-response'?: string; 'turnstile-response'?: string;
'm-captcha-response'?: string; 'm-captcha-response'?: string;
'testcaptcha-response'?: string;
} }
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); }>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
@ -132,6 +133,7 @@ export class ApiServerService {
'g-recaptcha-response'?: string; 'g-recaptcha-response'?: string;
'turnstile-response'?: string; 'turnstile-response'?: string;
'm-captcha-response'?: string; 'm-captcha-response'?: string;
'testcaptcha-response'?: string;
}; };
}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply)); }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));

View file

@ -39,6 +39,17 @@ export class GetterService {
return note; return note;
} }
@bindThis
public async getNoteWithUser(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
}
return note;
}
/** /**
* Get user for API processing * Get user for API processing
*/ */

View file

@ -71,6 +71,7 @@ export class SigninApiService {
'g-recaptcha-response'?: string; 'g-recaptcha-response'?: string;
'turnstile-response'?: string; 'turnstile-response'?: string;
'm-captcha-response'?: string; 'm-captcha-response'?: string;
'testcaptcha-response'?: string;
}; };
}>, }>,
reply: FastifyReply, reply: FastifyReply,
@ -194,6 +195,12 @@ export class SigninApiService {
throw new FastifyReplyError(400, err); throw new FastifyReplyError(400, err);
}); });
} }
if (this.meta.enableTestcaptcha) {
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
} }
if (same) { if (same) {

View file

@ -67,6 +67,7 @@ export class SignupApiService {
'g-recaptcha-response'?: string; 'g-recaptcha-response'?: string;
'turnstile-response'?: string; 'turnstile-response'?: string;
'm-captcha-response'?: string; 'm-captcha-response'?: string;
'testcaptcha-response'?: string;
} }
}>, }>,
reply: FastifyReply, reply: FastifyReply,
@ -99,6 +100,12 @@ export class SignupApiService {
throw new FastifyReplyError(400, err); throw new FastifyReplyError(400, err);
}); });
} }
if (this.meta.enableTestcaptcha) {
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
} }
const username = body['username']; const username = body['username'];

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { IdService } from '@/core/IdService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -13,6 +14,49 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations', requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations', kind: 'write:admin:avatar-decorations',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisDecoration: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -32,14 +76,25 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private avatarDecorationService: AvatarDecorationService, private avatarDecorationService: AvatarDecorationService,
private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.create({ const created = await this.avatarDecorationService.create({
name: ps.name, name: ps.name,
description: ps.description, description: ps.description,
url: ps.url, url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me); }, me);
return {
id: created.id,
createdAt: this.idService.parse(created.id).date.toISOString(),
updatedAt: null,
name: created.name,
description: created.description,
url: created.url,
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
};
}); });
} }
} }

View file

@ -4,10 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
import type { MiAnnouncement } from '@/models/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';

View file

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import type { DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
@ -78,25 +78,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
} }
let emojiId; // JSON schemeのanyOfの型変換がうまくいっていないらしい
if (ps.id) { const required = { id: ps.id, name: ps.name } as
emojiId = ps.id; | { id: MiEmoji['id']; name?: string }
const emoji = await this.customEmojiService.getEmojiById(ps.id); | { id?: MiEmoji['id']; name: string };
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
if (ps.name && (ps.name !== emoji.name)) {
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
}
} else {
if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
const emoji = await this.customEmojiService.getEmojiByName(ps.name);
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
emojiId = emoji.id;
}
await this.customEmojiService.update(emojiId, { const error = await this.customEmojiService.update({
...required,
driveFile, driveFile,
name: ps.name,
category: ps.category, category: ps.category,
aliases: ps.aliases, aliases: ps.aliases,
license: ps.license, license: ps.license,
@ -104,6 +93,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
localOnly: ps.localOnly, localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}, me); }, me);
switch (error) {
case null: return;
case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
}
// 網羅性チェック
const mustBeNever: never = error;
}); });
} }
} }

View file

@ -69,6 +69,10 @@ export const meta = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
enableTestcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
swPublickey: { swPublickey: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
@ -173,6 +177,13 @@ export const meta = {
type: 'string', type: 'string',
}, },
}, },
prohibitedWordsForNameOfUser: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
bannedEmailDomains: { bannedEmailDomains: {
type: 'array', type: 'array',
optional: true, nullable: false, optional: true, nullable: false,
@ -337,6 +348,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableStatsForFederatedInstances: {
type: 'boolean',
optional: false, nullable: false,
},
enableServerMachineStats: { enableServerMachineStats: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -555,6 +570,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
recaptchaSiteKey: instance.recaptchaSiteKey, recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile, enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey, turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha,
swPublickey: instance.swPublicKey, swPublickey: instance.swPublicKey,
themeColor: instance.themeColor, themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl, mascotImageUrl: instance.mascotImageUrl,
@ -581,6 +597,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mediaSilencedHosts: instance.mediaSilencedHosts, mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords, sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords, prohibitedWords: instance.prohibitedWords,
prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser,
preservedUsernames: instance.preservedUsernames, preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey, hcaptchaSecretKey: instance.hcaptchaSecretKey,
mcaptchaSecretKey: instance.mcaptchaSecretKey, mcaptchaSecretKey: instance.mcaptchaSecretKey,
@ -622,6 +639,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
truemailAuthKey: instance.truemailAuthKey, truemailAuthKey: instance.truemailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats, enableServerMachineStats: instance.enableServerMachineStats,
enableIdenticonGeneration: instance.enableIdenticonGeneration, enableIdenticonGeneration: instance.enableIdenticonGeneration,
bannedEmailDomains: instance.bannedEmailDomains, bannedEmailDomains: instance.bannedEmailDomains,

View file

@ -71,13 +71,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break; break;
} }
case 'moderator': { case 'moderator': {
const moderatorIds = await this.roleService.getModeratorIds(false); const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
if (moderatorIds.length === 0) return []; if (moderatorIds.length === 0) return [];
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds }); query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
break; break;
} }
case 'adminOrModerator': { case 'adminOrModerator': {
const adminOrModeratorIds = await this.roleService.getModeratorIds(); const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
if (adminOrModeratorIds.length === 0) return []; if (adminOrModeratorIds.length === 0) return [];
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds }); query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
break; break;

View file

@ -46,6 +46,11 @@ export const paramDef = {
type: 'string', type: 'string',
}, },
}, },
prohibitedWordsForNameOfUser: {
type: 'array', nullable: true, items: {
type: 'string',
},
},
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true }, mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true },
@ -78,6 +83,7 @@ export const paramDef = {
enableTurnstile: { type: 'boolean' }, enableTurnstile: { type: 'boolean' },
turnstileSiteKey: { type: 'string', nullable: true }, turnstileSiteKey: { type: 'string', nullable: true },
turnstileSecretKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true },
enableTestcaptcha: { type: 'boolean' },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' }, setSensitiveFlagAutomatically: { type: 'boolean' },
@ -130,6 +136,7 @@ export const paramDef = {
truemailAuthKey: { type: 'string', nullable: true }, truemailAuthKey: { type: 'string', nullable: true },
enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' },
enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' },
enableIdenticonGeneration: { type: 'boolean' }, enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } }, serverRules: { type: 'array', items: { type: 'string' } },
@ -213,6 +220,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.prohibitedWords)) { if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean); set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
} }
if (Array.isArray(ps.prohibitedWordsForNameOfUser)) {
set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean);
}
if (Array.isArray(ps.silencedHosts)) { if (Array.isArray(ps.silencedHosts)) {
let lastValue = ''; let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => { set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
@ -357,6 +367,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.turnstileSecretKey = ps.turnstileSecretKey; set.turnstileSecretKey = ps.turnstileSecretKey;
} }
if (ps.enableTestcaptcha !== undefined) {
set.enableTestcaptcha = ps.enableTestcaptcha;
}
if (ps.sensitiveMediaDetection !== undefined) { if (ps.sensitiveMediaDetection !== undefined) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection; set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
} }
@ -565,6 +579,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances; set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
} }
if (ps.enableStatsForFederatedInstances !== undefined) {
set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances;
}
if (ps.enableServerMachineStats !== undefined) { if (ps.enableServerMachineStats !== undefined) {
set.enableServerMachineStats = ps.enableServerMachineStats; set.enableServerMachineStats = ps.enableServerMachineStats;
} }

View file

@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js'; import type { MiUserProfile } from '@/models/UserProfile.js';
@ -22,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { HashtagService } from '@/core/HashtagService.js'; import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js'; import { RolePolicies, RoleService } from '@/core/RoleService.js';
@ -114,6 +115,13 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE', code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad', id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
}, },
nameContainsProhibitedWords: {
message: 'Your new name contains prohibited words.',
code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS',
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
httpStatusCode: 422,
},
}, },
res: { res: {
@ -171,6 +179,9 @@ export const paramDef = {
autoAcceptFollowed: { type: 'boolean' }, autoAcceptFollowed: { type: 'boolean' },
noCrawle: { type: 'boolean' }, noCrawle: { type: 'boolean' },
preventAiLearning: { type: 'boolean' }, preventAiLearning: { type: 'boolean' },
requireSigninToViewContents: { type: 'boolean' },
makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true },
makeNotesHiddenBefore: { type: 'integer', nullable: true },
isBot: { type: 'boolean' }, isBot: { type: 'boolean' },
isCat: { type: 'boolean' }, isCat: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' },
@ -223,6 +234,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private instanceMeta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -247,6 +261,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService, private cacheService: CacheService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService, private avatarDecorationService: AvatarDecorationService,
private utilityService: UtilityService,
) { ) {
super(meta, paramDef, async (ps, _user, token) => { super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@ -322,6 +337,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents;
if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore;
if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
@ -447,8 +465,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name; const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) { if (newName != null) {
let hasProhibitedWords = false;
if (!await this.roleService.isModerator(user)) {
hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser);
}
if (hasProhibitedWords) {
throw new ApiError(meta.errors.nameContainsProhibitedWords);
}
const tokens = mfm.parseSimple(newName); const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
} }
@ -468,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]); ]);
} }
if (newFollowedMessage != null) {
const tokens = mfm.parse(newFollowedMessage);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
updates.emojis = emojis; updates.emojis = emojis;
updates.tags = tags; updates.tags = tags;

View file

@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const policies = await this.roleService.getUserPolicies(me.id); const policies = await this.roleService.getUserPolicies(me.id);
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({ const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
id: MoreThan(this.idService.gen(Date.now() - (policies.inviteExpirationTime * 60 * 1000))), id: MoreThan(this.idService.gen(Date.now() - (policies.inviteLimitCycle * 60 * 1000))),
createdById: me.id, createdById: me.id,
}) : null; }) : null;

View file

@ -26,6 +26,12 @@ export const meta = {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
}, },
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: '8e75455b-738c-471d-9f80-62693f33372e',
},
}, },
} as const; } as const;
@ -44,11 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService, private getterService: GetterService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => { const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err; throw err;
}); });
if (note.user!.requireSigninToViewContents && me == null) {
throw new ApiError(meta.errors.signinRequired);
}
return await this.noteEntityService.pack(note, me, { return await this.noteEntityService.pack(note, me, {
detail: true, detail: true,
}); });

View file

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.activeUsersChart.read(me); this.activeUsersChart.read(me);
await this.noteEntityService.packMany(timeline, me); return await this.noteEntityService.packMany(timeline, me);
} }
const timeline = await this.fanoutTimelineEndpointService.timeline({ const timeline = await this.fanoutTimelineEndpointService.timeline({

View file

@ -42,6 +42,12 @@ export const meta = {
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
}, },
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
},
}, },
} as const; } as const;

View file

@ -30,6 +30,7 @@ import type {
EndedPollNotificationQueue, EndedPollNotificationQueue,
InboxQueue, InboxQueue,
ObjectStorageQueue, ObjectStorageQueue,
RelationshipQueue,
SystemQueue, SystemQueue,
UserWebhookDeliverQueue, UserWebhookDeliverQueue,
SystemWebhookDeliverQueue, SystemWebhookDeliverQueue,
@ -41,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type {
AnnouncementsRepository,
ChannelsRepository,
ClipsRepository,
FlashsRepository,
GalleryPostsRepository,
MiMeta,
NotesRepository,
PagesRepository,
ReversiGamesRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
@ -102,6 +116,9 @@ export class ClientServerService {
@Inject(DI.reversiGamesRepository) @Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository, private reversiGamesRepository: ReversiGamesRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -111,6 +128,7 @@ export class ClientServerService {
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService, private urlPreviewService: UrlPreviewService,
private feedService: FeedService, private feedService: FeedService,
private roleService: RoleService, private roleService: RoleService,
@ -121,6 +139,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@ -248,6 +267,7 @@ export class ClientServerService {
this.deliverQueue, this.deliverQueue,
this.inboxQueue, this.inboxQueue,
this.dbQueue, this.dbQueue,
this.relationshipQueue,
this.objectStorageQueue, this.objectStorageQueue,
this.userWebhookDeliverQueue, this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue, this.systemWebhookDeliverQueue,
@ -598,12 +618,15 @@ export class ClientServerService {
fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => { fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOne({
where: {
id: request.params.note, id: request.params.note,
visibility: In(['public', 'home']), visibility: In(['public', 'home']),
},
relations: ['user'],
}); });
if (note) { if (note && !note.user!.requireSigninToViewContents) {
const _note = await this.noteEntityService.pack(note); const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
@ -770,6 +793,24 @@ export class ClientServerService {
return await renderBase(reply); return await renderBase(reply);
} }
}); });
// 個別お知らせページ
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
const announcement = await this.announcementsRepository.findOneBy({
id: request.params.announcementId,
});
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('announcement', {
announcement: _announcement,
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
}
});
//#endregion //#endregion
//#region noindex pages //#region noindex pages

View file

@ -0,0 +1,21 @@
extends ./base
block vars
- const title = announcement.title;
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
- const url = `${config.url}/announcements/${announcement.id}`;
block title
= `${title} | ${instanceName}`
block desc
meta(name='description' content=description)
block og
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= description)
meta(property='og:url' content= url)
if announcement.imageUrl
meta(property='og:image' content=announcement.imageUrl)
meta(property='twitter:card' content='summary_large_image')

View file

@ -2,6 +2,7 @@ block vars
block loadClientEntry block loadClientEntry
- const entry = config.frontendEntry; - const entry = config.frontendEntry;
- const baseUrl = config.url;
doctype html doctype html
@ -32,7 +33,7 @@ html
link(rel='icon' href= icon || '/favicon.ico') link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)

View file

@ -0,0 +1,70 @@
# based on https://github.com/misskey-dev/misskey-hub/blob/7071f63a1c80ee35c71f0fd8a6d8722c118c7574/src/docs/admin/nginx.md
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name ${HOST};
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ${HOST};
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
ssl_trusted_certificate /etc/nginx/certificates/rootCA.crt;
ssl_certificate /etc/nginx/certificates/$server_name.crt;
ssl_certificate_key /etc/nginx/certificates/$server_name.key;
# SSL protocol settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://misskey.${HOST}:3000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_redirect off;
# If it's behind another reverse proxy or CDN, remove the following.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
proxy_force_ranges on;
add_header X-Cache $upstream_cache_status;
}
}

View file

@ -0,0 +1,25 @@
url: https://${HOST}/
port: 3000
db:
host: db.${HOST}
port: 5432
db: misskey
user: postgres
pass: postgres
dbReplications: false
redis:
host: redis.test
port: 6379
id: 'aidx'
proxyBypassHosts:
- api.deepl.com
- api-free.deepl.com
- www.recaptcha.net
- hcaptcha.com
- challenges.cloudflare.com
proxyRemoteFiles: true
signToActivityPubGet: true
allowedPrivateNetworks: [
'127.0.0.1/32',
'172.20.0.0/16'
]

View file

@ -0,0 +1,5 @@
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
POSTGRES_DB=misskey
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
MK_VERBOSE=true

View file

@ -0,0 +1,6 @@
certificates
volumes
.env
docker.env
*.test.conf
*.test.default.yml

View file

@ -0,0 +1,24 @@
## test-federation
Test federation between two Misskey servers: `a.test` and `b.test`.
Before testing, you need to build the entire project, and change working directory to here:
```sh
pnpm build
cd packages/backend/test-federation
```
First, you need to start servers by executing following commands:
```sh
bash ./setup.sh
docker compose up --scale tester=0
```
Then you can run all tests by a following command:
```sh
docker compose run --no-deps --rm tester
```
For testing a specific file, run a following command:
```sh
docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
```

View file

@ -0,0 +1,64 @@
services:
a.test:
extends:
file: ./compose.tpl.yml
service: nginx
depends_on:
misskey.a.test:
condition: service_healthy
networks:
- internal_network_a
volumes:
- type: bind
source: ./.config/a.test.conf
target: /etc/nginx/conf.d/a.test.conf
read_only: true
- type: bind
source: ./certificates/a.test.crt
target: /etc/nginx/certificates/a.test.crt
read_only: true
- type: bind
source: ./certificates/a.test.key
target: /etc/nginx/certificates/a.test.key
read_only: true
misskey.a.test:
extends:
file: ./compose.tpl.yml
service: misskey
depends_on:
db.a.test:
condition: service_healthy
redis.test:
condition: service_healthy
setup:
condition: service_completed_successfully
networks:
- internal_network_a
volumes:
- type: bind
source: ./.config/a.test.default.yml
target: /misskey/.config/default.yml
read_only: true
db.a.test:
extends:
file: ./compose.tpl.yml
service: db
networks:
- internal_network_a
volumes:
- type: bind
source: ./volumes/db.a
target: /var/lib/postgresql/data
bind:
create_host_path: true
networks:
internal_network_a:
internal: true
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
ip_range: 172.21.0.0/24

View file

@ -0,0 +1,64 @@
services:
b.test:
extends:
file: ./compose.tpl.yml
service: nginx
depends_on:
misskey.b.test:
condition: service_healthy
networks:
- internal_network_b
volumes:
- type: bind
source: ./.config/b.test.conf
target: /etc/nginx/conf.d/b.test.conf
read_only: true
- type: bind
source: ./certificates/b.test.crt
target: /etc/nginx/certificates/b.test.crt
read_only: true
- type: bind
source: ./certificates/b.test.key
target: /etc/nginx/certificates/b.test.key
read_only: true
misskey.b.test:
extends:
file: ./compose.tpl.yml
service: misskey
depends_on:
db.b.test:
condition: service_healthy
redis.test:
condition: service_healthy
setup:
condition: service_completed_successfully
networks:
- internal_network_b
volumes:
- type: bind
source: ./.config/b.test.default.yml
target: /misskey/.config/default.yml
read_only: true
db.b.test:
extends:
file: ./compose.tpl.yml
service: db
networks:
- internal_network_b
volumes:
- type: bind
source: ./volumes/db.b
target: /var/lib/postgresql/data
bind:
create_host_path: true
networks:
internal_network_b:
internal: true
driver: bridge
ipam:
config:
- subnet: 172.22.0.0/16
ip_range: 172.22.0.0/24

View file

@ -0,0 +1,117 @@
services:
setup:
volumes:
- type: volume
source: node_modules
target: /misskey/node_modules
- type: volume
source: node_modules_backend
target: /misskey/packages/backend/node_modules
- type: volume
source: node_modules_misskey-js
target: /misskey/packages/misskey-js/node_modules
- type: volume
source: node_modules_misskey-reversi
target: /misskey/packages/misskey-reversi/node_modules
tester:
networks:
external_network:
internal_network:
ipv4_address: 172.20.1.1
volumes:
- type: volume
source: node_modules_dev
target: /misskey/node_modules
- type: volume
source: node_modules_backend_dev
target: /misskey/packages/backend/node_modules
- type: volume
source: node_modules_misskey-js_dev
target: /misskey/packages/misskey-js/node_modules
daemon:
networks:
- external_network
- internal_network_a
- internal_network_b
volumes:
- type: volume
source: node_modules_dev
target: /misskey/node_modules
- type: volume
source: node_modules_backend_dev
target: /misskey/packages/backend/node_modules
redis.test:
networks:
- internal_network_a
- internal_network_b
a.test:
networks:
- internal_network
misskey.a.test:
networks:
- external_network
- internal_network
volumes:
- type: volume
source: node_modules
target: /misskey/node_modules
- type: volume
source: node_modules_backend
target: /misskey/packages/backend/node_modules
- type: volume
source: node_modules_misskey-js
target: /misskey/packages/misskey-js/node_modules
- type: volume
source: node_modules_misskey-reversi
target: /misskey/packages/misskey-reversi/node_modules
b.test:
networks:
- internal_network
misskey.b.test:
networks:
- external_network
- internal_network
volumes:
- type: volume
source: node_modules
target: /misskey/node_modules
- type: volume
source: node_modules_backend
target: /misskey/packages/backend/node_modules
- type: volume
source: node_modules_misskey-js
target: /misskey/packages/misskey-js/node_modules
- type: volume
source: node_modules_misskey-reversi
target: /misskey/packages/misskey-reversi/node_modules
networks:
external_network:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16
ip_range: 172.23.0.0/24
internal_network:
internal: true
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
ip_range: 172.20.0.0/24
volumes:
node_modules:
node_modules_dev:
node_modules_backend:
node_modules_backend_dev:
node_modules_misskey-js:
node_modules_misskey-js_dev:
node_modules_misskey-reversi:

View file

@ -0,0 +1,101 @@
services:
nginx:
image: nginx:1.27
volumes:
- type: bind
source: ./certificates/rootCA.crt
target: /etc/nginx/certificates/rootCA.crt
read_only: true
healthcheck:
test: service nginx status
interval: 5s
retries: 20
misskey:
image: node:20
env_file:
- ./.config/docker.env
environment:
- NODE_ENV=production
volumes:
- type: bind
source: ../../../built
target: /misskey/built
read_only: true
- type: bind
source: ../assets
target: /misskey/packages/backend/assets
read_only: true
- type: bind
source: ../built
target: /misskey/packages/backend/built
read_only: true
- type: bind
source: ../migration
target: /misskey/packages/backend/migration
read_only: true
- type: bind
source: ../ormconfig.js
target: /misskey/packages/backend/ormconfig.js
read_only: true
- type: bind
source: ../package.json
target: /misskey/packages/backend/package.json
read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
read_only: true
- type: bind
source: ../../misskey-js/package.json
target: /misskey/packages/misskey-js/package.json
read_only: true
- type: bind
source: ../../misskey-reversi/built
target: /misskey/packages/misskey-reversi/built
read_only: true
- type: bind
source: ../../misskey-reversi/package.json
target: /misskey/packages/misskey-reversi/package.json
read_only: true
- type: bind
source: ../../../healthcheck.sh
target: /misskey/healthcheck.sh
read_only: true
- type: bind
source: ../../../package.json
target: /misskey/package.json
read_only: true
- type: bind
source: ../../../pnpm-lock.yaml
target: /misskey/pnpm-lock.yaml
read_only: true
- type: bind
source: ../../../pnpm-workspace.yaml
target: /misskey/pnpm-workspace.yaml
read_only: true
- type: bind
source: ./certificates/rootCA.crt
target: /usr/local/share/ca-certificates/rootCA.crt
read_only: true
working_dir: /misskey
command: >
bash -c "
corepack enable && corepack prepare
pnpm -F backend migrate
pnpm -F backend start
"
healthcheck:
test: bash /misskey/healthcheck.sh
interval: 5s
retries: 20
db:
image: postgres:15-alpine
env_file:
- ./.config/docker.env
volumes:
healthcheck:
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
interval: 5s
retries: 20

View file

@ -0,0 +1,133 @@
include:
- ./compose.a.yml
- ./compose.b.yml
services:
setup:
extends:
file: ./compose.tpl.yml
service: misskey
command: >
bash -c "
corepack enable && corepack prepare
pnpm -F backend i
pnpm -F misskey-js i
pnpm -F misskey-reversi i
"
tester:
image: node:20
depends_on:
a.test:
condition: service_healthy
b.test:
condition: service_healthy
environment:
- NODE_ENV=development
- NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
volumes:
- type: bind
source: ../package.json
target: /misskey/packages/backend/package.json
read_only: true
- type: bind
source: ../test/resources
target: /misskey/packages/backend/test/resources
read_only: true
- type: bind
source: ./test
target: /misskey/packages/backend/test-federation/test
read_only: true
- type: bind
source: ../jest.config.cjs
target: /misskey/packages/backend/jest.config.cjs
read_only: true
- type: bind
source: ../jest.config.fed.cjs
target: /misskey/packages/backend/jest.config.fed.cjs
read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
read_only: true
- type: bind
source: ../../misskey-js/package.json
target: /misskey/packages/misskey-js/package.json
read_only: true
- type: bind
source: ../../../package.json
target: /misskey/package.json
read_only: true
- type: bind
source: ../../../pnpm-lock.yaml
target: /misskey/pnpm-lock.yaml
read_only: true
- type: bind
source: ../../../pnpm-workspace.yaml
target: /misskey/pnpm-workspace.yaml
read_only: true
- type: bind
source: ./certificates/rootCA.crt
target: /usr/local/share/ca-certificates/rootCA.crt
read_only: true
working_dir: /misskey
entrypoint: >
bash -c '
corepack enable && corepack prepare
pnpm -F misskey-js i --frozen-lockfile
pnpm -F backend i --frozen-lockfile
exec "$0" "$@"
'
command: pnpm -F backend test:fed
daemon:
image: node:20
depends_on:
redis.test:
condition: service_healthy
volumes:
- type: bind
source: ../package.json
target: /misskey/packages/backend/package.json
read_only: true
- type: bind
source: ./daemon.ts
target: /misskey/packages/backend/test-federation/daemon.ts
read_only: true
- type: bind
source: ./tsconfig.json
target: /misskey/packages/backend/test-federation/tsconfig.json
read_only: true
- type: bind
source: ../../../package.json
target: /misskey/package.json
read_only: true
- type: bind
source: ../../../pnpm-lock.yaml
target: /misskey/pnpm-lock.yaml
read_only: true
- type: bind
source: ../../../pnpm-workspace.yaml
target: /misskey/pnpm-workspace.yaml
read_only: true
working_dir: /misskey
command: >
bash -c "
corepack enable && corepack prepare
pnpm -F backend i --frozen-lockfile
pnpm exec tsc -p ./packages/backend/test-federation
node ./packages/backend/test-federation/built/daemon.js
"
redis.test:
image: redis:7-alpine
volumes:
- type: bind
source: ./volumes/redis
target: /data
bind:
create_host_path: true
healthcheck:
test: redis-cli ping
interval: 5s
retries: 20

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