mirror of
https://codeberg.org/yeentown/barkey
synced 2024-11-25 05:55:14 +00:00
merge: misskey 2024.7 (!583)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/583 Approved-by: Marie <github@yuugi.dev> Approved-by: Julia Johannesen <julia@insertdomain.name>
This commit is contained in:
commit
f5560783ea
598 changed files with 23943 additions and 9812 deletions
|
@ -1,5 +1,11 @@
|
|||
# misskey settings
|
||||
# MISSKEY_URL=https://example.tld/
|
||||
|
||||
# db settings
|
||||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER=example-misskey-user
|
||||
# DATABASE_USER=${POSTGRES_USER}
|
||||
POSTGRES_DB=misskey
|
||||
# DATABASE_DB=${POSTGRES_DB}
|
||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
# You can set url from an environment variable instead.
|
||||
url: https://example.tld/
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
|
@ -95,9 +96,11 @@ db:
|
|||
port: 5432
|
||||
|
||||
# Database name
|
||||
# You can set db from an environment variable instead.
|
||||
db: misskey
|
||||
|
||||
# Auth
|
||||
# You can set user and pass from environment variables instead.
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
|
@ -8,6 +6,7 @@ services:
|
|||
|
||||
volumes:
|
||||
- ../:/workspace:cached
|
||||
- node_modules:/workspace/node_modules
|
||||
|
||||
command: sleep infinity
|
||||
|
||||
|
@ -46,6 +45,7 @@ services:
|
|||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
node_modules:
|
||||
|
||||
networks:
|
||||
internal_network:
|
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "Misskey",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"dockerComposeFile": "compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "20.12.2"
|
||||
"version": "20.16.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
|
||||
"postCreateCommand": "/bin/bash .devcontainer/init.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
set -xe
|
||||
|
||||
sudo chown -R node /workspace
|
||||
sudo chown node node_modules
|
||||
git config --global --add safe.directory /workspace
|
||||
git submodule update --init
|
||||
corepack install
|
||||
corepack enable
|
||||
|
|
|
@ -7,12 +7,11 @@ Dockerfile
|
|||
build/
|
||||
built/
|
||||
db/
|
||||
docker-compose.yml
|
||||
.devcontainer/compose.yml
|
||||
node_modules/
|
||||
packages/*/node_modules
|
||||
redis/
|
||||
files/
|
||||
misskey-assets/
|
||||
fluent-emojis/
|
||||
.pnp.*
|
||||
|
||||
|
@ -28,4 +27,4 @@ fluent-emojis/
|
|||
|
||||
.idea/
|
||||
packages/*/.vscode/
|
||||
packages/backend/test/docker-compose.yml
|
||||
packages/backend/test/compose.yml
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -35,8 +35,8 @@ coverage
|
|||
!/.config/example.yml
|
||||
!/.config/docker_example.yml
|
||||
!/.config/docker_example.env
|
||||
docker-compose.yml
|
||||
!/.devcontainer/docker-compose.yml
|
||||
.devcontainer/compose.yml
|
||||
!/.devcontainer/compose.yml
|
||||
|
||||
# misskey
|
||||
/build
|
||||
|
@ -59,6 +59,7 @@ ormconfig.json
|
|||
temp
|
||||
/packages/frontend/src/**/*.stories.ts
|
||||
tsdoc-metadata.json
|
||||
misskey-assets
|
||||
|
||||
# Sharkey
|
||||
/packages/megalodon/lib
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,6 +1,3 @@
|
|||
[submodule "misskey-assets"]
|
||||
path = misskey-assets
|
||||
url = https://github.com/misskey-dev/assets.git
|
||||
[submodule "fluent-emojis"]
|
||||
path = fluent-emojis
|
||||
url = https://github.com/misskey-dev/emojis.git
|
||||
|
|
|
@ -1 +1 @@
|
|||
20.12.2
|
||||
20.16.0
|
||||
|
|
121
CHANGELOG.md
121
CHANGELOG.md
|
@ -1,14 +1,129 @@
|
|||
## Unreleased
|
||||
## 2024.7.0
|
||||
|
||||
### Note
|
||||
- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
|
||||
- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
|
||||
|
||||
### General
|
||||
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
|
||||
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
|
||||
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
|
||||
- Feat: メディアサイレンスを実装 #13842
|
||||
- メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。
|
||||
- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
|
||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
||||
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
|
||||
- 翻訳の更新
|
||||
- 依存関係の更新
|
||||
|
||||
### Client
|
||||
-
|
||||
- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
|
||||
- Feat: 検索ページはクエリを受け付けるようになりました (#14128)
|
||||
- Enhance: 検索ページのUI改善 (#14128)
|
||||
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
|
||||
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
|
||||
- Enhance: 非ログイン時のハイライトTLのデザインを改善
|
||||
- Enhance: フロントエンドのアクセシビリティ改善
|
||||
(Based on https://github.com/taiyme/misskey/pull/226)
|
||||
- Enhance: サーバー情報ページ・お問い合わせページを改善
|
||||
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
|
||||
- Enhance: AiScriptを0.19.0にアップデート
|
||||
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
|
||||
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
|
||||
- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように
|
||||
- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
|
||||
- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
|
||||
- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
|
||||
- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
|
||||
(Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
|
||||
- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
|
||||
- Enhance: ブラウザのコンテキストメニューを使用できるように
|
||||
- Enhance: 連合の「連合中」,「購読中」,「配信中」に対してブロックしているサーバー、配信停止しているサーバーを含めないように
|
||||
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
||||
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
||||
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
|
||||
- Fix: アンテナの編集画面のボタンに隙間を追加
|
||||
- Fix: テーマプレビューが見れない問題を修正
|
||||
- Fix: ショートカットキーが連打できる問題を修正
|
||||
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
|
||||
- Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため)
|
||||
- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574)
|
||||
- Fix: Twitchの埋め込みが開けない問題を修正
|
||||
- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
|
||||
- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
|
||||
- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
|
||||
- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
|
||||
- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
|
||||
- Fix: deck uiの通知音が重なる問題 (#14029)
|
||||
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
|
||||
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
|
||||
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
|
||||
- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正
|
||||
- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正
|
||||
- Fix: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
|
||||
- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正
|
||||
- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正
|
||||
- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
|
||||
- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正
|
||||
- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正
|
||||
|
||||
### Server
|
||||
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
|
||||
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
|
||||
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
|
||||
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
|
||||
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
|
||||
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
|
||||
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
|
||||
- Enhance: エンドポイント`api/meta`にプロパティ`noteSearchableScope`が増え、`string`値`local`または`global`を返却します
|
||||
- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
|
||||
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
|
||||
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
||||
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
|
||||
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
|
||||
- Fix: 空文字列のリアクションはフォールバックされるように
|
||||
- Fix: リノートにリアクションできないように
|
||||
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
|
||||
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
|
||||
- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
|
||||
1. フォロー中かつアクティブなユーザ
|
||||
2. フォロー中かつ非アクティブなユーザ
|
||||
3. フォローしていないアクティブなユーザ
|
||||
4. フォローしていない非アクティブなユーザ
|
||||
|
||||
また、自分自身のアカウントもサジェストされるようになりました。
|
||||
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
|
||||
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
|
||||
- Fix: FTT有効時にリモートユーザーのノートがHTLにキャッシュされる問題を修正
|
||||
- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正
|
||||
- Fix: エラーメッセージの誤字を修正 (#14213)
|
||||
- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
|
||||
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
|
||||
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
|
||||
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
|
||||
- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
|
||||
- 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります
|
||||
- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題
|
||||
(Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76)
|
||||
- NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます
|
||||
Migrationではカラム定義の変更のみが行われます。
|
||||
サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です
|
||||
- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正
|
||||
- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正
|
||||
- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
|
||||
- キュー処理のつまりが改善される可能性があります
|
||||
|
||||
### Misskey.js
|
||||
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
||||
- Feat: `/admin/role/create` のロールポリシーの型を修正
|
||||
|
||||
## 2024.5.0
|
||||
|
||||
|
|
148
CONTRIBUTING.md
148
CONTRIBUTING.md
|
@ -11,10 +11,12 @@ Before creating an issue, please check the following:
|
|||
- Issues should only be used to feature requests, suggestions, and bug tracking.
|
||||
- Please ask questions or troubleshooting in [Discord](https://discord.gg/6VgKmEqHNk).
|
||||
|
||||
> **Warning**
|
||||
> [!WARNING]
|
||||
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
||||
|
||||
## Before implementation
|
||||
### Recommended discussing before implementation
|
||||
We welcome your proposal.
|
||||
|
||||
When you want to add a feature or fix a bug, *please open an issue*,
|
||||
don't just start writing code. We may suggest different approaches, or
|
||||
show that the "bug" is actually intended behaviour (and offer
|
||||
|
@ -25,7 +27,20 @@ Misskey. Each of these examples have actually happened!
|
|||
On the other hand, it's very likely that we'll tell you "go
|
||||
ahead!". We try our best to incorporate improvements from our users!
|
||||
|
||||
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work.
|
||||
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you).
|
||||
By expressing your intention to work on the Issue, you can prevent conflicts in the work.
|
||||
|
||||
To the Committers: you should not assign someone on it before the Final Decision.
|
||||
|
||||
### How issues are triaged
|
||||
|
||||
The Committers may:
|
||||
* close an issue that is not reproducible on latest stable release,
|
||||
* merge an issue into another issue,
|
||||
* split an issue into multiple issues,
|
||||
* or re-open that has been closed for some reason which is not applicable anymore.
|
||||
|
||||
@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised.
|
||||
|
||||
## Well-known branches
|
||||
- **`stable`** branch is tracking the latest release and used for production purposes.
|
||||
|
@ -35,14 +50,14 @@ Also, when you start implementation, assign yourself to the Issue (if you cannot
|
|||
## Creating a PR
|
||||
Thank you for your PR! Before creating a PR, please check the following:
|
||||
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
|
||||
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
|
||||
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
|
||||
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc
|
||||
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
|
||||
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
|
||||
- Please add the summary of the changes to [`CHANGELOG.md`](CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring.
|
||||
- Check if there are any documents that need to be created or updated due to this change.
|
||||
- If you have added a feature or fixed a bug, please add a test case if possible.
|
||||
- Please make sure that tests and Lint are passed in advance.
|
||||
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
|
||||
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
|
||||
- If this PR includes UI changes, please attach a screenshot in the text.
|
||||
|
||||
Thanks for your cooperation 🤗
|
||||
|
@ -52,8 +67,8 @@ Be willing to comment on the good points and not just the things you want fixed
|
|||
|
||||
### Review perspective
|
||||
- Scope
|
||||
- Are the goals of the PR clear?
|
||||
- Is the granularity of the PR appropriate?
|
||||
- Are the goals of the PR clear?
|
||||
- Is the granularity of the PR appropriate?
|
||||
- Security
|
||||
- Does merging this PR create a vulnerability?
|
||||
- Performance
|
||||
|
@ -68,7 +83,7 @@ Be willing to comment on the good points and not just the things you want fixed
|
|||
|
||||
## Release
|
||||
### Release Instructions
|
||||
1. Commit version changes in the `develop` branch ([package.json](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/package.json))
|
||||
1. Commit version changes in the `develop` branch ([package.json](package.json))
|
||||
2. Create a release PR.
|
||||
- Into `stable` from `develop` branch.
|
||||
- The title must be in the format `Release: x.y.z`.
|
||||
|
@ -79,7 +94,7 @@ Be willing to comment on the good points and not just the things you want fixed
|
|||
- The target branch must be `stable`
|
||||
- The tag name must be the version
|
||||
|
||||
> **Note**
|
||||
> [!NOTE]
|
||||
> Why this instruction is necessary:
|
||||
> - To perform final QA checks
|
||||
> - To distribute responsibility
|
||||
|
@ -106,12 +121,42 @@ When Updating the Font make sure to copy **all generated files** from the `dest`
|
|||
For the CSS simply copy the file content and replace the old content in `style.css` and for the WOFF, TTF and SVG simply replace them.
|
||||
|
||||
## Development
|
||||
During development, it is useful to use the
|
||||
### Setup
|
||||
Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg.
|
||||
|
||||
You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it.
|
||||
|
||||
There are a few ways to proceed.
|
||||
|
||||
#### Use system-wide software
|
||||
You could install them in system-wide (such as from package manager).
|
||||
|
||||
#### Use `docker compose`
|
||||
You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`.
|
||||
|
||||
#### Use Devcontainer
|
||||
Devcontainer also has necessary setting. This method can be done by connecting from VSCode.
|
||||
|
||||
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
|
||||
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
|
||||
|
||||
It will run the following command automatically inside the container.
|
||||
``` bash
|
||||
git submodule update --init
|
||||
pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
After finishing the migration, you can proceed.
|
||||
|
||||
### Start developing
|
||||
During development, it is useful to use the
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
command.
|
||||
|
||||
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
|
||||
|
@ -135,26 +180,6 @@ MK_DEV_PREFER=backend pnpm dev
|
|||
- To change the port of Vite, specify with `VITE_PORT` environment variable.
|
||||
- HMR may not work in some environments such as Windows.
|
||||
|
||||
### Dev Container
|
||||
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
|
||||
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
|
||||
|
||||
It will run the following command automatically inside the container.
|
||||
``` bash
|
||||
git submodule update --init
|
||||
pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
After finishing the migration, run the `pnpm dev` command to start the development server.
|
||||
|
||||
``` bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
- Test codes are located in [`/packages/backend/test`](packages/backend/test).
|
||||
|
||||
|
@ -165,7 +190,7 @@ cp .github/misskey/test.yml .config/
|
|||
```
|
||||
Prepare DB/Redis for testing.
|
||||
```
|
||||
docker compose -f packages/backend/test/docker-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`.
|
||||
|
||||
|
@ -204,7 +229,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||
### ルート定義
|
||||
ルート定義は、以下の形式のオブジェクトの配列です。
|
||||
|
||||
``` ts
|
||||
```ts
|
||||
{
|
||||
name?: string;
|
||||
path: string;
|
||||
|
@ -217,7 +242,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||
}
|
||||
```
|
||||
|
||||
> **Warning**
|
||||
> [!WARNING]
|
||||
> 現状、ルートは定義された順に評価されます。
|
||||
> たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
|
||||
|
||||
|
@ -279,7 +304,7 @@ export const Default = {
|
|||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkAvatar>;
|
||||
} satisfies StoryObj<typeof MyComponent>;
|
||||
```
|
||||
|
||||
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
|
||||
|
@ -390,7 +415,7 @@ describe('test', () => {
|
|||
})
|
||||
.useMocker(...
|
||||
.compile();
|
||||
|
||||
|
||||
fooService = app.get<FooService>(FooService);
|
||||
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
|
||||
|
||||
|
@ -511,13 +536,13 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
|||
- 作成されたスクリプトは不必要な変更を含むため除去してください
|
||||
|
||||
### JSON SchemaのobjectでanyOfを使うとき
|
||||
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
|
||||
バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます)
|
||||
JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。
|
||||
バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます)
|
||||
https://github.com/misskey-dev/misskey/pull/10082
|
||||
|
||||
テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合:
|
||||
|
||||
```
|
||||
```ts
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -564,30 +589,45 @@ seems to do a decent job)
|
|||
|
||||
*after that commit*, do all the extra work, on the same branch:
|
||||
|
||||
* copy all changes:
|
||||
* from `NoteCreateService.create` to `NoteCreateService.import` (and
|
||||
vice versa if `git` got confused!)
|
||||
* from `NoteCreateService` to `NoteEditService`
|
||||
* from `ApNoteService.createNote` to `ApNoteService.updateNote`
|
||||
* from `endoints/notes/create.ts` to `endoints/notes/edit.ts`
|
||||
* from `MkNote*` to `SkNote*` (if sensible)
|
||||
* copy all changes (commit after each step):
|
||||
* in `packages/backend/src/core/NoteCreateService.ts`, from `create` to
|
||||
`import` (and vice versa if `git` got confused!)
|
||||
* from `packages/backend/src/core/NoteCreateService.ts` to
|
||||
`packages/backend/src/core/NoteEditService.vue`
|
||||
* in `packages/backend/src/core/activitypub/models/ApNoteService.ts`,
|
||||
from `createNote` to `updateNote`
|
||||
* from `packages/backend/src/server/api/endpoints/notes/create.ts`
|
||||
to `packages/backend/src/server/api/endpoints/notes/edit.ts`
|
||||
* from `packages/frontend/src/components/MkNote*.vue` to
|
||||
`packages/frontend/src/components/SkNote*.vue` (if sensible)
|
||||
* from the global timeline to the bubble timeline
|
||||
(`packages/backend/src/server/api/stream/channels/global-timeline.ts`,
|
||||
`packages/backend/src/server/api/stream/channels/bubble-timeline.ts`,
|
||||
`packages/frontend/src/timelines.ts`,
|
||||
`packages/frontend/src/components/MkTimeline.vue`,
|
||||
`packages/frontend/src/pages/timeline.vue`,
|
||||
`packages/frontend/src/ui/deck/tl-column.vue`,
|
||||
`packages/frontend/src/widgets/WidgetTimeline.vue`)
|
||||
* re-generate `misskey-js` (`pnpm build-misskey-js-with-types`) and commit
|
||||
* build the frontend: `rm -rf built/; NODE_ENV=development pnpm --filter=frontend
|
||||
build` (the `development` tells it to keep some of the original
|
||||
filenames in the built files)
|
||||
* make sure there aren't any new `ti-*` classes (Tabler Icons), and
|
||||
replace them with appropriate `ph-*` ones (Phosphor Icons).
|
||||
`git grep '["'\'']ti[ -](?!fw)'` should show you what to change.
|
||||
replace them with appropriate `ph-*` ones (Phosphor Icons):
|
||||
`grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change.
|
||||
NOTE: `ti-fw` is a special class that's defined by Misskey, leave it
|
||||
alone
|
||||
* re-generate `misskey-js`: `pnpm build-misskey-js-with-types`
|
||||
* run tests `pnpm test` and fix as much as you can
|
||||
* right now `megalodon` doesn't pass its tests, you probably need to
|
||||
run `pnpm --filter=backend test` (requires a test database, [see
|
||||
above](#testing)) and `pnpm --filter=frontend test`
|
||||
|
||||
after every change, re-build the frontend and check again, until
|
||||
there are no more `ti-*` classes in the built files
|
||||
|
||||
commit!
|
||||
* double-check the new migration, that they won't conflict with our db
|
||||
changes: `git diff develop -- packages/backend/migration/`
|
||||
* `pnpm clean; pnpm build`
|
||||
* run tests `pnpm --filter='!megalodon' test` (requires a test
|
||||
database, [see above](#testing)) and fix as much as you can
|
||||
* right now `megalodon` doesn't pass its tests, so we skip them
|
||||
* run lint `pnpm --filter=backend lint` + `pnpm --filter=frontend
|
||||
eslint` and fix as much as you can
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=20.12.2-alpine3.19
|
||||
ARG NODE_VERSION=20.16.0-alpine3.20
|
||||
|
||||
FROM node:${NODE_VERSION} as build
|
||||
|
||||
|
@ -45,6 +45,10 @@ RUN apk add ffmpeg tini jemalloc \
|
|||
USER sharkey
|
||||
WORKDIR /sharkey
|
||||
|
||||
# add package.json to add pnpm
|
||||
COPY --chown=sharkey:sharkey ./package.json ./package.json
|
||||
RUN corepack install
|
||||
|
||||
COPY --chown=sharkey:sharkey --from=build /sharkey/node_modules ./node_modules
|
||||
COPY --chown=sharkey:sharkey --from=build /sharkey/packages/backend/node_modules ./packages/backend/node_modules
|
||||
COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
|
@ -61,7 +65,6 @@ COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis
|
|||
COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist
|
||||
COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets
|
||||
|
||||
COPY --chown=sharkey:sharkey package.json ./package.json
|
||||
COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json
|
||||
COPY --chown=sharkey:sharkey packages/backend/scripts/check_connect.js ./packages/backend/scripts/check_connect.js
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
version: "3"
|
||||
|
||||
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
|
||||
|
||||
services:
|
|
@ -1,5 +1,3 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
# image: registry.activitypub.software/transfem-org/sharkey:latest
|
||||
|
@ -19,6 +17,8 @@ services:
|
|||
- "3000:3000"
|
||||
networks:
|
||||
- shonk
|
||||
# env_file:
|
||||
# - .config/docker.env
|
||||
volumes:
|
||||
- ./files:/sharkey/files
|
||||
- ./.config:/sharkey/.config:ro
|
||||
|
@ -53,8 +53,7 @@ services:
|
|||
# restart: always
|
||||
# image: mcaptcha/mcaptcha:latest
|
||||
# networks:
|
||||
# internal_network:
|
||||
# external_network:
|
||||
# shonks:
|
||||
# aliases:
|
||||
# - localhost
|
||||
# ports:
|
||||
|
@ -73,7 +72,7 @@ services:
|
|||
# mcaptcha_redis:
|
||||
# image: mcaptcha/cache:latest
|
||||
# networks:
|
||||
# - internal_network
|
||||
# - shonks
|
||||
# healthcheck:
|
||||
# test: "redis-cli ping"
|
||||
# interval: 5s
|
|
@ -1262,8 +1262,6 @@ _sfx:
|
|||
note: "الملاحظات"
|
||||
noteMy: "ملاحظتي"
|
||||
notification: "الإشعارات"
|
||||
antenna: "الهوائيات"
|
||||
channel: "إشعارات القنات"
|
||||
_ago:
|
||||
future: "المستقبَل"
|
||||
justNow: "اللحظة"
|
||||
|
@ -1566,6 +1564,10 @@ _webhookSettings:
|
|||
active: "مُفعّل"
|
||||
_events:
|
||||
reaction: "عند التفاعل"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "البريد الإلكتروني "
|
||||
_moderationLogTypes:
|
||||
suspend: "علِق"
|
||||
deleteDriveFile: "حُذف الملف"
|
||||
|
|
|
@ -1033,8 +1033,6 @@ _sfx:
|
|||
note: "নোটগুলি"
|
||||
noteMy: "নোট (আপনার)"
|
||||
notification: "বিজ্ঞপ্তি"
|
||||
antenna: "অ্যান্টেনাগুলি"
|
||||
channel: "চ্যানেলের বিজ্ঞপ্তি"
|
||||
_ago:
|
||||
future: "ভবিষ্যৎ"
|
||||
justNow: "এইমাত্র"
|
||||
|
@ -1346,6 +1344,10 @@ _deck:
|
|||
_webhookSettings:
|
||||
name: "নাম"
|
||||
active: "চালু"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "ইমেইল"
|
||||
_moderationLogTypes:
|
||||
suspend: "স্থগিত করা"
|
||||
resetPassword: "পাসওয়ার্ড রিসেট করুন"
|
||||
|
|
|
@ -540,7 +540,7 @@ objectStorageRegion: "Regió "
|
|||
objectStorageRegionDesc: "Especifica una regió com 'xx-east-1'. Si el teu servei no diferència regions has de posar 'us-east-1'. Deixa'l buit si fas servir variables d'entorn o un arxiu de configuració d'AWS."
|
||||
objectStorageUseSSL: "Fes servir SSL"
|
||||
objectStorageUseSSLDesc: "Desactiva'l si no tens pensat fer servir HTTPS per les connexions de l'API"
|
||||
objectStorageUseProxy: "Connectar-se mitjançant un Proxy"
|
||||
objectStorageUseProxy: "Connectar-se mitjançant un Proxy"
|
||||
objectStorageUseProxyDesc: "Desactiva'l si no faràs servir un Proxy per les connexions de l'API"
|
||||
objectStorageSetPublicRead: "Configurar les pujades com públiques "
|
||||
s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del dipòsit s'ha d'incloure a l'adreça URL en comtes del nom del host. Potser que necessitis activar-ho quan facis servir, per exemple, Minio a un servidor propi."
|
||||
|
@ -1739,7 +1739,7 @@ _email:
|
|||
_follow:
|
||||
title: "t'ha seguit"
|
||||
_receiveFollowRequest:
|
||||
title: "Has rebut una sol·licitud de seguiment"
|
||||
title: "Has rebut una sol·licitud de seguiment"
|
||||
_plugin:
|
||||
install: "Instal·lar un afegit "
|
||||
installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança."
|
||||
|
@ -1893,8 +1893,6 @@ _sfx:
|
|||
note: "Notes"
|
||||
noteMy: "Nota (per mi)"
|
||||
notification: "Notificacions"
|
||||
antenna: "Antenes"
|
||||
channel: "Notificacions dels canals"
|
||||
reaction: "Quan se selecciona una reacció "
|
||||
_soundSettings:
|
||||
driveFile: "Fer servir un fitxer d'àudio del disc"
|
||||
|
@ -2225,6 +2223,10 @@ _deck:
|
|||
_webhookSettings:
|
||||
name: "Nom"
|
||||
active: "Activat"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Correu electrònic"
|
||||
_moderationLogTypes:
|
||||
suspend: "Suspèn"
|
||||
resetPassword: "Restableix la contrasenya"
|
||||
|
|
|
@ -1645,8 +1645,6 @@ _sfx:
|
|||
note: "Poznámky"
|
||||
noteMy: "Moje poznámka"
|
||||
notification: "Oznámení"
|
||||
antenna: "Antény"
|
||||
channel: "Oznámení kanálu"
|
||||
_ago:
|
||||
future: "Budoucí"
|
||||
justNow: "Teď"
|
||||
|
@ -2012,7 +2010,6 @@ _webhookSettings:
|
|||
createWebhook: "Vytvořit Webhook"
|
||||
name: "Jméno"
|
||||
secret: "Tajné"
|
||||
events: "Události Webhook"
|
||||
active: "Zapnuto"
|
||||
_events:
|
||||
follow: "Při sledování uživatele"
|
||||
|
@ -2022,6 +2019,10 @@ _webhookSettings:
|
|||
renote: "Při renotaci poznámky"
|
||||
reaction: "Při obdržení reakce"
|
||||
mention: "Při zmínce"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
_moderationLogTypes:
|
||||
suspend: "Zmrazit"
|
||||
resetPassword: "Resetovat heslo"
|
||||
|
|
|
@ -1803,8 +1803,6 @@ _sfx:
|
|||
note: "Notizen"
|
||||
noteMy: "Meine Notizen"
|
||||
notification: "Benachrichtigungen"
|
||||
antenna: "Antennen"
|
||||
channel: "Kanalbenachrichtigung"
|
||||
_ago:
|
||||
future: "Zukunft"
|
||||
justNow: "Gerade eben"
|
||||
|
@ -2196,7 +2194,6 @@ _webhookSettings:
|
|||
createWebhook: "Webhook erstellen"
|
||||
name: "Name"
|
||||
secret: "Secret"
|
||||
events: "Webhook-Ereignisse"
|
||||
active: "Aktiviert"
|
||||
_events:
|
||||
follow: "Wenn du jemandem folgst"
|
||||
|
@ -2206,6 +2203,10 @@ _webhookSettings:
|
|||
renote: "Wenn du ein Renote erhältst"
|
||||
reaction: "Wenn du eine Reaktion erhältst"
|
||||
mention: "Wenn du erwähnt wirst"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
_moderationLogTypes:
|
||||
createRole: "Rolle erstellt"
|
||||
deleteRole: "Rolle gelöscht"
|
||||
|
|
|
@ -301,8 +301,6 @@ _theme:
|
|||
_sfx:
|
||||
note: "Σημειώματα"
|
||||
notification: "Ειδοποιήσεις"
|
||||
antenna: "Αντένες"
|
||||
channel: "Ειδοποιήσεις καναλιών"
|
||||
_ago:
|
||||
future: "Μελλοντικό"
|
||||
justNow: "Μόλις τώρα"
|
||||
|
|
|
@ -62,6 +62,7 @@ copyFileId: "Copy file ID"
|
|||
copyFolderId: "Copy folder ID"
|
||||
copyProfileUrl: "Copy profile URL"
|
||||
searchUser: "Search for a user"
|
||||
searchThisUsersNotes: "Search this user’s notes"
|
||||
reply: "Reply"
|
||||
loadMore: "Load more"
|
||||
showMore: "Show more"
|
||||
|
@ -129,8 +130,8 @@ add: "Add"
|
|||
reaction: "Reactions"
|
||||
reactions: "Reactions"
|
||||
emojiPicker: "Emoji picker"
|
||||
pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting."
|
||||
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker"
|
||||
pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting."
|
||||
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker."
|
||||
emojiPickerDisplay: "Emoji picker display"
|
||||
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
|
||||
overwriteFromPinnedEmojis: "Override from general settings"
|
||||
|
@ -162,6 +163,7 @@ editList: "Edit list"
|
|||
selectChannel: "Select a channel"
|
||||
selectAntenna: "Select an antenna"
|
||||
editAntenna: "Edit antenna"
|
||||
createAntenna: "Create an antenna"
|
||||
selectWidget: "Select a widget"
|
||||
editWidgets: "Edit widgets"
|
||||
editWidgetsExit: "Done"
|
||||
|
@ -173,7 +175,7 @@ emojiUrl: "Emoji URL"
|
|||
addEmoji: "Add an emoji"
|
||||
settingGuide: "Recommended settings"
|
||||
cacheRemoteFiles: "Cache remote files"
|
||||
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
|
||||
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
|
||||
youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view."
|
||||
cacheRemoteSensitiveFiles: "Cache sensitive remote files"
|
||||
cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching."
|
||||
|
@ -190,6 +192,10 @@ addAccount: "Add account"
|
|||
reloadAccountsList: "Reload account list"
|
||||
loginFailed: "Failed to sign in"
|
||||
showOnRemote: "View on remote instance"
|
||||
continueOnRemote: "Continue on remote instance"
|
||||
chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub"
|
||||
specifyServerHost: "Specify a server host directly"
|
||||
inputHostName: "Enter the domain"
|
||||
general: "General"
|
||||
wallpaper: "Wallpaper"
|
||||
setWallpaper: "Set wallpaper"
|
||||
|
@ -200,6 +206,7 @@ followConfirm: "Are you sure that you want to follow {name}?"
|
|||
proxyAccount: "Proxy account"
|
||||
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
|
||||
host: "Host"
|
||||
selectSelf: "Select myself"
|
||||
selectUser: "Select a user"
|
||||
recipient: "Recipient"
|
||||
annotation: "Comments"
|
||||
|
@ -215,6 +222,7 @@ perDay: "Per Day"
|
|||
stopActivityDelivery: "Stop sending activities"
|
||||
blockThisInstance: "Block this instance"
|
||||
silenceThisInstance: "Silence this instance"
|
||||
mediaSilenceThisInstance: "Silence media from this instance"
|
||||
operations: "Operations"
|
||||
software: "Software"
|
||||
version: "Version"
|
||||
|
@ -235,7 +243,9 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
|
|||
blockedInstances: "Blocked Instances"
|
||||
blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
|
||||
silencedInstances: "Silenced instances"
|
||||
silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances."
|
||||
silencedInstancesDescription: "List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
|
||||
mediaSilencedInstances: "Media-silenced instances"
|
||||
mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances."
|
||||
muteAndBlock: "Mutes and Blocks"
|
||||
mutedUsers: "Muted users"
|
||||
blockedUsers: "Blocked users"
|
||||
|
@ -400,7 +410,7 @@ mcaptcha: "mCaptcha"
|
|||
enableMcaptcha: "Enable mCaptcha"
|
||||
mcaptchaSiteKey: "Site key"
|
||||
mcaptchaSecretKey: "Secret key"
|
||||
mcaptchaInstanceUrl: "mCaptcha instance URL"
|
||||
mcaptchaInstanceUrl: "mCaptcha server URL"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Enable reCAPTCHA"
|
||||
recaptchaSiteKey: "Site key"
|
||||
|
@ -491,6 +501,7 @@ noMessagesYet: "No messages yet"
|
|||
newMessageExists: "There are new messages"
|
||||
onlyOneFileCanBeAttached: "You can only attach one file to a message"
|
||||
signinRequired: "Please register or sign in before continuing"
|
||||
signinOrContinueOnRemote: "To continue, you need to go to your instance to perform this action or sign up / log in to the instance you are trying to interact with."
|
||||
invitations: "Invites"
|
||||
invitationCode: "Invitation code"
|
||||
checking: "Checking..."
|
||||
|
@ -902,7 +913,7 @@ whatIsNew: "Show changes"
|
|||
translate: "Translate"
|
||||
translatedFrom: "Translated from {x}"
|
||||
accountDeletionInProgress: "Account deletion is currently in progress"
|
||||
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
|
||||
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
|
||||
aiChanMode: "Ai Mode"
|
||||
devMode: "Developer mode"
|
||||
keepCw: "Keep content warnings"
|
||||
|
@ -1154,6 +1165,8 @@ preservedUsernames: "Reserved usernames"
|
|||
preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected."
|
||||
createNoteFromTheFile: "Compose note from this file"
|
||||
archive: "Archive"
|
||||
archived: "Archived"
|
||||
unarchive: "Unarchive"
|
||||
channelArchiveConfirmTitle: "Really archive {name}?"
|
||||
channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore."
|
||||
thisChannelArchived: "This channel has been archived."
|
||||
|
@ -1164,6 +1177,9 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
|
|||
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
|
||||
options: "Options"
|
||||
specifyUser: "Specific user"
|
||||
lookupConfirm: "Are you sure that you want to look this up?"
|
||||
openTagPageConfirm: "Are you sure you want to open this hashtags page?"
|
||||
specifyHost: "Specify a host"
|
||||
failedToPreviewUrl: "Could not preview"
|
||||
update: "Update"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
|
||||
|
@ -1303,6 +1319,11 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will
|
|||
noDescription: "No description"
|
||||
alwaysConfirmFollow: "Always confirm when following"
|
||||
inquiry: "Contact"
|
||||
tryAgain: "Please try again later"
|
||||
confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
|
||||
sensitiveMediaRevealConfirm: "This media might be sensitive. Are you sure you want to reveal it?"
|
||||
createdLists: "Created lists"
|
||||
createdAntennas: "Created antennas"
|
||||
_delivery:
|
||||
status: "Delivery status"
|
||||
stop: "Suspend delivery"
|
||||
|
@ -1757,6 +1778,7 @@ _role:
|
|||
canManageAvatarDecorations: "Manage avatar decorations"
|
||||
driveCapacity: "Drive capacity"
|
||||
alwaysMarkNsfw: "Always mark files as NSFW"
|
||||
canUpdateBioMedia: "Allow users to edit their avatar or banner"
|
||||
pinMax: "Maximum number of pinned notes"
|
||||
antennaMax: "Maximum number of antennas"
|
||||
wordMuteMax: "Maximum number of characters allowed in word mutes"
|
||||
|
@ -2006,8 +2028,6 @@ _sfx:
|
|||
note: "New note"
|
||||
noteMy: "Own note"
|
||||
notification: "Notifications"
|
||||
antenna: "Antennas"
|
||||
channel: "Channel notifications"
|
||||
reaction: "On choosing a reaction"
|
||||
_soundSettings:
|
||||
driveFile: "Use an audio file in Drive."
|
||||
|
@ -2016,6 +2036,7 @@ _soundSettings:
|
|||
driveFileTypeWarnDescription: "Select an audio file"
|
||||
driveFileDurationWarn: "The audio is too long."
|
||||
driveFileDurationWarnDescription: "Long audio may disrupt using Sharkey. Still continue?"
|
||||
driveFileError: "The audio couldn't be loaded. Please make sure you selected an audio file."
|
||||
_ago:
|
||||
future: "Future"
|
||||
justNow: "Just now"
|
||||
|
@ -2133,7 +2154,7 @@ _permissions:
|
|||
"read:admin:invite-codes": "View invite codes"
|
||||
"write:admin:announcements": "Manage announcements"
|
||||
"read:admin:announcements": "View announcements"
|
||||
"write:admin:avatar-decorations": "Manage avatar decorations"
|
||||
"write:admin:avatar-decorations": "Can manage avatar decorations"
|
||||
"read:admin:avatar-decorations": "View avatar decorations"
|
||||
"write:admin:federation": "Manage federation data"
|
||||
"write:admin:account": "Manage user account"
|
||||
|
@ -2321,6 +2342,7 @@ _timelines:
|
|||
local: "Local"
|
||||
social: "Social"
|
||||
global: "Global"
|
||||
bubble: "Bubble"
|
||||
_play:
|
||||
new: "Create Play"
|
||||
edit: "Edit Play"
|
||||
|
@ -2441,7 +2463,7 @@ _deck:
|
|||
alwaysShowMainColumn: "Always show main column"
|
||||
columnAlign: "Align columns"
|
||||
addColumn: "Add column"
|
||||
newNoteNotificationSettings: "New note notification"
|
||||
newNoteNotificationSettings: "Notification setting for new notes"
|
||||
configureColumn: "Column settings"
|
||||
swapLeft: "Swap with the left column"
|
||||
swapRight: "Swap with the right column"
|
||||
|
@ -2480,9 +2502,10 @@ _drivecleaner:
|
|||
orderByCreatedAtAsc: "Ascending Dates"
|
||||
_webhookSettings:
|
||||
createWebhook: "Create Webhook"
|
||||
modifyWebhook: "Modify Webhook"
|
||||
name: "Name"
|
||||
secret: "Secret"
|
||||
events: "Webhook Events"
|
||||
trigger: "Trigger"
|
||||
active: "Enabled"
|
||||
_events:
|
||||
follow: "When following a user"
|
||||
|
@ -2492,6 +2515,26 @@ _webhookSettings:
|
|||
renote: "When boosted"
|
||||
reaction: "When receiving a reaction"
|
||||
mention: "When being mentioned"
|
||||
_systemEvents:
|
||||
abuseReport: "When received a new abuse report"
|
||||
abuseReportResolved: "When resolved abuse reports"
|
||||
userCreated: "When user is created"
|
||||
deleteConfirm: "Are you sure you want to delete the Webhook?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "Add a recipient for abuse reports"
|
||||
modifyRecipient: "Edit a recipient for abuse reports"
|
||||
recipientType: "Notification type"
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "Send an email to the moderators when an abuse report is received."
|
||||
webhook: "Send a notification to the SystemWebhook when an abuse report is received or resolved."
|
||||
keywords: "Keywords"
|
||||
notifiedUser: "Users to notify"
|
||||
notifiedWebhook: "Webhook to use"
|
||||
deleteConfirm: "Are you sure that you want to delete the notification recipient?"
|
||||
_moderationLogTypes:
|
||||
createRole: "Role created"
|
||||
deleteRole: "Role deleted"
|
||||
|
@ -2530,6 +2573,12 @@ _moderationLogTypes:
|
|||
deleteAvatarDecoration: "Avatar decoration deleted"
|
||||
unsetUserAvatar: "Unset this user's avatar"
|
||||
unsetUserBanner: "Unset this user's banner"
|
||||
createSystemWebhook: "Create SystemWebhook"
|
||||
updateSystemWebhook: "Update SystemWebHook"
|
||||
deleteSystemWebhook: "Delete SystemWebhook"
|
||||
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
|
||||
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
|
||||
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
|
||||
_mfm:
|
||||
uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
|
||||
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
|
||||
|
@ -2610,6 +2659,7 @@ _mfm:
|
|||
backgroundDescription: "Change the background color of text."
|
||||
plain: "Plain"
|
||||
plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
|
||||
|
||||
_fileViewer:
|
||||
title: "File details"
|
||||
type: "File type"
|
||||
|
@ -2679,7 +2729,7 @@ _dataSaver:
|
|||
description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped."
|
||||
_avatar:
|
||||
title: "Avatar image"
|
||||
description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic."
|
||||
description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic."
|
||||
_urlPreview:
|
||||
title: "URL preview thumbnails"
|
||||
description: "URL preview thumbnail images will no longer be loaded."
|
||||
|
@ -2755,3 +2805,8 @@ _mediaControls:
|
|||
pip: "Picture in Picture"
|
||||
playbackRate: "Playback Speed"
|
||||
loop: "Loop playback"
|
||||
_contextMenu:
|
||||
title: "Context menu"
|
||||
app: "Application"
|
||||
appWithShift: "Application with shift key"
|
||||
native: "Native"
|
||||
|
|
|
@ -302,7 +302,7 @@ location: "Lugar"
|
|||
theme: "Tema"
|
||||
themeForLightMode: "Tema para usar en Modo Linterna"
|
||||
themeForDarkMode: "Tema para usar en Modo Oscuro"
|
||||
light: "Linterna"
|
||||
light: "Claro"
|
||||
dark: "Oscuro"
|
||||
lightThemes: "Tema claro"
|
||||
darkThemes: "Tema oscuro"
|
||||
|
@ -867,7 +867,7 @@ whatIsNew: "Mostrar cambios"
|
|||
translate: "Traducir"
|
||||
translatedFrom: "Traducido de {x}"
|
||||
accountDeletionInProgress: "La eliminación de la cuenta está en curso"
|
||||
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
|
||||
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
|
||||
aiChanMode: "Modo Ai"
|
||||
devMode: "Modo de desarrollador"
|
||||
keepCw: "Mantener la advertencia de contenido"
|
||||
|
@ -1920,8 +1920,6 @@ _sfx:
|
|||
note: "Notas"
|
||||
noteMy: "Nota (a mí mismo)"
|
||||
notification: "Notificaciones"
|
||||
antenna: "Antena receptora"
|
||||
channel: "Notificaciones del canal"
|
||||
reaction: "Al seleccionar una reacción"
|
||||
_soundSettings:
|
||||
driveFile: "Usar un archivo de audio en Drive"
|
||||
|
@ -2356,7 +2354,7 @@ _deck:
|
|||
newProfile: "Nuevo perfil"
|
||||
deleteProfile: "Eliminar perfil"
|
||||
introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!"
|
||||
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
|
||||
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
|
||||
widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget."
|
||||
useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple"
|
||||
usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada"
|
||||
|
@ -2385,7 +2383,6 @@ _webhookSettings:
|
|||
createWebhook: "Crear Webhook"
|
||||
name: "Nombre"
|
||||
secret: "Secreto"
|
||||
events: "Eventos de webhook"
|
||||
active: "Activado"
|
||||
_events:
|
||||
follow: "Cuando se sigue a alguien"
|
||||
|
@ -2395,6 +2392,10 @@ _webhookSettings:
|
|||
renote: "Cuando reciba un \"re-note\""
|
||||
reaction: "Cuando se recibe una reacción"
|
||||
mention: "Cuando hay una mención"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Correo"
|
||||
_moderationLogTypes:
|
||||
createRole: "Rol creado"
|
||||
deleteRole: "Rol eliminado"
|
||||
|
|
|
@ -1245,7 +1245,7 @@ _announcement:
|
|||
end: "Archiver l'annonce"
|
||||
tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes."
|
||||
readConfirmTitle: "Marquer comme lu ?"
|
||||
readConfirmText: "Cela marquera le contenu de « {title} » comme lu."
|
||||
readConfirmText: "Cela marquera le contenu de « {title} » comme lu."
|
||||
shouldNotBeUsedToPresentPermanentInfo: "Puisque cela pourrait nuire considérablement à l'expérience utilisateur pour les nouveaux utilisateurs, il est recommandé d'utiliser les annonces pour afficher des informations temporaires plutôt que des informations persistantes."
|
||||
dialogAnnouncementUxWarn: "Avoir deux ou plus annonces de style dialogue en même temps pourrait nuire considérablement à l'expérience utilisateur. Veuillez les utiliser avec caution."
|
||||
silence: "Ne pas me notifier"
|
||||
|
@ -1712,8 +1712,6 @@ _sfx:
|
|||
note: "Nouvelle note"
|
||||
noteMy: "Ma note"
|
||||
notification: "Notifications"
|
||||
antenna: "Réception de l’antenne"
|
||||
channel: "Notifications de canal"
|
||||
reaction: "Lors de la sélection de la réaction"
|
||||
_soundSettings:
|
||||
driveFile: "Utiliser un effet sonore sur le Disque"
|
||||
|
@ -2073,6 +2071,10 @@ _drivecleaner:
|
|||
_webhookSettings:
|
||||
name: "Nom"
|
||||
active: "Activé"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "E-mail "
|
||||
_moderationLogTypes:
|
||||
createRole: "Rôle créé"
|
||||
deleteRole: "Rôle supprimé"
|
||||
|
|
|
@ -180,6 +180,10 @@ addAccount: "Tambahkan akun"
|
|||
reloadAccountsList: "Muat ulang daftar akun"
|
||||
loginFailed: "Gagal untuk masuk"
|
||||
showOnRemote: "Lihat profil asli"
|
||||
continueOnRemote: "Lihat di peladen asal"
|
||||
chooseServerOnMisskeyHub: "Pilih peladen dari Misskey Hub"
|
||||
specifyServerHost: "Tentukan domain peladen"
|
||||
inputHostName: "Masukkan nama domain"
|
||||
general: "Umum"
|
||||
wallpaper: "Wallpaper"
|
||||
setWallpaper: "Atur wallpaper"
|
||||
|
@ -316,6 +320,7 @@ selectFile: "Pilih berkas"
|
|||
selectFiles: "Pilih berkas"
|
||||
selectFolder: "Pilih folder"
|
||||
selectFolders: "Pilih folder"
|
||||
fileNotSelected: "Tidak ada file yang dipilih"
|
||||
renameFile: "Ubah nama berkas"
|
||||
folderName: "Nama folder"
|
||||
createFolder: "Buat folder"
|
||||
|
@ -579,7 +584,7 @@ sort: "Urutkan"
|
|||
ascendingOrder: "Urutkan naik"
|
||||
descendingOrder: "Urutkan menurun"
|
||||
scratchpad: "Scratchpad"
|
||||
scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScript. Kamu bisa menulis, mengeksuksi, serta mengecek hasil yang berinteraksi dengan Misskey."
|
||||
scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScript. Kamu bisa menulis, mengeksuksi, serta mengecek hasil yang berinteraksi dengan Misskey."
|
||||
output: "Keluaran"
|
||||
script: "Script"
|
||||
disablePagesScript: "Nonaktifkan script pada halaman"
|
||||
|
@ -1239,6 +1244,7 @@ keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas
|
|||
noDescription: "Tidak ada deskripsi"
|
||||
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
|
||||
inquiry: "Hubungi kami"
|
||||
tryAgain: "Silahkan coba lagi."
|
||||
_delivery:
|
||||
status: "Status pengiriman"
|
||||
stop: "Ditangguhkan"
|
||||
|
@ -1739,7 +1745,7 @@ _emailUnavailable:
|
|||
smtp: "Peladen alamat surel ini tidak merespon"
|
||||
banned: "Kamu tidak dapat mendaftar dengan alamat surel ini"
|
||||
_ffVisibility:
|
||||
public: "Terbitkan"
|
||||
public: "Publik"
|
||||
followers: "Tampil untuk pengikut saja"
|
||||
private: "Tersembunyi"
|
||||
_signup:
|
||||
|
@ -1932,8 +1938,6 @@ _sfx:
|
|||
note: "Catatan"
|
||||
noteMy: "Catatan (Saya)"
|
||||
notification: "Notifikasi"
|
||||
antenna: "Penerimaan Antenna"
|
||||
channel: "Notifikasi Kanal"
|
||||
reaction: "Ketika memilih reaksi"
|
||||
_soundSettings:
|
||||
driveFile: "Menggunakan berkas audio dalam Drive"
|
||||
|
@ -2396,9 +2400,9 @@ _drivecleaner:
|
|||
orderByCreatedAtAsc: "Tanggal (Naik)"
|
||||
_webhookSettings:
|
||||
createWebhook: "Buat Webhook"
|
||||
modifyWebhook: "Sunting Webhook"
|
||||
name: "Nama"
|
||||
secret: "Secret"
|
||||
events: "Webhook Events"
|
||||
active: "Aktif"
|
||||
_events:
|
||||
follow: "Ketika mengikuti pengguna"
|
||||
|
@ -2408,6 +2412,11 @@ _webhookSettings:
|
|||
renote: "Ketika direnote"
|
||||
reaction: "Ketika menerima reaksi"
|
||||
mention: "Ketika sedang disebut"
|
||||
deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Surel"
|
||||
_moderationLogTypes:
|
||||
createRole: "Peran telah dibuat"
|
||||
deleteRole: "Peran telah dihapus"
|
||||
|
|
242
locales/index.d.ts
vendored
242
locales/index.d.ts
vendored
|
@ -264,6 +264,10 @@ export interface Locale extends ILocale {
|
|||
* ユーザーを検索
|
||||
*/
|
||||
"searchUser": string;
|
||||
/**
|
||||
* ユーザーのノートを検索
|
||||
*/
|
||||
"searchThisUsersNotes": string;
|
||||
/**
|
||||
* 返信
|
||||
*/
|
||||
|
@ -664,6 +668,10 @@ export interface Locale extends ILocale {
|
|||
* アンテナを編集
|
||||
*/
|
||||
"editAntenna": string;
|
||||
/**
|
||||
* アンテナを作成
|
||||
*/
|
||||
"createAntenna": string;
|
||||
/**
|
||||
* ウィジェットを選択
|
||||
*/
|
||||
|
@ -776,6 +784,22 @@ export interface Locale extends ILocale {
|
|||
* リモートで表示
|
||||
*/
|
||||
"showOnRemote": string;
|
||||
/**
|
||||
* リモートで続行
|
||||
*/
|
||||
"continueOnRemote": string;
|
||||
/**
|
||||
* Misskey Hubからサーバーを選択
|
||||
*/
|
||||
"chooseServerOnMisskeyHub": string;
|
||||
/**
|
||||
* サーバーのドメインを直接指定
|
||||
*/
|
||||
"specifyServerHost": string;
|
||||
/**
|
||||
* ドメインを入力してください
|
||||
*/
|
||||
"inputHostName": string;
|
||||
/**
|
||||
* 全般
|
||||
*/
|
||||
|
@ -816,6 +840,10 @@ export interface Locale extends ILocale {
|
|||
* ホスト
|
||||
*/
|
||||
"host": string;
|
||||
/**
|
||||
* 自分を選択
|
||||
*/
|
||||
"selectSelf": string;
|
||||
/**
|
||||
* ユーザーを選択
|
||||
*/
|
||||
|
@ -869,13 +897,17 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"stopActivityDelivery": string;
|
||||
/**
|
||||
* このインスタンスをブロック
|
||||
* このサーバーをブロック
|
||||
*/
|
||||
"blockThisInstance": string;
|
||||
/**
|
||||
* インスタンスをサイレンス
|
||||
* サーバーをサイレンス
|
||||
*/
|
||||
"silenceThisInstance": string;
|
||||
/**
|
||||
* サーバーをメディアサイレンス
|
||||
*/
|
||||
"mediaSilenceThisInstance": string;
|
||||
/**
|
||||
* 操作
|
||||
*/
|
||||
|
@ -960,6 +992,14 @@ export interface Locale extends ILocale {
|
|||
* サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。
|
||||
*/
|
||||
"silencedInstancesDescription": string;
|
||||
/**
|
||||
* メディアサイレンスしたサーバー
|
||||
*/
|
||||
"mediaSilencedInstances": string;
|
||||
/**
|
||||
* メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
|
||||
*/
|
||||
"mediaSilencedInstancesDescription": string;
|
||||
/**
|
||||
* ミュートとブロック
|
||||
*/
|
||||
|
@ -1977,9 +2017,13 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"onlyOneFileCanBeAttached": string;
|
||||
/**
|
||||
* 続行する前に、サインアップまたはサインインが必要です
|
||||
* 続行する前に、登録またはログインが必要です
|
||||
*/
|
||||
"signinRequired": string;
|
||||
/**
|
||||
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
|
||||
*/
|
||||
"signinOrContinueOnRemote": string;
|
||||
/**
|
||||
* 招待
|
||||
*/
|
||||
|
@ -2144,6 +2188,10 @@ export interface Locale extends ILocale {
|
|||
* 画像を新しいタブで開く
|
||||
*/
|
||||
"openImageInNewTab": string;
|
||||
/**
|
||||
* 代替テキストを入れ忘れたときに警告する
|
||||
*/
|
||||
"warnForMissingAltText": string;
|
||||
/**
|
||||
* ダッシュボード
|
||||
*/
|
||||
|
@ -4629,6 +4677,14 @@ export interface Locale extends ILocale {
|
|||
* アーカイブ
|
||||
*/
|
||||
"archive": string;
|
||||
/**
|
||||
* アーカイブ済み
|
||||
*/
|
||||
"archived": string;
|
||||
/**
|
||||
* アーカイブ解除
|
||||
*/
|
||||
"unarchive": string;
|
||||
/**
|
||||
* {name}をアーカイブしますか?
|
||||
*/
|
||||
|
@ -4669,6 +4725,18 @@ export interface Locale extends ILocale {
|
|||
* ユーザー指定
|
||||
*/
|
||||
"specifyUser": string;
|
||||
/**
|
||||
* 照会しますか?
|
||||
*/
|
||||
"lookupConfirm": string;
|
||||
/**
|
||||
* ハッシュタグのページを開きますか?
|
||||
*/
|
||||
"openTagPageConfirm": string;
|
||||
/**
|
||||
* ホスト指定
|
||||
*/
|
||||
"specifyHost": string;
|
||||
/**
|
||||
* プレビューできません
|
||||
*/
|
||||
|
@ -5225,6 +5293,26 @@ export interface Locale extends ILocale {
|
|||
* お問い合わせ
|
||||
*/
|
||||
"inquiry": string;
|
||||
/**
|
||||
* もう一度お試しください。
|
||||
*/
|
||||
"tryAgain": string;
|
||||
/**
|
||||
* センシティブなメディアを表示するとき確認する
|
||||
*/
|
||||
"confirmWhenRevealingSensitiveMedia": string;
|
||||
/**
|
||||
* センシティブなメディアです。表示しますか?
|
||||
*/
|
||||
"sensitiveMediaRevealConfirm": string;
|
||||
/**
|
||||
* 作成したリスト
|
||||
*/
|
||||
"createdLists": string;
|
||||
/**
|
||||
* 作成したアンテナ
|
||||
*/
|
||||
"createdAntennas": string;
|
||||
"_delivery": {
|
||||
/**
|
||||
* 配信状態
|
||||
|
@ -6843,6 +6931,10 @@ export interface Locale extends ILocale {
|
|||
* ファイルにNSFWを常に付与
|
||||
*/
|
||||
"alwaysMarkNsfw": string;
|
||||
/**
|
||||
* アイコンとバナーの更新を許可
|
||||
*/
|
||||
"canUpdateBioMedia": string;
|
||||
/**
|
||||
* ノートのピン留めの最大数
|
||||
*/
|
||||
|
@ -7788,14 +7880,6 @@ export interface Locale extends ILocale {
|
|||
* 通知
|
||||
*/
|
||||
"notification": string;
|
||||
/**
|
||||
* アンテナ受信
|
||||
*/
|
||||
"antenna": string;
|
||||
/**
|
||||
* チャンネル通知
|
||||
*/
|
||||
"channel": string;
|
||||
/**
|
||||
* リアクション選択時
|
||||
*/
|
||||
|
@ -7826,6 +7910,10 @@ export interface Locale extends ILocale {
|
|||
* 長い音声を使用するとSharkeyの使用に支障をきたす可能性があります。それでも続行しますか?
|
||||
*/
|
||||
"driveFileDurationWarnDescription": string;
|
||||
/**
|
||||
* 音声が読み込めませんでした。設定を変更してください
|
||||
*/
|
||||
"driveFileError": string;
|
||||
};
|
||||
"_ago": {
|
||||
/**
|
||||
|
@ -9006,6 +9094,10 @@ export interface Locale extends ILocale {
|
|||
* グローバル
|
||||
*/
|
||||
"global": string;
|
||||
/**
|
||||
* バッッブル
|
||||
*/
|
||||
"bubble": string;
|
||||
};
|
||||
"_play": {
|
||||
/**
|
||||
|
@ -9614,6 +9706,10 @@ export interface Locale extends ILocale {
|
|||
* Webhookを作成
|
||||
*/
|
||||
"createWebhook": string;
|
||||
/**
|
||||
* Webhookを編集
|
||||
*/
|
||||
"modifyWebhook": string;
|
||||
/**
|
||||
* 名前
|
||||
*/
|
||||
|
@ -9623,9 +9719,9 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"secret": string;
|
||||
/**
|
||||
* Webhookを実行するタイミング
|
||||
* トリガー
|
||||
*/
|
||||
"events": string;
|
||||
"trigger": string;
|
||||
/**
|
||||
* 有効
|
||||
*/
|
||||
|
@ -9660,6 +9756,76 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"mention": string;
|
||||
};
|
||||
"_systemEvents": {
|
||||
/**
|
||||
* ユーザーから通報があったとき
|
||||
*/
|
||||
"abuseReport": string;
|
||||
/**
|
||||
* ユーザーからの通報を処理したとき
|
||||
*/
|
||||
"abuseReportResolved": string;
|
||||
/**
|
||||
* ユーザーが作成されたとき
|
||||
*/
|
||||
"userCreated": string;
|
||||
};
|
||||
/**
|
||||
* Webhookを削除しますか?
|
||||
*/
|
||||
"deleteConfirm": string;
|
||||
};
|
||||
"_abuseReport": {
|
||||
"_notificationRecipient": {
|
||||
/**
|
||||
* 通報の通知先を追加
|
||||
*/
|
||||
"createRecipient": string;
|
||||
/**
|
||||
* 通報の通知先を編集
|
||||
*/
|
||||
"modifyRecipient": string;
|
||||
/**
|
||||
* 通知先の種類
|
||||
*/
|
||||
"recipientType": string;
|
||||
"_recipientType": {
|
||||
/**
|
||||
* メール
|
||||
*/
|
||||
"mail": string;
|
||||
/**
|
||||
* Webhook
|
||||
*/
|
||||
"webhook": string;
|
||||
"_captions": {
|
||||
/**
|
||||
* モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
|
||||
*/
|
||||
"mail": string;
|
||||
/**
|
||||
* 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
|
||||
*/
|
||||
"webhook": string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* キーワード
|
||||
*/
|
||||
"keywords": string;
|
||||
/**
|
||||
* 通知先ユーザー
|
||||
*/
|
||||
"notifiedUser": string;
|
||||
/**
|
||||
* 使用するWebhook
|
||||
*/
|
||||
"notifiedWebhook": string;
|
||||
/**
|
||||
* 通知先を削除しますか?
|
||||
*/
|
||||
"deleteConfirm": string;
|
||||
};
|
||||
};
|
||||
"_moderationLogTypes": {
|
||||
/**
|
||||
|
@ -9810,6 +9976,30 @@ export interface Locale extends ILocale {
|
|||
* ユーザーのバナーを解除
|
||||
*/
|
||||
"unsetUserBanner": string;
|
||||
/**
|
||||
* SystemWebhookを作成
|
||||
*/
|
||||
"createSystemWebhook": string;
|
||||
/**
|
||||
* SystemWebhookを更新
|
||||
*/
|
||||
"updateSystemWebhook": string;
|
||||
/**
|
||||
* SystemWebhookを削除
|
||||
*/
|
||||
"deleteSystemWebhook": string;
|
||||
/**
|
||||
* 通報の通知先を作成
|
||||
*/
|
||||
"createAbuseReportNotificationRecipient": string;
|
||||
/**
|
||||
* 通報の通知先を更新
|
||||
*/
|
||||
"updateAbuseReportNotificationRecipient": string;
|
||||
/**
|
||||
* 通報の通知先を削除
|
||||
*/
|
||||
"deleteAbuseReportNotificationRecipient": string;
|
||||
};
|
||||
"_mfm": {
|
||||
/**
|
||||
|
@ -10336,7 +10526,7 @@ export interface Locale extends ILocale {
|
|||
"_dataSaver": {
|
||||
"_media": {
|
||||
/**
|
||||
* メディアの読み込み
|
||||
* メディアの読み込みを無効化
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
|
@ -10346,7 +10536,7 @@ export interface Locale extends ILocale {
|
|||
};
|
||||
"_avatar": {
|
||||
/**
|
||||
* アイコン画像
|
||||
* アイコン画像のアニメーションを無効化
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
|
@ -10356,7 +10546,7 @@ export interface Locale extends ILocale {
|
|||
};
|
||||
"_urlPreview": {
|
||||
/**
|
||||
* URLプレビューのサムネイル
|
||||
* URLプレビューのサムネイルを非表示
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
|
@ -10366,7 +10556,7 @@ export interface Locale extends ILocale {
|
|||
};
|
||||
"_code": {
|
||||
/**
|
||||
* コードハイライト
|
||||
* コードハイライトを非表示
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
|
@ -10641,6 +10831,24 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"loop": string;
|
||||
};
|
||||
"_contextMenu": {
|
||||
/**
|
||||
* コンテキストメニュー
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* アプリケーション
|
||||
*/
|
||||
"app": string;
|
||||
/**
|
||||
* Shiftキーでアプリケーション
|
||||
*/
|
||||
"appWithShift": string;
|
||||
/**
|
||||
* ブラウザのUI
|
||||
*/
|
||||
"native": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -56,7 +56,11 @@ const primaries = {
|
|||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '').replaceAll(new RegExp(/\\+\{/,'g'), '{');
|
||||
|
||||
export function build() {
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
||||
// vitestの挙動を調整するため、一度ローカル変数化する必要がある
|
||||
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
|
||||
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
|
||||
const metaUrl = import.meta.url;
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
|
||||
|
||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||
const removeEmpty = (obj) => {
|
||||
|
|
|
@ -109,11 +109,14 @@ enterEmoji: "Inserisci emoji"
|
|||
renote: "Rinota"
|
||||
unrenote: "Elimina la Rinota"
|
||||
renoted: "Rinotata!"
|
||||
renotedToX: "Rinota da {name}."
|
||||
cantRenote: "È impossibile rinotare questa nota."
|
||||
cantReRenote: "È impossibile rinotare una Rinota."
|
||||
quote: "Citazione"
|
||||
inChannelRenote: "Rinota nel canale"
|
||||
inChannelQuote: "Cita nel canale"
|
||||
renoteToChannel: "Rinota al canale"
|
||||
renoteToOtherChannel: "Rinota a un altro canale"
|
||||
pinnedNote: "Nota in primo piano"
|
||||
pinned: "Fissa sul profilo"
|
||||
you: "Tu"
|
||||
|
@ -178,6 +181,10 @@ addAccount: "Aggiungi profilo"
|
|||
reloadAccountsList: "Ricarica l'elenco dei profili"
|
||||
loginFailed: "Accesso non riuscito"
|
||||
showOnRemote: "Leggi sull'istanza remota"
|
||||
continueOnRemote: "Continua da remoto"
|
||||
chooseServerOnMisskeyHub: "Scegli l'istanza sul sito Misskey Hub"
|
||||
specifyServerHost: "Indica l'indirizzo dell'istanza"
|
||||
inputHostName: "Digita il nome del dominio "
|
||||
general: "Generali"
|
||||
wallpaper: "Sfondo"
|
||||
setWallpaper: "Imposta sfondo"
|
||||
|
@ -314,6 +321,7 @@ selectFile: "Scelta allegato"
|
|||
selectFiles: "Scelta allegato"
|
||||
selectFolder: "Seleziona cartella"
|
||||
selectFolders: "Seleziona cartella"
|
||||
fileNotSelected: "Nessun file selezionato"
|
||||
renameFile: "Rinomina file"
|
||||
folderName: "Nome della cartella"
|
||||
createFolder: "Nuova cartella"
|
||||
|
@ -469,10 +477,12 @@ retype: "Conferma"
|
|||
noteOf: "Note di {user}"
|
||||
quoteAttached: "Citazione allegata"
|
||||
quoteQuestion: "Vuoi aggiungere una citazione?"
|
||||
attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?"
|
||||
noMessagesYet: "Ancora nessuna chat"
|
||||
newMessageExists: "Hai ricevuto un nuovo messaggio"
|
||||
onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file"
|
||||
signinRequired: "Occorre avere un profilo registrato su questa istanza"
|
||||
signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere"
|
||||
invitations: "Invita"
|
||||
invitationCode: "Codice di invito"
|
||||
checking: "Confermando"
|
||||
|
@ -696,7 +706,7 @@ reporterOrigin: "Segnalazione da"
|
|||
forwardReport: "Inoltro di un report a un'istanza remota."
|
||||
forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo."
|
||||
send: "Inviare"
|
||||
abuseMarkAsResolved: "Contrassegna la segnalazione come risolta"
|
||||
abuseMarkAsResolved: "Risolvi segnalazione"
|
||||
openInNewTab: "Apri in una nuova scheda"
|
||||
openInSideView: "Apri in vista laterale"
|
||||
defaultNavigationBehaviour: "Navigazione preimpostata"
|
||||
|
@ -835,6 +845,7 @@ administration: "Gestione"
|
|||
accounts: "Profilo"
|
||||
switch: "Cambia"
|
||||
noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore."
|
||||
noInquiryUrlWarning: "Non è stata impostata la URL di contatto"
|
||||
noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot"
|
||||
configure: "Imposta"
|
||||
postToGallery: "Pubblicare nella galleria"
|
||||
|
@ -1025,6 +1036,7 @@ thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
|
|||
thisPostMayBeAnnoyingCancel: "Annulla"
|
||||
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
|
||||
collapseRenotes: "Comprimi le Rinota già viste"
|
||||
collapseRenotesDescription: "Comprimi le Note con cui hai già interagito."
|
||||
internalServerError: "Errore interno del server"
|
||||
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
|
||||
copyErrorInfo: "Copia le informazioni sull'errore"
|
||||
|
@ -1237,11 +1249,20 @@ useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità
|
|||
keepOriginalFilename: "Mantieni il nome file originale"
|
||||
keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali."
|
||||
noDescription: "Manca la descrizione"
|
||||
alwaysConfirmFollow: "Richiedi conferma per i Follow"
|
||||
inquiry: "Contattaci"
|
||||
tryAgain: "Per favore riprova"
|
||||
confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti"
|
||||
sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?"
|
||||
_delivery:
|
||||
status: "Stato della distribuzione di attività"
|
||||
stop: "Sospendi la distribuzione di attività"
|
||||
resume: "Riprendi la distribuzione di attività"
|
||||
_type:
|
||||
none: "Pubblicazione"
|
||||
manuallySuspended: "Sospesa manualmente"
|
||||
goneSuspended: "Sospensione server a causa dell'eliminazione"
|
||||
autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta"
|
||||
_bubbleGame:
|
||||
howToPlay: "Come giocare"
|
||||
hold: "Tieni"
|
||||
|
@ -1367,6 +1388,8 @@ _serverSettings:
|
|||
fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare."
|
||||
fanoutTimelineDbFallback: "Elaborazione dati alternativa"
|
||||
fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline."
|
||||
inquiryUrl: "URL di contatto"
|
||||
inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
|
||||
_accountMigration:
|
||||
moveFrom: "Migra un altro profilo dentro a questo"
|
||||
moveFromSub: "Crea un alias verso un altro profilo remoto"
|
||||
|
@ -1683,6 +1706,7 @@ _role:
|
|||
canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
|
||||
driveCapacity: "Capienza del Drive"
|
||||
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
|
||||
canUpdateBioMedia: "Può aggiornare foto profilo e di testata"
|
||||
pinMax: "Quantità massima di Note in primo piano"
|
||||
antennaMax: "Quantità massima di Antenne"
|
||||
wordMuteMax: "Lunghezza massima del filtro parole"
|
||||
|
@ -1701,6 +1725,11 @@ _role:
|
|||
roleAssignedTo: "Assegnato a ruoli manualmente"
|
||||
isLocal: "Profilo locale"
|
||||
isRemote: "Profilo remoto"
|
||||
isCat: "È un gattino"
|
||||
isBot: "È un bot"
|
||||
isSuspended: "È sospeso"
|
||||
isLocked: "È in stato privato"
|
||||
isExplorable: "Autorizza la pubblicazione nei cataloghi"
|
||||
createdLessThan: "Profilo creato da meno di N"
|
||||
createdMoreThan: "Profilo creato da più di N"
|
||||
followersLessThanOrEq: "Profilo con N follower o meno"
|
||||
|
@ -1754,7 +1783,7 @@ _ad:
|
|||
_forgotPassword:
|
||||
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
|
||||
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
|
||||
contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
|
||||
contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
|
||||
_gallery:
|
||||
my: "Le mie pubblicazioni"
|
||||
liked: "Pubblicazioni che mi piacciono"
|
||||
|
@ -1922,8 +1951,6 @@ _sfx:
|
|||
note: "Nota"
|
||||
noteMy: "Mia nota"
|
||||
notification: "Notifiche"
|
||||
antenna: "Ricezione dell'antenna"
|
||||
channel: "Notifiche di canale"
|
||||
reaction: "Quando seleziono una reazione"
|
||||
_soundSettings:
|
||||
driveFile: "Suoni del Drive"
|
||||
|
@ -2348,6 +2375,7 @@ _deck:
|
|||
alwaysShowMainColumn: "Mostra sempre la colonna principale"
|
||||
columnAlign: "Allineare colonne"
|
||||
addColumn: "Aggiungi colonna"
|
||||
newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note"
|
||||
configureColumn: "Impostazioni colonna"
|
||||
swapLeft: "Sposta a sinistra"
|
||||
swapRight: "Sposta a destra"
|
||||
|
@ -2377,7 +2405,7 @@ _deck:
|
|||
roleTimeline: "Timeline Ruolo"
|
||||
_dialog:
|
||||
charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})"
|
||||
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})"
|
||||
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})"
|
||||
_disabledTimeline:
|
||||
title: "Timeline disabilitata"
|
||||
description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
|
||||
|
@ -2386,9 +2414,9 @@ _drivecleaner:
|
|||
orderByCreatedAtAsc: "Dal più vecchio al più recente"
|
||||
_webhookSettings:
|
||||
createWebhook: "Creazione Webhook"
|
||||
modifyWebhook: "Modifica Webhook"
|
||||
name: "Nome"
|
||||
secret: "Segreto"
|
||||
events: "Quando eseguire il Webhook"
|
||||
active: "Attivo"
|
||||
_events:
|
||||
follow: "Quando segui un profilo"
|
||||
|
@ -2398,6 +2426,25 @@ _webhookSettings:
|
|||
renote: "Quando la Nota è Rinotata"
|
||||
reaction: "Quando ricevo una reazione"
|
||||
mention: "Quando mi menzionano"
|
||||
_systemEvents:
|
||||
abuseReport: "Quando arriva una segnalazione"
|
||||
abuseReportResolved: "Quando una segnalazione è risolta"
|
||||
deleteConfirm: "Vuoi davvero eliminare il Webhook?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "Aggiungi destinatario della segnalazione"
|
||||
modifyRecipient: "Modifica destinatario della segnalazione"
|
||||
recipientType: "Tipo di notifica"
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "Quando ricevi un abuso, notifica l'amministrazione via email"
|
||||
webhook: "Spedire una notifica al SystemWebhook specificato (sia quando si riceve una segnalazione, che quando viene risolta)"
|
||||
keywords: "Parole chiave"
|
||||
notifiedUser: "Profili da notificare"
|
||||
notifiedWebhook: "Webhook da usare"
|
||||
deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?"
|
||||
_moderationLogTypes:
|
||||
createRole: "Ruolo creato"
|
||||
deleteRole: "Ruolo eliminato"
|
||||
|
@ -2435,6 +2482,12 @@ _moderationLogTypes:
|
|||
deleteAvatarDecoration: "Eliminazione decorazione della foto profilo"
|
||||
unsetUserAvatar: "Rimossa foto profilo"
|
||||
unsetUserBanner: "Rimossa intestazione profilo"
|
||||
createSystemWebhook: "Crea un SystemWebhook"
|
||||
updateSystemWebhook: "Modifica SystemWebhook"
|
||||
deleteSystemWebhook: "Elimina SystemWebhook"
|
||||
createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni"
|
||||
updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni"
|
||||
deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni"
|
||||
_fileViewer:
|
||||
title: "Dettagli del file"
|
||||
type: "Tipo di file"
|
||||
|
@ -2560,6 +2613,8 @@ _urlPreviewSetting:
|
|||
userAgent: "User-Agent"
|
||||
userAgentDescription: "Definire con quale User-Agent si intende identificarsi durante l'acquisizione di un'anteprima. Se è vuoto, useremo il valore predefinito."
|
||||
summaryProxy: "Endpoint proxy che genera l'anteprima"
|
||||
summaryProxyDescription: "Genera anteprime utilizzando un proxy Summaly anziché Misskey."
|
||||
summaryProxyDescription2: "I parametri sono collegano al proxy come stringa query. Se il proxy non li supporta, verranno ignorati."
|
||||
_mediaControls:
|
||||
pip: "Sovraimpressione"
|
||||
playbackRate: "Velocità di riproduzione"
|
||||
|
|
|
@ -62,6 +62,7 @@ copyFileId: "ファイルIDをコピー"
|
|||
copyFolderId: "フォルダーIDをコピー"
|
||||
copyProfileUrl: "プロフィールURLをコピー"
|
||||
searchUser: "ユーザーを検索"
|
||||
searchThisUsersNotes: "ユーザーのノートを検索"
|
||||
reply: "返信"
|
||||
loadMore: "もっと見る"
|
||||
showMore: "もっと見る"
|
||||
|
@ -162,6 +163,7 @@ editList: "リストを編集"
|
|||
selectChannel: "チャンネルを選択"
|
||||
selectAntenna: "アンテナを選択"
|
||||
editAntenna: "アンテナを編集"
|
||||
createAntenna: "アンテナを作成"
|
||||
selectWidget: "ウィジェットを選択"
|
||||
editWidgets: "ウィジェットを編集"
|
||||
editWidgetsExit: "編集を終了"
|
||||
|
@ -190,6 +192,10 @@ addAccount: "アカウントを追加"
|
|||
reloadAccountsList: "アカウントリストの情報を更新"
|
||||
loginFailed: "ログインに失敗しました"
|
||||
showOnRemote: "リモートで表示"
|
||||
continueOnRemote: "リモートで続行"
|
||||
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
|
||||
specifyServerHost: "サーバーのドメインを直接指定"
|
||||
inputHostName: "ドメインを入力してください"
|
||||
general: "全般"
|
||||
wallpaper: "壁紙"
|
||||
setWallpaper: "壁紙を設定"
|
||||
|
@ -200,6 +206,7 @@ followConfirm: "{name}をフォローしますか?"
|
|||
proxyAccount: "プロキシアカウント"
|
||||
proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
|
||||
host: "ホスト"
|
||||
selectSelf: "自分を選択"
|
||||
selectUser: "ユーザーを選択"
|
||||
recipient: "宛先"
|
||||
annotation: "注釈"
|
||||
|
@ -213,8 +220,9 @@ charts: "チャート"
|
|||
perHour: "1時間ごと"
|
||||
perDay: "1日ごと"
|
||||
stopActivityDelivery: "アクティビティの配送を停止"
|
||||
blockThisInstance: "このインスタンスをブロック"
|
||||
silenceThisInstance: "インスタンスをサイレンス"
|
||||
blockThisInstance: "このサーバーをブロック"
|
||||
silenceThisInstance: "サーバーをサイレンス"
|
||||
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
|
||||
operations: "操作"
|
||||
software: "ソフトウェア"
|
||||
version: "バージョン"
|
||||
|
@ -236,6 +244,8 @@ blockedInstances: "ブロックしたサーバー"
|
|||
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
|
||||
silencedInstances: "サイレンスしたサーバー"
|
||||
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
|
||||
mediaSilencedInstances: "メディアサイレンスしたサーバー"
|
||||
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
|
@ -490,7 +500,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
|
|||
noMessagesYet: "まだチャットはありません"
|
||||
newMessageExists: "新しいメッセージがあります"
|
||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
||||
signinRequired: "続行する前に、登録またはログインが必要です"
|
||||
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
|
||||
invitations: "招待"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しています"
|
||||
|
@ -1154,6 +1165,8 @@ preservedUsernames: "予約ユーザー名"
|
|||
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
|
||||
createNoteFromTheFile: "このファイルからノートを作成"
|
||||
archive: "アーカイブ"
|
||||
archived: "アーカイブ済み"
|
||||
unarchive: "アーカイブ解除"
|
||||
channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
|
||||
channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
|
||||
thisChannelArchived: "このチャンネルはアーカイブされています。"
|
||||
|
@ -1164,6 +1177,9 @@ preventAiLearning: "生成AIによる学習を拒否"
|
|||
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
|
||||
options: "オプション"
|
||||
specifyUser: "ユーザー指定"
|
||||
lookupConfirm: "照会しますか?"
|
||||
openTagPageConfirm: "ハッシュタグのページを開きますか?"
|
||||
specifyHost: "ホスト指定"
|
||||
failedToPreviewUrl: "プレビューできません"
|
||||
update: "更新"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
|
||||
|
@ -1303,6 +1319,11 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
|
|||
noDescription: "説明文はありません"
|
||||
alwaysConfirmFollow: "フォローの際常に確認する"
|
||||
inquiry: "お問い合わせ"
|
||||
tryAgain: "もう一度お試しください。"
|
||||
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
|
||||
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
|
||||
createdLists: "作成したリスト"
|
||||
createdAntennas: "作成したアンテナ"
|
||||
|
||||
_delivery:
|
||||
status: "配信状態"
|
||||
|
@ -1768,6 +1789,7 @@ _role:
|
|||
canManageAvatarDecorations: "アバターデコレーションの管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
antennaMax: "アンテナの作成可能数"
|
||||
wordMuteMax: "ワードミュートの最大文字数"
|
||||
|
@ -2040,8 +2062,6 @@ _sfx:
|
|||
note: "ノート"
|
||||
noteMy: "ノート(自分)"
|
||||
notification: "通知"
|
||||
antenna: "アンテナ受信"
|
||||
channel: "チャンネル通知"
|
||||
reaction: "リアクション選択時"
|
||||
|
||||
_soundSettings:
|
||||
|
@ -2051,6 +2071,7 @@ _soundSettings:
|
|||
driveFileTypeWarnDescription: "音声ファイルを選択してください"
|
||||
driveFileDurationWarn: "音声が長すぎます"
|
||||
driveFileDurationWarnDescription: "長い音声を使用するとSharkeyの使用に支障をきたす可能性があります。それでも続行しますか?"
|
||||
driveFileError: "音声が読み込めませんでした。設定を変更してください"
|
||||
|
||||
_ago:
|
||||
future: "未来"
|
||||
|
@ -2374,6 +2395,7 @@ _timelines:
|
|||
local: "ローカル"
|
||||
social: "ソーシャル"
|
||||
global: "グローバル"
|
||||
bubble: "バッッブル"
|
||||
|
||||
_play:
|
||||
new: "Playの作成"
|
||||
|
@ -2546,9 +2568,10 @@ _drivecleaner:
|
|||
|
||||
_webhookSettings:
|
||||
createWebhook: "Webhookを作成"
|
||||
modifyWebhook: "Webhookを編集"
|
||||
name: "名前"
|
||||
secret: "シークレット"
|
||||
events: "Webhookを実行するタイミング"
|
||||
trigger: "トリガー"
|
||||
active: "有効"
|
||||
_events:
|
||||
follow: "フォローしたとき"
|
||||
|
@ -2558,6 +2581,27 @@ _webhookSettings:
|
|||
renote: "Boostされたとき"
|
||||
reaction: "リアクションがあったとき"
|
||||
mention: "メンションされたとき"
|
||||
_systemEvents:
|
||||
abuseReport: "ユーザーから通報があったとき"
|
||||
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
||||
userCreated: "ユーザーが作成されたとき"
|
||||
deleteConfirm: "Webhookを削除しますか?"
|
||||
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "通報の通知先を追加"
|
||||
modifyRecipient: "通報の通知先を編集"
|
||||
recipientType: "通知先の種類"
|
||||
_recipientType:
|
||||
mail: "メール"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
|
||||
webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
|
||||
keywords: "キーワード"
|
||||
notifiedUser: "通知先ユーザー"
|
||||
notifiedWebhook: "使用するWebhook"
|
||||
deleteConfirm: "通知先を削除しますか?"
|
||||
|
||||
_moderationLogTypes:
|
||||
createRole: "ロールを作成"
|
||||
|
@ -2597,6 +2641,12 @@ _moderationLogTypes:
|
|||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||
unsetUserAvatar: "ユーザーのアイコンを解除"
|
||||
unsetUserBanner: "ユーザーのバナーを解除"
|
||||
createSystemWebhook: "SystemWebhookを作成"
|
||||
updateSystemWebhook: "SystemWebhookを更新"
|
||||
deleteSystemWebhook: "SystemWebhookを削除"
|
||||
createAbuseReportNotificationRecipient: "通報の通知先を作成"
|
||||
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
|
||||
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
|
||||
|
||||
_mfm:
|
||||
uncommonFeature: "この機能は一般的に普及していないため、他のMisskeyフォークを含めた多くのFediverseソフトウェアで表示できないことがあります。"
|
||||
|
@ -2745,16 +2795,16 @@ _dataRequest:
|
|||
|
||||
_dataSaver:
|
||||
_media:
|
||||
title: "メディアの読み込み"
|
||||
title: "メディアの読み込みを無効化"
|
||||
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
|
||||
_avatar:
|
||||
title: "アイコン画像"
|
||||
title: "アイコン画像のアニメーションを無効化"
|
||||
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
|
||||
_urlPreview:
|
||||
title: "URLプレビューのサムネイル"
|
||||
title: "URLプレビューのサムネイルを非表示"
|
||||
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
|
||||
_code:
|
||||
title: "コードハイライト"
|
||||
title: "コードハイライトを非表示"
|
||||
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
||||
|
||||
_hemisphere:
|
||||
|
@ -2830,3 +2880,9 @@ _mediaControls:
|
|||
pip: "ピクチャインピクチャ"
|
||||
playbackRate: "再生速度"
|
||||
loop: "ループ再生"
|
||||
|
||||
_contextMenu:
|
||||
title: "コンテキストメニュー"
|
||||
app: "アプリケーション"
|
||||
appWithShift: "Shiftキーでアプリケーション"
|
||||
native: "ブラウザのUI"
|
||||
|
|
|
@ -105,11 +105,12 @@ followRequests: "フォロー申請"
|
|||
unfollow: "フォローやめる"
|
||||
followRequestPending: "フォロー許してくれるん待っとる"
|
||||
enterEmoji: "絵文字を入れてや"
|
||||
renote: "ブースト"
|
||||
unrenote: "ブーストやめる"
|
||||
renoted: "ブーストしたで。"
|
||||
cantRenote: "この投稿はブーストできへんっぽい。"
|
||||
cantReRenote: "ブースト自体はブーストできへんで。"
|
||||
renote: "リノート"
|
||||
unrenote: "リノートやめる"
|
||||
renoted: "リノートしたで。"
|
||||
renotedToX: "{name}にリノートしたで"
|
||||
cantRenote: "この投稿はリノートできへんっぽい。"
|
||||
cantReRenote: "リノート自体はリノートできへんで。"
|
||||
quote: "引用"
|
||||
inChannelRenote: "チャンネルの中でブースト"
|
||||
inChannelQuote: "チャンネル内引用"
|
||||
|
@ -315,6 +316,7 @@ selectFile: "ファイル選んでや"
|
|||
selectFiles: "ファイル選んでや"
|
||||
selectFolder: "フォルダ選んでや"
|
||||
selectFolders: "フォルダ選んでや"
|
||||
fileNotSelected: "ファイルが選択されてへんで"
|
||||
renameFile: "ファイル名をいらう"
|
||||
folderName: "フォルダー名"
|
||||
createFolder: "フォルダー作る"
|
||||
|
@ -470,6 +472,7 @@ retype: "もっかい入力"
|
|||
noteOf: "{user}はんのノート"
|
||||
quoteAttached: "引用付いとるで"
|
||||
quoteQuestion: "引用として添付してもええか?"
|
||||
attachAsFileQuestion: "クリップボードのテキストが長すぎるからテキストファイルとして添付してもええか?"
|
||||
noMessagesYet: "まだチャットはあらへんで"
|
||||
newMessageExists: "新しいメッセージがきたで"
|
||||
onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
|
||||
|
@ -834,6 +837,7 @@ administration: "管理"
|
|||
accounts: "アカウント"
|
||||
switch: "切り替え"
|
||||
noMaintainerInformationWarning: "管理者情報が設定されてへんで"
|
||||
noInquiryUrlWarning: "問い合わせ先URLが設定されてへんで。"
|
||||
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
|
||||
configure: "設定する"
|
||||
postToGallery: "ギャラリーへ投稿"
|
||||
|
@ -1925,8 +1929,6 @@ _sfx:
|
|||
note: "ノート"
|
||||
noteMy: "ノート(自分)"
|
||||
notification: "通知"
|
||||
antenna: "アンテナ受信"
|
||||
channel: "チャンネル通知"
|
||||
reaction: "ツッコミ選んどるとき"
|
||||
_soundSettings:
|
||||
driveFile: "ドライブん中の音使う"
|
||||
|
@ -2391,7 +2393,6 @@ _webhookSettings:
|
|||
createWebhook: "Webhookをつくる"
|
||||
name: "名前"
|
||||
secret: "シークレット"
|
||||
events: "Webhookを投げるタイミング"
|
||||
active: "有効"
|
||||
_events:
|
||||
follow: "フォローしたとき~!"
|
||||
|
@ -2401,6 +2402,12 @@ _webhookSettings:
|
|||
renote: "ブーストされるとき~!"
|
||||
reaction: "ツッコまれたとき~!"
|
||||
mention: "メンションがあるとき~!"
|
||||
deleteConfirm: "ほんまにWebhookをほかしてもええんか?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "メール"
|
||||
deleteConfirm: "通知先を削除してもええか?"
|
||||
_moderationLogTypes:
|
||||
createRole: "ロールを追加すんで"
|
||||
deleteRole: "ロールほかす"
|
||||
|
|
|
@ -104,3 +104,7 @@ _deck:
|
|||
_columns:
|
||||
notifications: "Ilɣuyen"
|
||||
list: "Tibdarin"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Imayl"
|
||||
|
|
|
@ -805,6 +805,10 @@ _deck:
|
|||
mentions: "받언 멘션"
|
||||
_webhookSettings:
|
||||
name: "이럼"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "전자우펜"
|
||||
_moderationLogTypes:
|
||||
suspend: "얼우기"
|
||||
deleteNote: "노트 뭉캐기"
|
||||
|
|
|
@ -52,14 +52,14 @@ deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니
|
|||
addToList: "리스트에 추가"
|
||||
addToAntenna: "안테나에 추가"
|
||||
sendMessage: "메시지 보내기"
|
||||
copyRSS: "RSS 복사"
|
||||
copyUsername: "사용자 이름 복사"
|
||||
copyUserId: "사용자 ID 복사"
|
||||
copyRSS: "RSS 주소 복사"
|
||||
copyUsername: "유저명 복사"
|
||||
copyUserId: "유저 ID 복사"
|
||||
copyNoteId: "노트 ID 복사"
|
||||
copyFileId: "파일 ID 복사"
|
||||
copyFolderId: "폴더 ID 복사"
|
||||
copyProfileUrl: "프로필 URL 복사"
|
||||
searchUser: "사용자 검색"
|
||||
searchUser: "유저 검색"
|
||||
reply: "답글"
|
||||
loadMore: "더 보기"
|
||||
showMore: "더 보기"
|
||||
|
@ -108,22 +108,25 @@ enterEmoji: "이모지 입력"
|
|||
renote: "리노트"
|
||||
unrenote: "리노트 취소"
|
||||
renoted: "리노트했습니다"
|
||||
renotedToX: "{name}명이 리노트했습니다."
|
||||
cantRenote: "이 게시물은 리노트 할 수 없습니다."
|
||||
cantReRenote: "리노트를 리노트할 수 없습니다."
|
||||
cantReRenote: "리노트를 리노트 할 수 없습니다."
|
||||
quote: "인용"
|
||||
inChannelRenote: "채널 내 리노트"
|
||||
inChannelQuote: "채널 내 인용"
|
||||
renoteToChannel: "채널에 리노트"
|
||||
renoteToOtherChannel: "다른 채널에 리노트"
|
||||
pinnedNote: "고정된 노트"
|
||||
pinned: "고정하기"
|
||||
you: "나"
|
||||
clickToShow: "클릭하여 보기"
|
||||
sensitive: "열람 주의"
|
||||
add: "추가"
|
||||
reaction: "반응"
|
||||
reactions: "반응"
|
||||
reaction: "리액션"
|
||||
reactions: "리액션"
|
||||
emojiPicker: "이모지 선택기"
|
||||
pinnedEmojisForReactionSettingDescription: "리액션을 할 때 프로필에 고정하여 표시할 이모지를 설정할 수 있습니다"
|
||||
pinnedEmojisSettingDescription: "이모지를 입력할 때 프로필에 고정하여 표시할 이모지를 설정할 수 있습니다"
|
||||
pinnedEmojisForReactionSettingDescription: "리액션을 할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있습니다."
|
||||
pinnedEmojisSettingDescription: "이모지를 입력할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있습니다."
|
||||
emojiPickerDisplay: "선택기 표시"
|
||||
overwriteFromPinnedEmojisForReaction: "리액션 설정을 덮어쓰기"
|
||||
overwriteFromPinnedEmojis: "일반 설정을 덮어쓰기"
|
||||
|
@ -136,7 +139,7 @@ unmarkAsSensitive: "열람주의 해제"
|
|||
enterFileName: "파일명을 입력"
|
||||
mute: "뮤트"
|
||||
unmute: "뮤트 해제"
|
||||
renoteMute: "리노트 뮤트하기"
|
||||
renoteMute: "리노트 뮤트"
|
||||
renoteUnmute: "리노트 뮤트 해제"
|
||||
block: "차단"
|
||||
unblock: "차단 해제"
|
||||
|
@ -174,12 +177,16 @@ flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
|
|||
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
|
||||
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
|
||||
addAccount: "계정 추가"
|
||||
reloadAccountsList: "계정 리스트 정보 갱신"
|
||||
reloadAccountsList: "계정 목록 새로고침"
|
||||
loginFailed: "로그인에 실패했습니다"
|
||||
showOnRemote: "리모트에서 보기"
|
||||
continueOnRemote: "리모트에서 계속"
|
||||
chooseServerOnMisskeyHub: "Misskey Hub에서 서버 찾아보기"
|
||||
specifyServerHost: "서버 도메인 직접 지정"
|
||||
inputHostName: "도메인을 입력하세요"
|
||||
general: "일반"
|
||||
wallpaper: "배경"
|
||||
setWallpaper: "배경화면 설정"
|
||||
setWallpaper: "배경 설정"
|
||||
removeWallpaper: "배경 제거"
|
||||
searchWith: "검색: {q}"
|
||||
youHaveNoLists: "리스트가 없습니다"
|
||||
|
@ -187,7 +194,7 @@ followConfirm: "{name}님을 팔로우 하시겠습니까?"
|
|||
proxyAccount: "프록시 계정"
|
||||
proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 서버로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다."
|
||||
host: "호스트"
|
||||
selectUser: "사용자 선택"
|
||||
selectUser: "유저 선택"
|
||||
recipient: "수신인"
|
||||
annotation: "내용에 대한 주석"
|
||||
federation: "연합"
|
||||
|
@ -230,7 +237,7 @@ noUsers: "아무도 없습니다"
|
|||
editProfile: "프로필 수정"
|
||||
noteDeleteConfirm: "이 노트를 삭제하시겠습니까?"
|
||||
pinLimitExceeded: "더 이상 고정할 수 없습니다."
|
||||
intro: "Misskey의 설치를 완료했습니다! 관리자 계정을 만들어 주세요."
|
||||
intro: "Misskey의 설치가 완료되었습니다! 관리자 계정을 생성해주세요."
|
||||
done: "완료"
|
||||
processing: "처리중"
|
||||
preview: "미리보기"
|
||||
|
@ -247,7 +254,7 @@ publishing: "배포 중"
|
|||
notResponding: "응답 없음"
|
||||
instanceFollowing: "서버의 팔로잉"
|
||||
instanceFollowers: "서버의 팔로워"
|
||||
instanceUsers: "서버의 유저"
|
||||
instanceUsers: "서버의 사용자"
|
||||
changePassword: "비밀번호 변경"
|
||||
security: "보안"
|
||||
retypedNotMatch: "입력이 일치하지 않습니다."
|
||||
|
@ -263,12 +270,12 @@ lookup: "찾아보기"
|
|||
announcements: "공지사항"
|
||||
imageUrl: "이미지 URL"
|
||||
remove: "삭제"
|
||||
removed: "삭제하였습니다"
|
||||
removed: "삭제했습니다"
|
||||
removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
|
||||
deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
|
||||
resetAreYouSure: "초기화 하시겠습니까?"
|
||||
areYouSure: "계속 진행하시겠습니까?"
|
||||
saved: "저장하였습니다"
|
||||
saved: "저장했습니다"
|
||||
messaging: "대화"
|
||||
upload: "업로드"
|
||||
keepOriginalUploading: "원본 이미지를 유지"
|
||||
|
@ -296,7 +303,7 @@ activity: "활동"
|
|||
images: "이미지"
|
||||
image: "이미지"
|
||||
birthday: "생일"
|
||||
yearsOld: "만 {age} 세"
|
||||
yearsOld: "{age}세"
|
||||
registeredDate: "등록일"
|
||||
location: "장소"
|
||||
theme: "테마"
|
||||
|
@ -313,6 +320,7 @@ selectFile: "파일 선택"
|
|||
selectFiles: "파일 선택"
|
||||
selectFolder: "폴더 선택"
|
||||
selectFolders: "폴더 선택"
|
||||
fileNotSelected: "파일을 선택하지 않았습니다"
|
||||
renameFile: "파일 이름 변경"
|
||||
folderName: "폴더 이름"
|
||||
createFolder: "폴더 만들기"
|
||||
|
@ -370,7 +378,7 @@ inMb: "메가바이트 단위"
|
|||
bannerUrl: "배너 이미지 URL"
|
||||
backgroundImageUrl: "배경 이미지 URL"
|
||||
basicInfo: "기본 정보"
|
||||
pinnedUsers: "고정된 유저"
|
||||
pinnedUsers: "고정한 사용자"
|
||||
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
|
||||
pinnedPages: "고정한 페이지"
|
||||
pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
|
||||
|
@ -437,13 +445,13 @@ moderationNote: "조정 기록"
|
|||
addModerationNote: "조정 기록 추가하기"
|
||||
moderationLogs: "모더레이션 로그"
|
||||
nUsersMentioned: "{n}명이 언급함"
|
||||
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
||||
securityKeyAndPasskey: "보안 키 또는 패스키"
|
||||
securityKey: "보안 키"
|
||||
lastUsed: "마지막 사용"
|
||||
lastUsedAt: "마지막 사용: {t}"
|
||||
unregister: "등록 해제"
|
||||
passwordLessLogin: "비밀번호 없이 로그인"
|
||||
passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다."
|
||||
passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인합니다."
|
||||
resetPassword: "비밀번호 재설정"
|
||||
newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
|
||||
reduceUiAnimation: "UI의 애니메이션을 줄이기"
|
||||
|
@ -468,10 +476,12 @@ retype: "다시 입력"
|
|||
noteOf: "{user}의 노트"
|
||||
quoteAttached: "인용함"
|
||||
quoteQuestion: "인용해서 작성하시겠습니까?"
|
||||
attachAsFileQuestion: "붙여넣으려는 글이 너무 깁니다. 텍스트 파일로 첨부하시겠습니까?"
|
||||
noMessagesYet: "아직 대화가 없습니다"
|
||||
newMessageExists: "새 메시지가 있습니다"
|
||||
onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
|
||||
signinRequired: "진행하기 전에 로그인을 해 주세요"
|
||||
signinOrContinueOnRemote: "계속하려면 사용하는 서버로 이동하거나 이 서버에 로그인해야 합니다."
|
||||
invitations: "초대"
|
||||
invitationCode: "초대 코드"
|
||||
checking: "확인하는 중입니다"
|
||||
|
@ -486,7 +496,7 @@ strongPassword: "강한 비밀번호"
|
|||
passwordMatched: "일치합니다"
|
||||
passwordNotMatched: "일치하지 않습니다"
|
||||
signinWith: "{x}로 로그인"
|
||||
signinFailed: "로그인할 수 없습니다. 사용자명과 비밀번호를 확인하여 주십시오."
|
||||
signinFailed: "로그인할 수 없습니다. 사용자 이름과 비밀번호를 확인해 주십시오."
|
||||
or: "혹은"
|
||||
language: "언어"
|
||||
uiLanguage: "UI 표시 언어"
|
||||
|
@ -494,7 +504,7 @@ aboutX: "{x}에 대하여"
|
|||
emojiStyle: "이모지 스타일"
|
||||
native: "기본"
|
||||
disableDrawer: "드로어 메뉴를 사용하지 않기"
|
||||
showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시"
|
||||
showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기"
|
||||
showReactionsCount: "노트의 반응 수를 표시하기"
|
||||
noHistory: "기록이 없습니다"
|
||||
signinHistory: "로그인 기록"
|
||||
|
@ -559,7 +569,7 @@ popout: "새 창으로 열기"
|
|||
volume: "음량"
|
||||
masterVolume: "마스터 볼륨"
|
||||
notUseSound: "음소거 하기"
|
||||
useSoundOnlyWhenActive: "Misskey가 활성화 되어져 있을 때만 소리 출력하기"
|
||||
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
||||
details: "자세히"
|
||||
chooseEmoji: "이모지 선택"
|
||||
unableToProcess: "작업을 완료할 수 없습니다"
|
||||
|
@ -588,7 +598,7 @@ deleteAllFiles: "모든 파일 삭제"
|
|||
deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?"
|
||||
removeAllFollowing: "모든 팔로잉 해제"
|
||||
removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요."
|
||||
userSuspended: "이 계정은 정지된 상태입니다."
|
||||
userSuspended: "이 사용자는 정지되었습니다."
|
||||
userSilenced: "이 계정은 사일런스된 상태입니다."
|
||||
yourAccountSuspendedTitle: "계정이 정지되었습니다"
|
||||
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
|
||||
|
@ -752,7 +762,7 @@ experimentalFeatures: "실험실"
|
|||
experimental: "실험실"
|
||||
thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양이 변경되거나 정상적으로 동작하지 않을 가능성이 있습니다."
|
||||
developer: "개발자"
|
||||
makeExplorable: "\"발견하기\"에 내 계정 보이기"
|
||||
makeExplorable: "계정을 쉽게 발견하도록 하기"
|
||||
makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다."
|
||||
showGapBetweenNotesInTimeline: "타임라인의 노트 사이를 띄워서 표시"
|
||||
duplicate: "복제"
|
||||
|
@ -798,7 +808,7 @@ emailNotification: "메일 알림"
|
|||
publish: "게시"
|
||||
inChannelSearch: "채널에서 검색"
|
||||
useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
|
||||
typingUsers: "{users} 님이 입력하고 있어요.."
|
||||
typingUsers: "{users}님이 입력 중"
|
||||
jumpToSpecifiedDate: "특정 날짜로 이동"
|
||||
showingPastTimeline: "과거의 타임라인을 표시하고 있어요"
|
||||
clear: "지우기"
|
||||
|
@ -832,6 +842,7 @@ administration: "관리"
|
|||
accounts: "계정"
|
||||
switch: "전환"
|
||||
noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습니다."
|
||||
noInquiryUrlWarning: "문의처 주소를 설정하지 않았습니다."
|
||||
noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
|
||||
configure: "설정하기"
|
||||
postToGallery: "갤러리에 업로드"
|
||||
|
@ -1021,6 +1032,7 @@ thisPostMayBeAnnoyingHome: "홈에 게시"
|
|||
thisPostMayBeAnnoyingCancel: "그만두기"
|
||||
thisPostMayBeAnnoyingIgnore: "이대로 게시"
|
||||
collapseRenotes: "이미 본 리노트를 간략화하기"
|
||||
collapseRenotesDescription: "반응이나 리노트를 한 노트를 접어서 표시합니다."
|
||||
internalServerError: "내부 서버 오류"
|
||||
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
|
||||
copyErrorInfo: "오류 정보 복사"
|
||||
|
@ -1090,7 +1102,7 @@ serverRules: "서버 규칙"
|
|||
pleaseConfirmBelowBeforeSignup: "이 서버에 가입하기 전에 아래 사항을 확인하여 주십시오."
|
||||
pleaseAgreeAllToContinue: "계속하시려면 모든 항목에 동의하십시오."
|
||||
continue: "계속"
|
||||
preservedUsernames: "예약된 사용자명"
|
||||
preservedUsernames: "예약한 사용자 이름"
|
||||
preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
|
||||
createNoteFromTheFile: "이 파일로 노트를 작성"
|
||||
archive: "아카이브"
|
||||
|
@ -1230,10 +1242,22 @@ useTotp: "일회용 비밀번호 사용"
|
|||
useBackupCode: "백업 코드 사용"
|
||||
launchApp: "앱 실행"
|
||||
useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
|
||||
keepOriginalFilename: "원본 파일 이름을 유지"
|
||||
keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파일 이름이 자동으로 무작위 문자열로 바뀝니다."
|
||||
noDescription: "설명문이 없습니다"
|
||||
alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
|
||||
inquiry: "문의하기"
|
||||
tryAgain: "다시 시도해 주세요."
|
||||
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
|
||||
_delivery:
|
||||
status: "전송 상태"
|
||||
stop: "정지됨"
|
||||
resume: "전송 다시 시작"
|
||||
_type:
|
||||
none: "배포 중"
|
||||
manuallySuspended: "수동 정지 중"
|
||||
goneSuspended: "서버 삭제를 이유로 정지 중"
|
||||
autoSuspendedForNotResponding: "서버 응답 없음을 이유로 정지 중"
|
||||
_bubbleGame:
|
||||
howToPlay: "설명"
|
||||
hold: "홀드"
|
||||
|
@ -1359,6 +1383,8 @@ _serverSettings:
|
|||
fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있습니다. 단, Redis의 메모리 사용량이 증가합니다. 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 비활성화할 수 있습니다."
|
||||
fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기"
|
||||
fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다."
|
||||
inquiryUrl: "문의처 URL"
|
||||
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
|
||||
_accountMigration:
|
||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
||||
|
@ -1675,10 +1701,11 @@ _role:
|
|||
canManageAvatarDecorations: "아바타 꾸미기 관리"
|
||||
driveCapacity: "드라이브 용량"
|
||||
alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
|
||||
canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
|
||||
pinMax: "고정할 수 있는 노트 수"
|
||||
antennaMax: "만들 수 있는 안테나 수"
|
||||
wordMuteMax: "단어 뮤트할 수 있는 문자 수"
|
||||
webhookMax: "만들 수 있는 웹후크 수"
|
||||
webhookMax: "만들 수 있는 Webhook 수"
|
||||
clipMax: "만들 수 있는 클립 수"
|
||||
noteEachClipsMax: "클립에 넣을 수 있는 노트 수"
|
||||
userListMax: "만들 수 있는 사용자 리스트 수"
|
||||
|
@ -1693,6 +1720,11 @@ _role:
|
|||
roleAssignedTo: "수동 역할에 이미 할당됨"
|
||||
isLocal: "로컬 사용자"
|
||||
isRemote: "리모트 사용자"
|
||||
isCat: "고양이 사용자"
|
||||
isBot: "봇 사용자"
|
||||
isSuspended: "정지된 사용자"
|
||||
isLocked: "잠금 계정 사용자"
|
||||
isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자"
|
||||
createdLessThan: "가입한 지 다음 일수 이내인 유저"
|
||||
createdMoreThan: "가입한 지 다음 일수 이상인 유저"
|
||||
followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
|
||||
|
@ -1913,8 +1945,6 @@ _sfx:
|
|||
note: "새 노트"
|
||||
noteMy: "내 노트"
|
||||
notification: "알림"
|
||||
antenna: "안테나 수신"
|
||||
channel: "채널 알림"
|
||||
reaction: "리액션 선택"
|
||||
_soundSettings:
|
||||
driveFile: "드라이브에 있는 오디오를 사용"
|
||||
|
@ -1975,6 +2005,7 @@ _2fa:
|
|||
backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다."
|
||||
backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
|
||||
backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요."
|
||||
moreDetailedGuideHere: "여기에 자세한 설명이 있습니다"
|
||||
_permissions:
|
||||
"read:account": "계정의 정보를 봅니다"
|
||||
"write:account": "계정의 정보를 변경합니다"
|
||||
|
@ -2163,7 +2194,7 @@ _postForm:
|
|||
c: "무엇을 생각하고 있나요?"
|
||||
d: "말하고 싶은 게 있나요?"
|
||||
e: "여기에 적어 주세요"
|
||||
f: "글 쓰기를 기다려요…"
|
||||
f: "작성해주시길 기다리고 있어요..."
|
||||
_profile:
|
||||
name: "이름"
|
||||
username: "사용자 이름"
|
||||
|
@ -2338,6 +2369,7 @@ _deck:
|
|||
alwaysShowMainColumn: "메인 칼럼 항상 표시"
|
||||
columnAlign: "칼럼 정렬"
|
||||
addColumn: "칼럼 추가"
|
||||
newNoteNotificationSettings: "새 노트 알림 설정"
|
||||
configureColumn: "칼럼 설정"
|
||||
swapLeft: "왼쪽으로 이동"
|
||||
swapRight: "오른쪽으로 이동"
|
||||
|
@ -2376,9 +2408,9 @@ _drivecleaner:
|
|||
orderByCreatedAtAsc: "등록일이 오래된 순"
|
||||
_webhookSettings:
|
||||
createWebhook: "Webhook 생성"
|
||||
modifyWebhook: "Webhook 수정"
|
||||
name: "이름"
|
||||
secret: "시크릿"
|
||||
events: "Webhook을 실행할 타이밍"
|
||||
active: "활성화"
|
||||
_events:
|
||||
follow: "누군가를 팔로우했을 때"
|
||||
|
@ -2388,6 +2420,26 @@ _webhookSettings:
|
|||
renote: "누군가 내 글을 리노트했을 때"
|
||||
reaction: "누군가 내 노트에 리액션했을 때"
|
||||
mention: "누군가 나를 멘션했을 때"
|
||||
_systemEvents:
|
||||
abuseReport: "유저로부터 신고를 받았을 때"
|
||||
abuseReportResolved: "받은 신고를 처리했을 때"
|
||||
userCreated: "유저가 생성되었을 때"
|
||||
deleteConfirm: "Webhook을 삭제할까요?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "신고 수신자 추가"
|
||||
modifyRecipient: "신고 수신자 편집"
|
||||
recipientType: "알림 수신 유형"
|
||||
_recipientType:
|
||||
mail: "이메일"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림을 보냅니다 (신고를 받은 때에만)"
|
||||
webhook: "지정한 SystemWebhook에 알림을 보냅니다 (신고를 받은 때와 해결했을 때에 송신)"
|
||||
keywords: "키워드"
|
||||
notifiedUser: "신고 알림을 보낼 유저"
|
||||
notifiedWebhook: "사용할 Webhook"
|
||||
deleteConfirm: "수신자를 삭제하시겠습니까?"
|
||||
_moderationLogTypes:
|
||||
createRole: "역할 생성"
|
||||
deleteRole: "역할 삭제"
|
||||
|
@ -2403,7 +2455,7 @@ _moderationLogTypes:
|
|||
updateUserNote: "조정 기록 갱신"
|
||||
deleteDriveFile: "파일 삭제"
|
||||
deleteNote: "노트 삭제"
|
||||
createGlobalAnnouncement: "모든 공지사항 만들기"
|
||||
createGlobalAnnouncement: "전역 공지사항 생성"
|
||||
createUserAnnouncement: "사용자 공지사항 만들기"
|
||||
updateGlobalAnnouncement: "모든 공지사항 수정"
|
||||
updateUserAnnouncement: "사용자 공지사항 수정"
|
||||
|
@ -2425,6 +2477,12 @@ _moderationLogTypes:
|
|||
deleteAvatarDecoration: "아바타 장식 삭제"
|
||||
unsetUserAvatar: "유저 아바타 제거"
|
||||
unsetUserBanner: "유저 배너 제거"
|
||||
createSystemWebhook: "SystemWebhook을 생성"
|
||||
updateSystemWebhook: "SystemWebhook을 수정"
|
||||
deleteSystemWebhook: "SystemWebhook을 삭제"
|
||||
createAbuseReportNotificationRecipient: "신고 알림 수신자 생성"
|
||||
updateAbuseReportNotificationRecipient: "신고 알림 수신자 편집"
|
||||
deleteAbuseReportNotificationRecipient: "신고 알림 수신자 삭제"
|
||||
_fileViewer:
|
||||
title: "파일 상세"
|
||||
type: "파일 유형"
|
||||
|
|
|
@ -18,15 +18,15 @@ enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້"
|
|||
renotedBy: "Renoted ໂດຍ {user}"
|
||||
noNotes: "ບໍ່ມີ note"
|
||||
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
|
||||
instance: "ອີນສະແຕນ"
|
||||
settings: "ກຳນົດຄ່າ"
|
||||
instance: "ເຊີຟເວີຣ໌"
|
||||
settings: "ຕັ້ງຄ່າ"
|
||||
notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ"
|
||||
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
|
||||
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
|
||||
openInWindow: "ເປີດໃນປ່ອງຢ້ຽມ"
|
||||
profile: "ໂພຼຟາຍ"
|
||||
openInWindow: "ເປີດໃນ window"
|
||||
profile: "ໂປຣໄຟລ໌"
|
||||
timeline: "ໄທມ໌ໄລນ໌"
|
||||
noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ"
|
||||
noAccountDescription: "ຜູ້ໃຊ້ຄົນນີ້ຍັງບໍ່ໄດ້ຂຽນຄຳແນະນຳໂຕ"
|
||||
login: "ເຂົ້າສູ່ລະບົບ"
|
||||
loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..."
|
||||
logout: "ອອກຈາກລະບົບ"
|
||||
|
@ -37,7 +37,7 @@ users: "ຜູ້ໃຊ້"
|
|||
addUser: "ເພີ່ມຜູ້ໃຊ້"
|
||||
favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ"
|
||||
favorites: "ລາຍການທີ່ມັກ"
|
||||
unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ"
|
||||
unfavorite: "ເອົາອອກຈາກລາຍການທີ່ມັກ"
|
||||
favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ"
|
||||
alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ."
|
||||
cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້."
|
||||
|
@ -48,41 +48,41 @@ copyLink: "ຄັດລອກລິ້ງ"
|
|||
copyLinkRenote: "ຄັດລອກລິ້ງຂອງ renote"
|
||||
delete: "ລຶບ"
|
||||
deleteAndEdit: "ລຶບແລະແກ້ໄຂ"
|
||||
deleteAndEditConfirm: "ເຈົ້າແນ່ໃຈບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບ note ນີ້ ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍ reaction, renote, ແລະການຕອບກັບທັງໝົດ"
|
||||
deleteAndEditConfirm: "ຕ້ອງການລຶບ note ນີ້ແລະແກ້ໄຂໃໝ່ແມ່ນບໍ່? reaction, renote ແລະການຕອບກັບຕໍ່ note ນີ້ ທັງເບິດຈະຖືກລຶບອອກ"
|
||||
addToList: "ເພີ່ມໃສ່ລາຍຊື່"
|
||||
addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ"
|
||||
sendMessage: "ສົ່ງຂໍ້ຄວາມ"
|
||||
copyRSS: "ສຳເນົາ RSS"
|
||||
copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
|
||||
copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້"
|
||||
copyNoteId: "ສຳເນົາ ID ບັນທຶກ"
|
||||
copyFileId: "ສຳເນົາ ID ໄຟລ໌"
|
||||
copyFolderId: "ສຳເນົາ ID ໂຟນເດີ"
|
||||
copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌"
|
||||
copyRSS: "ຄັດລອກ RSS"
|
||||
copyUsername: "ຄັດລອກຊື່ຜູ້ໃຊ້"
|
||||
copyUserId: "ຄັດລອກ ID ຜູ້ໃຊ້"
|
||||
copyNoteId: "ຄັດລອກ ID ຂອງ note"
|
||||
copyFileId: "ຄັດລອກ ID ໄຟລ໌"
|
||||
copyFolderId: "ຄັດລອກ ID ໂຟລ໌ເດີຣ໌"
|
||||
copyProfileUrl: "ຄັດລອກ URL ໂປຣໄຟລ໌"
|
||||
searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
|
||||
reply: "ຕອບໄປທີ"
|
||||
reply: "ຕອບກັບ"
|
||||
loadMore: "ໂຫຼດເພີ່ມເຕີມ"
|
||||
showMore: "ໂຫຼດເພີ່ມເຕີມ"
|
||||
showLess: "ປິດ"
|
||||
youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ"
|
||||
receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ"
|
||||
followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ"
|
||||
mention: "ກ່າວຖືງ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
youGotNewFollower: "ໄດ້ຕິດຕາມເຈົ້າ"
|
||||
receiveFollowRequest: "ມີຄຳຂໍຕິດຕາມສົ່ງມາ"
|
||||
followRequestAccepted: "ການຕິດຕາມໄດ້ຮັບອນຸຍາດແລ້ວ"
|
||||
mention: "ເວົ້າເຖີງ"
|
||||
mentions: "ເວົ້າເຖີງເຈົ້າ"
|
||||
directNotes: "ໂພສ Direct note"
|
||||
importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ"
|
||||
import: "ນຳເຂົ້າ"
|
||||
export: "ສົ່ງອອກ"
|
||||
files: "ໄຟລ໌"
|
||||
download: "ດາວໂຫລດ"
|
||||
driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? note ທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ"
|
||||
unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?"
|
||||
exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ"
|
||||
importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ"
|
||||
driveFileDeleteConfirm: "ຕ້ອງການລຶບໄຟລ໌ “{name}” ແມ່ນບໍ່? Note ທີ່ແນບມາກັບໄຟລ໌ນີ້ຈະຖືກລຶບອອກ"
|
||||
unfollowConfirm: "ຕ້ອງການເລີກຕິດຕາມ {name} ແມ່ນບໍ່?"
|
||||
exportRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການສົ່ງອອກ ອາດໃຊ້ເວລາຈັກໜ່ອຍ ເມື່ອແລ້ວຈະຖືກເພີ່ມໃສ່ drive"
|
||||
importRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການນຳເຂົ້າ ການດຳເນິນການນີ້ອາດໃຊ້ເວລາຈັກໜ່ອຍ"
|
||||
lists: "ລາຍການ"
|
||||
noLists: "ທ່ານບໍ່ມີລາຍການໃດໆ"
|
||||
note: "ບັນທຶກ"
|
||||
notes: "ບັນທຶກ"
|
||||
noLists: "ບໍ່ມີລາຍການໃດໆ"
|
||||
note: "Note"
|
||||
notes: "Note"
|
||||
following: "ກຳລັງຕິດຕາມ"
|
||||
followers: "ຜູ້ຕິດຕາມ"
|
||||
followsYou: "ຕິດຕາມເຈົ້າ"
|
||||
|
@ -124,11 +124,11 @@ reactions: "reaction"
|
|||
attachCancel: "ເອົາໄຟລ໌ແນບ"
|
||||
mute: "ປີດສຽງ"
|
||||
unmute: "ເປີດສຽງ"
|
||||
block: "ບ໋ອກ"
|
||||
unblock: "ຍົກເລີກກາຮົບລັອກ"
|
||||
block: "ບລັອກ"
|
||||
unblock: "ເລີກບລັອກ"
|
||||
suspend: "ລະງັບ"
|
||||
unsuspend: "ເຊົາລະງັບ"
|
||||
selectList: "ເລືອກບັນຊີລາຍການ"
|
||||
selectList: "ເລືອກລາຍຊື່"
|
||||
editList: "ແກ້ໄຂລາຍຊື່"
|
||||
selectChannel: "ເລືອກຊ່ອງ"
|
||||
selectAntenna: "ເລືອກເສົາອາກາດ"
|
||||
|
@ -151,30 +151,30 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ
|
|||
autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
|
||||
addAccount: "ເພີ່ມບັນຊີ"
|
||||
loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
|
||||
showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ"
|
||||
showOnRemote: "ເບິ່ງໃນເຊີຟເວີຣ໌ໄລຍະໄກ"
|
||||
general: "ທົ່ວໄປ"
|
||||
wallpaper: "ພາບພື້ນຫລັງ"
|
||||
setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
|
||||
removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ"
|
||||
searchWith: "ຊອກຫາ: {q}"
|
||||
youHaveNoLists: "ທ່ານບໍ່ມີລາຍການໃດໆ"
|
||||
youHaveNoLists: "ເຈົ້າບໍ່ມີລາຍຊື່ໃດໆ"
|
||||
proxyAccount: "ບັນຊີພຣັອກຊີ"
|
||||
host: "ໂຮດສ"
|
||||
host: "ໂຮສຕ໌"
|
||||
selectUser: "ເລືອກຜູ້ໃຊ້"
|
||||
recipient: "ເຖິງ"
|
||||
annotation: "ຄຳເຫັນ"
|
||||
federation: "ສະຫະພັນ"
|
||||
instances: "ອີນສະແຕນ"
|
||||
instances: "ເຊີຟເວີຣ໌"
|
||||
registeredAt: "ລົງທະບຽນຢູ່"
|
||||
storageUsage: "ບ່ອນຈັດເກັບຂໍ້ມູນທີ່ໃຊ້"
|
||||
charts: "ອັນດັບເພງ"
|
||||
charts: "ແຜນພູມ"
|
||||
perHour: "ຕໍ່ຊົ່ວໂມງ"
|
||||
perDay: "ຕໍ່ມື້"
|
||||
stopActivityDelivery: "ຢຸດເຊົາການສົ່ງກິດຈະກໍາ"
|
||||
blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້"
|
||||
operations: "ການດຳເນີນງານ"
|
||||
software: "ຊອບແວ"
|
||||
version: "ສະບັບ"
|
||||
version: "ເວີຣ໌ຊັນ"
|
||||
metadata: "Metadata"
|
||||
withNFiles: "{n} ໄຟລ໌(s)"
|
||||
monitor: "ຈໍພາບ"
|
||||
|
@ -199,15 +199,15 @@ federating: "ສະຫະພັນ"
|
|||
blocked: "ບລັອກແລ້ວ "
|
||||
suspended: "ໂຈະ"
|
||||
all: "ທັງໝົດ"
|
||||
subscribing: "ສະໝັກສະມາຊິກແລັວ"
|
||||
publishing: "ການພິມເຜີຍແຜ່"
|
||||
subscribing: "ກຳລັງສະມັກສະມາຊິກ"
|
||||
publishing: "ກຳລັງເຜີຍແພ່"
|
||||
notResponding: "ບໍ່ຕອບສະໜອງ"
|
||||
instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ"
|
||||
instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ"
|
||||
instanceUsers: "ຜູ້ຊົມໃຊ້ຂອງຕົວຢ່າງນີ້"
|
||||
instanceFollowing: "ກຳລັງຕິດຕາມບົນເຊີຟເວີຣ໌"
|
||||
instanceFollowers: "ຜູ້ຕິດຕາມຂອງເຊີຟເວີຣ໌"
|
||||
instanceUsers: "ຜູ້ໃຊ້ຂອງເຊີຟເວີຣ໌ນີ້"
|
||||
changePassword: "ປ່ຽນລະຫັດຜ່ານ"
|
||||
security: "ຄວາມປອດໄພ"
|
||||
retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
|
||||
retypedNotMatch: "ປ້ອນຂໍ້ມູນບໍ່ກົງກັນ"
|
||||
currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
|
||||
newPassword: "ລະຫັດຜ່ານໃໝ່"
|
||||
newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ"
|
||||
|
@ -223,14 +223,14 @@ remove: "ລຶບ"
|
|||
removed: "ລຶບແລ້ວ"
|
||||
resetAreYouSure: "ຣີເຊັດບໍ?"
|
||||
saved: "ບັນທຶກແລ້ວ"
|
||||
messaging: "ແຊ໋ດ"
|
||||
messaging: "ແຊັຕ"
|
||||
upload: "ອັບໂຫຼດ"
|
||||
keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
|
||||
fromDrive: "ຈາກ Drive"
|
||||
fromUrl: "ຈາກ URL"
|
||||
uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
|
||||
uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
|
||||
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ"
|
||||
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດແລ້ວ"
|
||||
explore: "ສຳຫຼວດ"
|
||||
messageRead: "ອ່ານແລ້ວ"
|
||||
startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
|
||||
|
@ -244,47 +244,47 @@ images: "ຮູບພາບ"
|
|||
image: "ຮູບພາບ"
|
||||
birthday: "ວັນເກີດ"
|
||||
yearsOld: "{age} ປີ"
|
||||
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
|
||||
registeredDate: "ວັນທີ່ລົງທະບຽນ"
|
||||
location: "ທີ່ຕັ້ງ"
|
||||
theme: "ແທ໋ມ"
|
||||
themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ"
|
||||
themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ"
|
||||
theme: "Theme"
|
||||
themeForLightMode: "Theme ໃຊ້ໃນໂໝດສະຫວ່າງ"
|
||||
themeForDarkMode: "Theme ໃຊ້ໃນໂໝດມືດ"
|
||||
light: "ສະຫວ່າງ"
|
||||
dark: "ມືດ"
|
||||
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
|
||||
darkThemes: "ຮູບແບບສີສັນມືດ"
|
||||
syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ"
|
||||
drive: "ຂັບ"
|
||||
drive: "Drive"
|
||||
fileName: "ຊື່ໄຟລ໌"
|
||||
selectFile: "ເລືອກໄຟລ໌"
|
||||
selectFiles: "ເລືອກໄຟລ໌"
|
||||
selectFolder: "ເລືອກໂຟລເດີ"
|
||||
selectFolders: "ເລືອກໂຟລເດີ"
|
||||
renameFile: "ປ່ຽນຊື່ໄຟລ໌"
|
||||
folderName: "ຊື່ໂຟນເດີ"
|
||||
folderName: "ຊື່ໂຟລເດີຣ໌"
|
||||
createFolder: "ສ້າງໂຟລເດີ"
|
||||
renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້"
|
||||
deleteFolder: "ລົບໂຟລເດີ"
|
||||
addFile: "ເພີ່ມໄຟລ໌"
|
||||
emptyDrive: "Drive ຂອງທ່ານຫວ່າງເປົ່າ"
|
||||
emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ"
|
||||
emptyFolder: "ໂຟລເດີຣ໌ນີ້ວ່າງເປົ່າ"
|
||||
unableToDelete: "ບໍ່ສາມາດລົບໄດ້"
|
||||
inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃໝ່"
|
||||
inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່"
|
||||
inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່"
|
||||
circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ"
|
||||
rename: "ປ່ຽນຊື່"
|
||||
doNothing: "ບໍ່ສົນໃຈ"
|
||||
watch: "ເບິ່ງ"
|
||||
unwatch: "ຢຸດເບິ່ງ"
|
||||
doNothing: "ຢ່າມັນ"
|
||||
watch: "ເພັ່ງເລັງ"
|
||||
unwatch: "ຢຸດເພັ່ງເລັງ"
|
||||
accept: "ອະນຸຍາດ"
|
||||
reject: "ປະຕິເສດ"
|
||||
normal: "ປົກກະຕິ"
|
||||
instanceName: "ຊື່ເຊີເວີ້"
|
||||
instanceDescription: "ຄໍາອະທິບາຍຕົວຢ່າງ"
|
||||
instanceDescription: "ຄຳອະທິບາຍແນະນຳເຊີຟເວີຣ໌"
|
||||
maintainerName: "ຜູ້ດູແລ"
|
||||
maintainerEmail: "ອີເມວ admin"
|
||||
tosUrl: "ເງື່ອນໄຂການໃຫ້ບໍລິການ URL"
|
||||
maintainerEmail: "ອີເມລຜູ້ດູແລ"
|
||||
tosUrl: " URL ເງື່ອນໄຂການໃຫ້ບໍລິການ"
|
||||
thisYear: "ປີນີ້"
|
||||
thisMonth: "ເດືອນນີ້"
|
||||
today: "ມື້ນີ້"
|
||||
|
@ -292,34 +292,34 @@ dayX: "ວັນ {day}"
|
|||
monthX: "ເດືອນ {month}"
|
||||
yearX: "ປີ {year}"
|
||||
pages: "ໜ້າ"
|
||||
integration: "ຄວາມສຳພັນຂອງ"
|
||||
integration: "ເຊື່ອມໂຍງ"
|
||||
connectService: "ເຊື່ອມຕໍ່"
|
||||
disconnectService: "ຕັດການເຊື່ອມຕໍ່"
|
||||
enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ່ນ"
|
||||
enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ"
|
||||
disablingTimelinesInfo: "ຜູ້ເບິ່ງແຍງລະບົບ ແລະຜູ້ຄວບຄຸມຈະມີການເຂົ້າເຖິງທຸກກຳນົດເວລາ, ເຖິງແມ່ນວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ"
|
||||
disablingTimelinesInfo: "ຜູ້ດູແລລະບບແລະຜູ້ຄວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍ່ຕາມ"
|
||||
registration: "ລົງທະບຽນ"
|
||||
enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່"
|
||||
invite: "ເຊີນ"
|
||||
driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
|
||||
driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
|
||||
driveCapacityPerLocalAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
|
||||
driveCapacityPerRemoteAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ໄລຍະໄກ"
|
||||
basicInfo: "ຂໍ້ມຸນເບື້ອງຕົ້ນ"
|
||||
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
|
||||
hcaptchaSiteKey: "ກະແຈໄຊທ໌"
|
||||
hcaptchaSecretKey: "ກະແຈລັບ"
|
||||
mcaptchaSiteKey: "ກະແຈໄຊທ໌"
|
||||
mcaptchaSecretKey: "ກະແຈລັບ"
|
||||
pinnedNotes: "Note ທີ່ປັກໝຸດໄວ້"
|
||||
hcaptchaSiteKey: "Site key"
|
||||
hcaptchaSecretKey: "Secret key"
|
||||
mcaptchaSiteKey: "Site key"
|
||||
mcaptchaSecretKey: "Secret Key"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "ເປີດໃຊ້ງານລີແຄ໋ບຈາ"
|
||||
recaptchaSiteKey: "ກະແຈໄຊທ໌"
|
||||
recaptchaSecretKey: "ກະແຈລັບ"
|
||||
turnstileSiteKey: "ກະແຈໄຊທ໌"
|
||||
turnstileSecretKey: "ກະແຈລັບ"
|
||||
enableRecaptcha: "ເປີດໃຊ້ງານ reCAPTCHA"
|
||||
recaptchaSiteKey: "Site key"
|
||||
recaptchaSecretKey: "Secret key"
|
||||
turnstileSiteKey: "Site key"
|
||||
turnstileSecretKey: "Secret key"
|
||||
name: "ຊື່"
|
||||
userList: "ລາຍການ"
|
||||
about: "ກ່ຽວກັບ"
|
||||
aboutMisskey: "ກ່ຽວກັບ Misskey"
|
||||
administrator: "ຜູ້ບໍລິຫານ"
|
||||
administrator: "ຜູ້ດູແລ"
|
||||
token: "ໂທເຄັນ"
|
||||
share: "ແບ່ງປັນ"
|
||||
notFound: "ບໍ່ພົບ"
|
||||
|
@ -332,27 +332,27 @@ title: "ຫົວຂໍ້"
|
|||
text: "ຂໍ້ຄວາມ"
|
||||
enable: "ເປີດໃຊ້"
|
||||
next: "ຕໍ່ໄປ"
|
||||
retype: "ເຂົ້າໄປອີກຄັ້ງ"
|
||||
quoteAttached: "ວົງຢືມ"
|
||||
retype: "ລອງພິມລະຫັດອີກເທື່ອໜຶ່ງ"
|
||||
quoteAttached: "ອ້າງອິງ"
|
||||
invitations: "ເຊີນ"
|
||||
unavailable: "ບໍ່ສາມາດໃຊ້ໄດ້"
|
||||
language: "ພາສາ"
|
||||
aboutX: "ກ່ຽວກັບ {x}"
|
||||
emojiStyle: "ຮູບແບບອີໂມຈິ"
|
||||
native: "ພາສາແມ່"
|
||||
noHistory: "ບໍ່ມີລາຍການຢູ່ບ່ອນນີ້"
|
||||
noHistory: "ບໍ່ມີປະຫວັດ"
|
||||
doing: "ກຳລັງປະມວນຜົນ..."
|
||||
category: "ຫມວດຫມູ່"
|
||||
tags: "ແທ໋ກ"
|
||||
tags: "Aliases"
|
||||
createAccount: "ສ້າງບັນຊີ"
|
||||
existingAccount: "ທີ່ມີຢູ່"
|
||||
dashboard: "ໜ້າປັດ"
|
||||
existingAccount: "ບັນຊີທີ່ມີຢູ່ແລ້ວ"
|
||||
dashboard: "Dashboard"
|
||||
local: "ທ້ອງຖິ່ນ"
|
||||
numberOfDays: "ຈຳນວນມື້"
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStoragePrefix: "Prefix"
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageRegion: "ພາກພື້ນ"
|
||||
objectStorageRegion: "ພູມິພາກ"
|
||||
deleteAll: "ລຶບທັງໝົດ"
|
||||
sounds: "ສຽງ"
|
||||
sound: "ສຽງ"
|
||||
|
@ -365,11 +365,11 @@ state: "ສະຖານະ"
|
|||
sort: "ຈັດຮຽງໂດຍ"
|
||||
ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່"
|
||||
descendingOrder: "ໃຫຍ່ຫານ້ອຍ"
|
||||
output: "ຜົນຜະລິດ"
|
||||
script: "ບົດຄວາມ"
|
||||
output: "Output"
|
||||
script: "Script"
|
||||
menu: "ເມນູ"
|
||||
rearrange: "ຈັດລຽງຄືນ"
|
||||
poll: "ການພູນ"
|
||||
rearrange: "ຈັດລຽງໃໝ່"
|
||||
poll: "Poll"
|
||||
description: "ລາຍລະອຽດ"
|
||||
author: "ຜູ້ຂຽນ"
|
||||
manage: "ການຈັດການ"
|
||||
|
@ -383,7 +383,7 @@ permission: "ການອະນຸຍາດ"
|
|||
notificationType: "ປະເພດການແຈ້ງເຕືອນ"
|
||||
edit: "ແກ້ໄຂ"
|
||||
email: "ອີເມວ"
|
||||
smtpHost: "ໂຮດສ"
|
||||
smtpHost: "ໂຮສຕ໌"
|
||||
smtpUser: "ຊື່ຜູ້ໃຊ້"
|
||||
smtpPass: "ລະຫັດຜ່ານ"
|
||||
clearCache: "ລຶບລ້າງແຄສ"
|
||||
|
@ -393,12 +393,12 @@ administration: "ການຈັດການ"
|
|||
middle: "ປານກາງ"
|
||||
searchByGoogle: "ຄົ້ນຫາ"
|
||||
file: "ໄຟລ໌"
|
||||
replies: "ຕອບໄປທີ"
|
||||
replies: "ຕອບກັບ"
|
||||
renotes: "Renote"
|
||||
_delivery:
|
||||
stop: "ໂຈະ"
|
||||
_type:
|
||||
none: "ການພິມເຜີຍແຜ່"
|
||||
none: "ກຳລັງເຜີຍແພ່"
|
||||
_role:
|
||||
_priority:
|
||||
middle: "ປານກາງ"
|
||||
|
@ -416,8 +416,8 @@ _sfx:
|
|||
_2fa:
|
||||
renewTOTPCancel: "ບໍ່ແມ່ນຕອນນີ້"
|
||||
_widgets:
|
||||
profile: "ໂພຼຟາຍ"
|
||||
instanceInfo: "ອີນສະແຕນ"
|
||||
profile: "ໂປຣໄຟລ໌"
|
||||
instanceInfo: "ຂໍ້ມູລເຊີຟເວີຣ໌"
|
||||
notifications: "ການແຈ້ງເຕືອນ"
|
||||
timeline: "ເສັ້ນກຳນົດເວລາ"
|
||||
activity: "ກິດຈະກຳ"
|
||||
|
@ -436,28 +436,28 @@ _profile:
|
|||
_exportOrImport:
|
||||
followingList: "ກຳລັງຕິດຕາມ"
|
||||
muteList: "ປີດສຽງ"
|
||||
blockingList: "ບ໋ອກ"
|
||||
blockingList: "ບລັອກ"
|
||||
userLists: "ລາຍການ"
|
||||
_charts:
|
||||
federation: "ສະຫະພັນ"
|
||||
_timelines:
|
||||
home: "ໜ້າຫຼັກ"
|
||||
_play:
|
||||
script: "ບົດຄວາມ"
|
||||
script: "Script"
|
||||
summary: "ລາຍລະອຽດ"
|
||||
_pages:
|
||||
blocks:
|
||||
image: "ຮູບພາບ"
|
||||
_notification:
|
||||
youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ"
|
||||
youWereFollowed: "ໄດ້ຕິດຕາມເຈົ້າ"
|
||||
_types:
|
||||
follow: "ກຳລັງຕິດຕາມ"
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
mention: "ໄດ້ກ່າວເຖິງ"
|
||||
renote: "Renote"
|
||||
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
|
||||
reaction: "ປະຕິກິລິຍາ"
|
||||
quote: "ອ້າງອີງ"
|
||||
reaction: "Reaction"
|
||||
_actions:
|
||||
reply: "ຕອບໄປທີ"
|
||||
reply: "ຕອບກັບ"
|
||||
renote: "Renote"
|
||||
_deck:
|
||||
_columns:
|
||||
|
@ -465,8 +465,12 @@ _deck:
|
|||
tl: "ເສັ້ນກຳນົດເວລາ"
|
||||
list: "ລາຍການ"
|
||||
channel: "ຊ່ອງ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
mentions: "ກ່າວເຖິງເຈົ້າ"
|
||||
_webhookSettings:
|
||||
name: "ຊື່"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "ອີເມວ"
|
||||
_moderationLogTypes:
|
||||
suspend: "ລະງັບ"
|
||||
|
|
|
@ -251,7 +251,7 @@ upload: "Uploaden"
|
|||
keepOriginalUploading: "Origineel beeld behouden."
|
||||
keepOriginalUploadingDescription: "Bewaar de originele versie bij het uploaden van afbeeldingen. Indien uitgeschakeld, wordt bij het uploaden een alternatieve versie voor webpublicatie genereert."
|
||||
fromDrive: "Van schijf"
|
||||
fromUrl: "Van URL"
|
||||
fromUrl: "Van URL"
|
||||
uploadFromUrl: "Uploaden vanaf een URL"
|
||||
uploadFromUrlDescription: "URL van het bestand dat je wil uploaden"
|
||||
uploadFromUrlRequested: "Uploadverzoek"
|
||||
|
|
|
@ -721,5 +721,9 @@ _deck:
|
|||
direct: "Direkte"
|
||||
_webhookSettings:
|
||||
name: "Navn"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "E-post"
|
||||
_moderationLogTypes:
|
||||
suspend: "Suspender"
|
||||
|
|
|
@ -865,7 +865,7 @@ whatIsNew: "Pokaż zmiany"
|
|||
translate: "Przetłumacz"
|
||||
translatedFrom: "Przetłumaczone z {x}"
|
||||
accountDeletionInProgress: "Trwa usuwanie konta"
|
||||
usernameInfo: "Nazwa, która identyfikuje Twoje konto spośród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreślników (_). Nazwy użytkownika nie mogą być później zmieniane."
|
||||
usernameInfo: "Nazwa, która identyfikuje Twoje konto spośród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreślników (_). Nazwy użytkownika nie mogą być później zmieniane."
|
||||
aiChanMode: "Tryb Ai"
|
||||
devMode: "Tryb programisty"
|
||||
keepCw: "Zostaw ostrzeżenia o zawartości"
|
||||
|
@ -1221,8 +1221,6 @@ _sfx:
|
|||
note: "Wpisy"
|
||||
noteMy: "Mój wpis"
|
||||
notification: "Powiadomienia"
|
||||
antenna: "Anteny"
|
||||
channel: "Powiadomienia kanału"
|
||||
_ago:
|
||||
future: "W przyszłości"
|
||||
justNow: "Przed chwilą"
|
||||
|
@ -1546,7 +1544,6 @@ _webhookSettings:
|
|||
createWebhook: "Stwórz Webhook"
|
||||
name: "Nazwa"
|
||||
secret: "Sekret"
|
||||
events: "Uruchomienie Webhooka"
|
||||
active: "Właczono"
|
||||
_events:
|
||||
follow: "Po zaobserwowaniu użytkownika"
|
||||
|
@ -1556,6 +1553,10 @@ _webhookSettings:
|
|||
renote: "Po udostępnieniu wpisu"
|
||||
reaction: "Po otrzymaniu reakcji"
|
||||
mention: "Po zostaniu wspomnianym"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Adres e-mail"
|
||||
_moderationLogTypes:
|
||||
suspend: "Zawieś"
|
||||
resetPassword: "Zresetuj hasło"
|
||||
|
|
|
@ -248,7 +248,7 @@ done: "Concluído"
|
|||
processing: "Em Progresso"
|
||||
preview: "Pré-visualizar"
|
||||
default: "Predefinição"
|
||||
defaultValueIs: "Predefinição: {value}"
|
||||
defaultValueIs: "Predefinição: {value}"
|
||||
noCustomEmojis: "Não há emojis"
|
||||
noJobs: "Não há tarefas"
|
||||
federating: "Federando"
|
||||
|
@ -2267,7 +2267,7 @@ _timelines:
|
|||
social: "Social"
|
||||
global: "Global"
|
||||
_play:
|
||||
new: "Criar Play"
|
||||
new: "Criar Play"
|
||||
edit: "Editar Play"
|
||||
created: "Play criado"
|
||||
updated: "Play editado"
|
||||
|
|
|
@ -728,6 +728,10 @@ _deck:
|
|||
mentions: "Mențiuni"
|
||||
_webhookSettings:
|
||||
name: "Nume"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
_moderationLogTypes:
|
||||
suspend: "Suspendă"
|
||||
resetPassword: "Resetează parola"
|
||||
|
|
|
@ -1612,8 +1612,6 @@ _sfx:
|
|||
note: "Заметки"
|
||||
noteMy: "Собственные заметки"
|
||||
notification: "Уведомления"
|
||||
antenna: "Антенна"
|
||||
channel: "Канал"
|
||||
_ago:
|
||||
future: "Из будущего"
|
||||
justNow: "Только что"
|
||||
|
@ -1983,6 +1981,10 @@ _webhookSettings:
|
|||
createWebhook: "Создать вебхук"
|
||||
name: "Название"
|
||||
active: "Вкл."
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Электронная почта"
|
||||
_moderationLogTypes:
|
||||
suspend: "Заморозить"
|
||||
addCustomEmoji: "Добавлено эмодзи"
|
||||
|
|
|
@ -1124,8 +1124,6 @@ _sfx:
|
|||
note: "Poznámky"
|
||||
noteMy: "Vlastná poznámka"
|
||||
notification: "Oznámenia"
|
||||
antenna: "Antény"
|
||||
channel: "Upozornenia kanála"
|
||||
_ago:
|
||||
future: "Budúcnosť"
|
||||
justNow: "Teraz"
|
||||
|
@ -1447,6 +1445,10 @@ _deck:
|
|||
_webhookSettings:
|
||||
name: "Názov"
|
||||
active: "Zapnuté"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
_moderationLogTypes:
|
||||
suspend: "Zmraziť"
|
||||
resetPassword: "Resetovať heslo"
|
||||
|
|
|
@ -512,7 +512,6 @@ _theme:
|
|||
_sfx:
|
||||
note: "Noter"
|
||||
notification: "Notifikationer"
|
||||
antenna: "Antenner"
|
||||
_2fa:
|
||||
renewTOTPCancel: "Nej tack"
|
||||
_antennaSources:
|
||||
|
@ -577,6 +576,10 @@ _deck:
|
|||
_webhookSettings:
|
||||
name: "Namn"
|
||||
active: "Aktiverad"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "E-post"
|
||||
_moderationLogTypes:
|
||||
suspend: "Suspendera"
|
||||
resetPassword: "Återställ Lösenord"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -359,7 +359,7 @@ smtpUser: "Kullanıcı Adı"
|
|||
smtpPass: "Şifre"
|
||||
notificationSetting: "Bildirim ayarları"
|
||||
instanceTicker: "Notların sunucu bilgileri"
|
||||
noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et."
|
||||
noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et."
|
||||
clearCache: "Ön belleği temizle"
|
||||
onlineUsersCount: "{n} kullanıcı çevrim içi"
|
||||
user: "Kullanıcı"
|
||||
|
|
|
@ -1318,8 +1318,6 @@ _sfx:
|
|||
note: "Нотатки"
|
||||
noteMy: "Мої нотатки"
|
||||
notification: "Сповіщення"
|
||||
antenna: "Прийом антени"
|
||||
channel: "Повідомлення каналу"
|
||||
_ago:
|
||||
future: "Майбутнє"
|
||||
justNow: "Щойно"
|
||||
|
@ -1622,6 +1620,10 @@ _deck:
|
|||
_webhookSettings:
|
||||
name: "Ім'я"
|
||||
active: "Увімкнено"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "E-mail"
|
||||
_moderationLogTypes:
|
||||
suspend: "Призупинити"
|
||||
resetPassword: "Скинути пароль"
|
||||
|
|
|
@ -443,7 +443,7 @@ text: "Matn"
|
|||
enable: "Yoqish"
|
||||
next: "Keyingisi"
|
||||
retype: "Qayta kiriting"
|
||||
noteOf: "{user} tomonidan joylandi\n"
|
||||
noteOf: "{user} tomonidan joylandi\n"
|
||||
quoteAttached: "Iqtibos"
|
||||
quoteQuestion: "Iqtibos sifatida qo'shilsinmi?"
|
||||
noMessagesYet: "Bu yerda xabarlar yo'q"
|
||||
|
@ -1089,6 +1089,10 @@ _webhookSettings:
|
|||
_events:
|
||||
renote: "Qayta qayd qilinganda"
|
||||
mention: "Eslanganda"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
_moderationLogTypes:
|
||||
suspend: "To'xtatish"
|
||||
resetPassword: "Parolni tiklash"
|
||||
|
|
|
@ -376,6 +376,7 @@ mcaptcha: "mCaptcha"
|
|||
enableMcaptcha: "Bật mCaptcha"
|
||||
mcaptchaSiteKey: "Khóa của trang"
|
||||
mcaptchaSecretKey: "Khóa bí mật"
|
||||
mcaptchaInstanceUrl: "URL mCaptcha máy chủ"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Bật reCAPTCHA"
|
||||
recaptchaSiteKey: "Khóa của trang"
|
||||
|
@ -426,6 +427,7 @@ moderator: "Kiểm duyệt viên"
|
|||
moderation: "Kiểm duyệt"
|
||||
moderationNote: "Ghi chú kiểm duyệt"
|
||||
addModerationNote: "Thêm ghi chú kiểm duyệt"
|
||||
moderationLogs: "Nhật kí quản trị"
|
||||
nUsersMentioned: "Dùng bởi {n} người"
|
||||
securityKeyAndPasskey: "Mã bảo mật・Passkey"
|
||||
securityKey: "Khóa bảo mật"
|
||||
|
@ -458,6 +460,7 @@ retype: "Nhập lại"
|
|||
noteOf: "Tút của {user}"
|
||||
quoteAttached: "Trích dẫn"
|
||||
quoteQuestion: "Trích dẫn lại?"
|
||||
attachAsFileQuestion: "Văn bản ở trong bộ nhớ tạm rất dài. Bạn có muốn đăng nó dưới dạng một tệp văn bản không?"
|
||||
noMessagesYet: "Chưa có tin nhắn"
|
||||
newMessageExists: "Bạn có tin nhắn mới"
|
||||
onlyOneFileCanBeAttached: "Bạn chỉ có thể đính kèm một tập tin"
|
||||
|
@ -1285,7 +1288,7 @@ _achievements:
|
|||
_iLoveMisskey:
|
||||
title: "Tôi Yêu Misskey"
|
||||
description: "Đăng lời nói \"I ❤ #Misskey\""
|
||||
flavor: "Xin chân thành cảm ơn bạn đã sử dụng Misskey!! by Đội ngũ phát triển"
|
||||
flavor: "Xin chân thành cảm ơn bạn đã sử dụng Misskey!! by Đội ngũ phát triển"
|
||||
_foundTreasure:
|
||||
title: "Tìm kiếm kho báu"
|
||||
description: "Tìm thấy được những kho báu cất giấu"
|
||||
|
@ -1326,7 +1329,7 @@ _achievements:
|
|||
description: "Bấm chỗ này"
|
||||
_justPlainLucky:
|
||||
title: "Chỉ là một cuộc máy mắn"
|
||||
description: "Mỗi 10 giây thu nhận được với tỷ lệ 0.005%"
|
||||
description: "Mỗi 10 giây thu nhận được với tỷ lệ 0.005%"
|
||||
_setNameToSyuilo:
|
||||
title: "Ngưỡng mộ với vị thần"
|
||||
description: "Đạt tên là syuilo"
|
||||
|
@ -1480,7 +1483,7 @@ _wordMute:
|
|||
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
|
||||
muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng."
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn."
|
||||
instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn."
|
||||
instanceMuteDescription2: "Tách bằng cách xuống dòng"
|
||||
title: "Ẩn tút từ những máy chủ đã liệt kê."
|
||||
heading: "Danh sách những máy chủ bị ẩn"
|
||||
|
@ -1559,8 +1562,6 @@ _sfx:
|
|||
note: "Tút"
|
||||
noteMy: "Tút của tôi"
|
||||
notification: "Thông báo"
|
||||
antenna: "Trạm phát sóng"
|
||||
channel: "Kênh"
|
||||
_ago:
|
||||
future: "Tương lai"
|
||||
justNow: "Vừa xong"
|
||||
|
@ -1917,11 +1918,14 @@ _webhookSettings:
|
|||
createWebhook: "Tạo Webhook"
|
||||
name: "Tên"
|
||||
secret: "Mã bí mật"
|
||||
events: "Sự kiện Webhook"
|
||||
active: "Đã bật"
|
||||
_events:
|
||||
reaction: "Khi nhận được sự kiện"
|
||||
mention: "Khi có người nhắc tới bạn"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
_moderationLogTypes:
|
||||
suspend: "Vô hiệu hóa"
|
||||
resetPassword: "Đặt lại mật khẩu"
|
||||
|
|
|
@ -60,6 +60,7 @@ copyFileId: "复制文件ID"
|
|||
copyFolderId: "复制文件夹ID"
|
||||
copyProfileUrl: "复制个人资料URL"
|
||||
searchUser: "搜索用户"
|
||||
searchThisUsersNotes: "搜索用户帖子"
|
||||
reply: "回复"
|
||||
loadMore: "查看更多"
|
||||
showMore: "查看更多"
|
||||
|
@ -154,6 +155,7 @@ editList: "编辑列表"
|
|||
selectChannel: "选择频道"
|
||||
selectAntenna: "选择天线"
|
||||
editAntenna: "编辑天线"
|
||||
createAntenna: "创建天线"
|
||||
selectWidget: "选择小工具"
|
||||
editWidgets: "编辑部件"
|
||||
editWidgetsExit: "完成编辑"
|
||||
|
@ -180,6 +182,10 @@ addAccount: "添加账户"
|
|||
reloadAccountsList: "更新账户列表"
|
||||
loginFailed: "登录失败"
|
||||
showOnRemote: "转到所在服务器显示"
|
||||
continueOnRemote: "转到所在服务器继续"
|
||||
chooseServerOnMisskeyHub: "从 Misskey Hub 选择服务器"
|
||||
specifyServerHost: "直接输入服务器域名"
|
||||
inputHostName: "请输入域名"
|
||||
general: "常规设置"
|
||||
wallpaper: "壁纸"
|
||||
setWallpaper: "设置壁纸"
|
||||
|
@ -190,6 +196,7 @@ followConfirm: "你确定要关注 {name} 吗?"
|
|||
proxyAccount: "代理账户"
|
||||
proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
|
||||
host: "主机名"
|
||||
selectSelf: "选择自己"
|
||||
selectUser: "选择用户"
|
||||
recipient: "收件人"
|
||||
annotation: "注解"
|
||||
|
@ -205,6 +212,7 @@ perDay: "每天"
|
|||
stopActivityDelivery: "停止发送活动"
|
||||
blockThisInstance: "阻止此服务器向本服务器推流"
|
||||
silenceThisInstance: "使服务器静音"
|
||||
mediaSilenceThisInstance: "隐藏此服务器的媒体文件"
|
||||
operations: "操作"
|
||||
software: "软件"
|
||||
version: "版本"
|
||||
|
@ -223,9 +231,11 @@ clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执
|
|||
clearCachedFiles: "清除缓存"
|
||||
clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?"
|
||||
blockedInstances: "被封锁的服务器"
|
||||
blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
|
||||
blockedInstancesDescription: "设定要封锁的服务器,以换行分隔。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
|
||||
silencedInstances: "被静音的服务器"
|
||||
silencedInstancesDescription: "设置要静音的服务器,以换行符分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
|
||||
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
|
||||
mediaSilencedInstances: "已隐藏媒体文件的服务器"
|
||||
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
|
||||
muteAndBlock: "静音/拉黑"
|
||||
mutedUsers: "已静音用户"
|
||||
blockedUsers: "已拉黑的用户"
|
||||
|
@ -351,7 +361,7 @@ instanceName: "服务器名称"
|
|||
instanceDescription: "服务器简介"
|
||||
maintainerName: "管理员名称"
|
||||
maintainerEmail: "管理员电子邮箱"
|
||||
tosUrl: "服务条款 URL"
|
||||
tosUrl: "服务条款地址"
|
||||
thisYear: "今年"
|
||||
thisMonth: "本月"
|
||||
today: "今天"
|
||||
|
@ -433,8 +443,8 @@ administrator: "管理员"
|
|||
token: "Token (令牌)"
|
||||
2fa: "双因素认证"
|
||||
setupOf2fa: "设置双因素认证"
|
||||
totp: "身份验证应用"
|
||||
totpDescription: "使用认证应用输入一次性密码。"
|
||||
totp: "验证器"
|
||||
totpDescription: "使用验证器输入一次性密码"
|
||||
moderator: "监察员"
|
||||
moderation: "管理"
|
||||
moderationNote: "管理笔记"
|
||||
|
@ -477,6 +487,7 @@ noMessagesYet: "现在没有新的聊天"
|
|||
newMessageExists: "新信息"
|
||||
onlyOneFileCanBeAttached: "只能添加一个附件"
|
||||
signinRequired: "请先登录"
|
||||
signinOrContinueOnRemote: "若要继续,需要转到您所使用的实例,或者在此服务器上注册或登录。"
|
||||
invitations: "邀请"
|
||||
invitationCode: "邀请码"
|
||||
checking: "正在确认"
|
||||
|
@ -837,6 +848,7 @@ administration: "管理"
|
|||
accounts: "账户"
|
||||
switch: "切换"
|
||||
noMaintainerInformationWarning: "管理人员信息未设置。"
|
||||
noInquiryUrlWarning: "尚未设置联络地址。"
|
||||
noBotProtectionWarning: "Bot 防御未设置。"
|
||||
configure: "设置"
|
||||
postToGallery: "发送到图库"
|
||||
|
@ -848,7 +860,7 @@ shareWithNote: "在帖子中分享"
|
|||
ads: "广告"
|
||||
expiration: "截止时间"
|
||||
startingperiod: "开始时间"
|
||||
memo: "便笺"
|
||||
memo: "备注"
|
||||
priority: "优先级"
|
||||
high: "高"
|
||||
middle: "中"
|
||||
|
@ -1100,6 +1112,8 @@ preservedUsernames: "保留的用户名"
|
|||
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
|
||||
createNoteFromTheFile: "从文件创建帖子"
|
||||
archive: "归档"
|
||||
archived: "已归档"
|
||||
unarchive: "取消归档"
|
||||
channelArchiveConfirmTitle: "要将 {name} 归档吗?"
|
||||
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
|
||||
thisChannelArchived: "该频道已被归档。"
|
||||
|
@ -1110,6 +1124,9 @@ preventAiLearning: "拒绝接受生成式 AI 的学习"
|
|||
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
|
||||
options: "选项"
|
||||
specifyUser: "用户指定"
|
||||
lookupConfirm: "确定查询?"
|
||||
openTagPageConfirm: "确定打开话题标签页面?"
|
||||
specifyHost: "指定主机名"
|
||||
failedToPreviewUrl: "无法预览"
|
||||
update: "更新"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
|
||||
|
@ -1180,7 +1197,7 @@ externalServices: "外部服务"
|
|||
sourceCode: "源代码"
|
||||
sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
|
||||
repositoryUrl: "仓库地址"
|
||||
repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若是按原样使用 Misskey(并未追加或者修改代码)的情况请填入 https://github.com/misskey-dev/misskey。"
|
||||
repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若并未追加或者修改 Misskey 的代码,请填入 https://github.com/misskey-dev/misskey。"
|
||||
repositoryUrlOrTarballRequired: "若仓库并未公开,则需要提供 tarball 作为替代。详情请看 .config/example.yml。"
|
||||
feedback: "反馈"
|
||||
feedbackUrl: "反馈地址"
|
||||
|
@ -1241,6 +1258,11 @@ keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名
|
|||
noDescription: "没有描述"
|
||||
alwaysConfirmFollow: "总是确认关注"
|
||||
inquiry: "联系我们"
|
||||
tryAgain: "请再试一次"
|
||||
confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认"
|
||||
sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?"
|
||||
createdLists: "已创建的列表"
|
||||
createdAntennas: "已创建的天线"
|
||||
_delivery:
|
||||
status: "投递状态"
|
||||
stop: "停止投递"
|
||||
|
@ -1375,6 +1397,8 @@ _serverSettings:
|
|||
fanoutTimelineDescription: "当启用时,可显著提高获取各种时间线时的性能,并减轻数据库的负荷。但是相对的 Redis 的内存使用量将会增加。如果服务器的内存不是很大,又或者运行不稳定的话可以把它关掉。"
|
||||
fanoutTimelineDbFallback: "回退到数据库"
|
||||
fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。"
|
||||
inquiryUrl: "联络地址"
|
||||
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
|
||||
_accountMigration:
|
||||
moveFrom: "从别的账号迁移到此账户"
|
||||
moveFromSub: "为另一个账户建立别名"
|
||||
|
@ -1670,8 +1694,8 @@ _role:
|
|||
descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。"
|
||||
displayOrder: "显示顺序"
|
||||
descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
|
||||
canEditMembersByModerator: "允许监察者编辑成员"
|
||||
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
||||
canEditMembersByModerator: "允许监察员编辑成员"
|
||||
descriptionOfCanEditMembersByModerator: "如果选中,监察员和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
||||
priority: "优先级"
|
||||
_priority:
|
||||
low: "低"
|
||||
|
@ -1690,6 +1714,7 @@ _role:
|
|||
canManageAvatarDecorations: "管理头像挂件"
|
||||
driveCapacity: "网盘容量"
|
||||
alwaysMarkNsfw: "总是将文件标记为 NSFW"
|
||||
canUpdateBioMedia: "可以更新头像和横幅"
|
||||
pinMax: "帖子置顶数量限制"
|
||||
antennaMax: "可创建的最大天线数量"
|
||||
wordMuteMax: "屏蔽词的字数限制"
|
||||
|
@ -1933,8 +1958,6 @@ _sfx:
|
|||
note: "帖子"
|
||||
noteMy: "我的帖子"
|
||||
notification: "通知"
|
||||
antenna: "天线接收"
|
||||
channel: "频道通知"
|
||||
reaction: "选择回应时"
|
||||
_soundSettings:
|
||||
driveFile: "使用网盘内的音频"
|
||||
|
@ -1943,6 +1966,7 @@ _soundSettings:
|
|||
driveFileTypeWarnDescription: "请选择音频文件"
|
||||
driveFileDurationWarn: "音频过长"
|
||||
driveFileDurationWarnDescription: "使用长音频可能会影响 Misskey 的使用。即使这样也要继续吗?"
|
||||
driveFileError: "无法读取声音。请更改设置。"
|
||||
_ago:
|
||||
future: "未来"
|
||||
justNow: "最近"
|
||||
|
@ -1969,7 +1993,7 @@ _time:
|
|||
day: "日"
|
||||
_2fa:
|
||||
alreadyRegistered: "此设备已被注册"
|
||||
registerTOTP: "开始设置认证应用"
|
||||
registerTOTP: "开始设置验证器"
|
||||
step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
|
||||
step2: "然后,扫描屏幕上显示的二维码。"
|
||||
step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI"
|
||||
|
@ -1978,23 +2002,23 @@ _2fa:
|
|||
setupCompleted: "设置完成"
|
||||
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
|
||||
securityKeyNotSupported: "您的浏览器不支持安全密钥。"
|
||||
registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器应用程序。"
|
||||
registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器。"
|
||||
securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
|
||||
registerSecurityKey: "注册安全密钥或 Passkey"
|
||||
securityKeyName: "输入密钥名称"
|
||||
tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。"
|
||||
removeKey: "删除安全密钥"
|
||||
removeKeyConfirm: "您确定要删除 {name} 吗?"
|
||||
whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。"
|
||||
renewTOTP: "重置验证器应用程序"
|
||||
renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效"
|
||||
whyTOTPOnlyRenew: "当注册了安全密钥时,无法取消使用验证器。"
|
||||
renewTOTP: "重置验证器"
|
||||
renewTOTPConfirm: "当前验证器的验证码及备用代码已失效"
|
||||
renewTOTPOk: "重新配置"
|
||||
renewTOTPCancel: "不用,谢谢"
|
||||
checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码"
|
||||
backupCodes: "备用代码"
|
||||
backupCodesDescription: "如果无法使用认证应用,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
|
||||
backupCodeUsedWarning: "已使用备用代码。如果无法使用认证应用,请尽快重新设定。"
|
||||
backupCodesExhaustedWarning: "已使用完所有的备用代码。如果无法使用认证应用,将无法再访问您的账户。请再次设定认证应用。"
|
||||
backupCodesDescription: "如果无法使用验证器,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
|
||||
backupCodeUsedWarning: "已使用备用代码。若验证器无法使用,请尽快重置验证器。"
|
||||
backupCodesExhaustedWarning: "已使用完所有的备用代码。若验证器无法使用,则无法再访问您的账户。请重置验证器。"
|
||||
moreDetailedGuideHere: "此处为详细指南"
|
||||
_permissions:
|
||||
"read:account": "查看账户信息"
|
||||
|
@ -2398,9 +2422,10 @@ _drivecleaner:
|
|||
orderByCreatedAtAsc: "按添加日期降序排列"
|
||||
_webhookSettings:
|
||||
createWebhook: "创建 Webhook"
|
||||
modifyWebhook: "编辑 webhook"
|
||||
name: "名称"
|
||||
secret: "密钥"
|
||||
events: "何时运行 Webhook"
|
||||
trigger: "触发器"
|
||||
active: "已启用"
|
||||
_events:
|
||||
follow: "关注时"
|
||||
|
@ -2410,6 +2435,26 @@ _webhookSettings:
|
|||
renote: "被转发时"
|
||||
reaction: "被回应时"
|
||||
mention: "被提及时"
|
||||
_systemEvents:
|
||||
abuseReport: "当收到举报时"
|
||||
abuseReportResolved: "当举报被处理时"
|
||||
userCreated: "当用户被创建时"
|
||||
deleteConfirm: "要删除 webhook 吗?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "新建举报通知"
|
||||
modifyRecipient: "编辑举报通知"
|
||||
recipientType: "通知类型"
|
||||
_recipientType:
|
||||
mail: "邮箱"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "当收到新举报时,向持有监察员权限的用户发送通知邮件"
|
||||
webhook: "当收到新举报及举报被处理时,使用指定的 SystemWebhook 发送通知"
|
||||
keywords: "关键字"
|
||||
notifiedUser: "通知的用户"
|
||||
notifiedWebhook: "使用的 webhook"
|
||||
deleteConfirm: "要删除通知吗?"
|
||||
_moderationLogTypes:
|
||||
createRole: "创建角色"
|
||||
deleteRole: "删除角色"
|
||||
|
@ -2447,6 +2492,12 @@ _moderationLogTypes:
|
|||
deleteAvatarDecoration: "删除头像挂件"
|
||||
unsetUserAvatar: "清除用户头像"
|
||||
unsetUserBanner: "清除用户横幅"
|
||||
createSystemWebhook: "新建了 SystemWebhook"
|
||||
updateSystemWebhook: "更新了 SystemWebhook"
|
||||
deleteSystemWebhook: "删除了 SystemWebhook"
|
||||
createAbuseReportNotificationRecipient: "新建了举报通知"
|
||||
updateAbuseReportNotificationRecipient: "更新了举报通知"
|
||||
deleteAbuseReportNotificationRecipient: "删除了举报通知"
|
||||
_fileViewer:
|
||||
title: "文件信息"
|
||||
type: "文件类型"
|
||||
|
@ -2578,3 +2629,8 @@ _mediaControls:
|
|||
pip: "画中画"
|
||||
playbackRate: "播放速度"
|
||||
loop: "循环播放"
|
||||
_contextMenu:
|
||||
title: "上下文菜单"
|
||||
app: "应用"
|
||||
appWithShift: "Shift 键应用"
|
||||
native: "浏览器的用户界面"
|
||||
|
|
|
@ -60,6 +60,7 @@ copyFileId: "複製檔案 ID"
|
|||
copyFolderId: "複製資料夾ID"
|
||||
copyProfileUrl: "複製個人資料網址"
|
||||
searchUser: "搜尋使用者"
|
||||
searchThisUsersNotes: "搜尋這個使用者的貼文"
|
||||
reply: "回覆"
|
||||
loadMore: "載入更多"
|
||||
showMore: "載入更多"
|
||||
|
@ -154,6 +155,7 @@ editList: "編輯清單"
|
|||
selectChannel: "選擇頻道"
|
||||
selectAntenna: "選擇天線"
|
||||
editAntenna: "編輯天線"
|
||||
createAntenna: "建立天線"
|
||||
selectWidget: "選擇小工具"
|
||||
editWidgets: "編輯小工具"
|
||||
editWidgetsExit: "完成"
|
||||
|
@ -180,6 +182,10 @@ addAccount: "新增帳戶"
|
|||
reloadAccountsList: "更新帳戶清單的資訊"
|
||||
loginFailed: "登入失敗"
|
||||
showOnRemote: "轉到所在實例顯示"
|
||||
continueOnRemote: "在遠端伺服器繼續"
|
||||
chooseServerOnMisskeyHub: "從 Misskey Hub 選擇伺服器"
|
||||
specifyServerHost: "直接指定伺服器網域"
|
||||
inputHostName: "請輸入域名"
|
||||
general: "一般"
|
||||
wallpaper: "桌布"
|
||||
setWallpaper: "設定桌布"
|
||||
|
@ -190,6 +196,7 @@ followConfirm: "你真的要追隨{name}嗎?"
|
|||
proxyAccount: "代理帳戶"
|
||||
proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。"
|
||||
host: "主機"
|
||||
selectSelf: "選擇自己"
|
||||
selectUser: "選取使用者"
|
||||
recipient: "收件人"
|
||||
annotation: "註解"
|
||||
|
@ -205,6 +212,7 @@ perDay: "每日"
|
|||
stopActivityDelivery: "停止發送活動"
|
||||
blockThisInstance: "封鎖此伺服器"
|
||||
silenceThisInstance: "禁言此伺服器"
|
||||
mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
|
||||
operations: "操作"
|
||||
software: "軟體"
|
||||
version: "版本"
|
||||
|
@ -226,6 +234,8 @@ blockedInstances: "已封鎖的伺服器"
|
|||
blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
|
||||
silencedInstances: "被禁言的伺服器"
|
||||
silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
|
||||
mediaSilencedInstances: "媒體被禁言的伺服器"
|
||||
mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。"
|
||||
muteAndBlock: "靜音和封鎖"
|
||||
mutedUsers: "被靜音的使用者"
|
||||
blockedUsers: "被封鎖的使用者"
|
||||
|
@ -243,10 +253,10 @@ noCustomEmojis: "沒有自訂的表情符號"
|
|||
noJobs: "沒有任務"
|
||||
federating: "聯邦運作中"
|
||||
blocked: "已封鎖"
|
||||
suspended: "已凍結"
|
||||
suspended: "停止發送"
|
||||
all: "全部"
|
||||
subscribing: "訂閱中"
|
||||
publishing: "直播中"
|
||||
publishing: "發送中"
|
||||
notResponding: "沒有回應"
|
||||
instanceFollowing: "追隨的伺服器"
|
||||
instanceFollowers: "伺服器的追隨者"
|
||||
|
@ -343,7 +353,7 @@ reload: "重新整理"
|
|||
doNothing: "無視"
|
||||
reloadConfirm: "確定要重新整理嗎?"
|
||||
watch: "關注"
|
||||
unwatch: "取消追隨"
|
||||
unwatch: "取消關注"
|
||||
accept: "接受"
|
||||
reject: "拒絕"
|
||||
normal: "正常"
|
||||
|
@ -477,6 +487,7 @@ noMessagesYet: "沒有訊息"
|
|||
newMessageExists: "有新的訊息"
|
||||
onlyOneFileCanBeAttached: "只能加入一個附件"
|
||||
signinRequired: "請先登入"
|
||||
signinOrContinueOnRemote: "若要繼續,需前往您所在的伺服器,或者註冊並登入此伺服器"
|
||||
invitations: "邀請"
|
||||
invitationCode: "邀請碼"
|
||||
checking: "確認中"
|
||||
|
@ -837,6 +848,7 @@ administration: "管理"
|
|||
accounts: "帳戶"
|
||||
switch: "切換"
|
||||
noMaintainerInformationWarning: "尚未設定管理員訊息。"
|
||||
noInquiryUrlWarning: "尚未設定聯絡表單網址。"
|
||||
noBotProtectionWarning: "尚未設定 Bot 防護。"
|
||||
configure: "設定"
|
||||
postToGallery: "發佈到相簿"
|
||||
|
@ -1100,6 +1112,8 @@ preservedUsernames: "保留的使用者名稱"
|
|||
preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。"
|
||||
createNoteFromTheFile: "由此檔案建立貼文"
|
||||
archive: "封存"
|
||||
archived: "已封存"
|
||||
unarchive: "取消封存"
|
||||
channelArchiveConfirmTitle: "要封存{name}嗎?"
|
||||
channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。"
|
||||
thisChannelArchived: "這個頻道已被封存。"
|
||||
|
@ -1110,6 +1124,9 @@ preventAiLearning: "拒絕接受生成式AI的訓練"
|
|||
preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。"
|
||||
options: "選項"
|
||||
specifyUser: "指定使用者"
|
||||
lookupConfirm: "要查詢嗎?"
|
||||
openTagPageConfirm: "要開啟標籤的頁面嗎?"
|
||||
specifyHost: "指定主機"
|
||||
failedToPreviewUrl: "無法預覽"
|
||||
update: "更新"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色"
|
||||
|
@ -1241,12 +1258,17 @@ keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱
|
|||
noDescription: "沒有說明文字"
|
||||
alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
|
||||
inquiry: "聯絡我們"
|
||||
tryAgain: "請再試一次。"
|
||||
confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
|
||||
sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?"
|
||||
createdLists: "已建立的清單"
|
||||
createdAntennas: "已建立的天線"
|
||||
_delivery:
|
||||
status: "傳送狀態"
|
||||
stop: "停止傳送"
|
||||
resume: "恢復傳送"
|
||||
stop: "停止發送"
|
||||
resume: "恢復發送"
|
||||
_type:
|
||||
none: "直播中"
|
||||
none: "發送中"
|
||||
manuallySuspended: "手動暫停中"
|
||||
goneSuspended: "因為伺服器刪除所以暫停中"
|
||||
autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
|
||||
|
@ -1376,7 +1398,7 @@ _serverSettings:
|
|||
fanoutTimelineDbFallback: "資料庫的回退"
|
||||
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
|
||||
inquiryUrl: "聯絡表單網址"
|
||||
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
|
||||
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
|
||||
_accountMigration:
|
||||
moveFrom: "從其他帳戶遷移到這個帳戶"
|
||||
moveFromSub: "為另一個帳戶建立別名"
|
||||
|
@ -1693,6 +1715,7 @@ _role:
|
|||
canManageAvatarDecorations: "管理頭像裝飾"
|
||||
driveCapacity: "雲端硬碟容量"
|
||||
alwaysMarkNsfw: "總是將檔案標記為NSFW"
|
||||
canUpdateBioMedia: "允許更新大頭貼和橫幅"
|
||||
pinMax: "置頂貼文的最大數量"
|
||||
antennaMax: "可建立的天線數量"
|
||||
wordMuteMax: "靜音文字的最大字數"
|
||||
|
@ -1936,8 +1959,6 @@ _sfx:
|
|||
note: "貼文"
|
||||
noteMy: "我的貼文"
|
||||
notification: "通知"
|
||||
antenna: "天線接收"
|
||||
channel: "頻道通知"
|
||||
reaction: "選擇反應時"
|
||||
_soundSettings:
|
||||
driveFile: "使用雲端硬碟的音效檔案"
|
||||
|
@ -1946,6 +1967,7 @@ _soundSettings:
|
|||
driveFileTypeWarnDescription: "請選擇音效檔案"
|
||||
driveFileDurationWarn: "音效太長了"
|
||||
driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?"
|
||||
driveFileError: "無法載入語音。請更改設定"
|
||||
_ago:
|
||||
future: "未來"
|
||||
justNow: "剛剛"
|
||||
|
@ -2217,7 +2239,7 @@ _charts:
|
|||
federation: "聯邦宇宙"
|
||||
apRequest: "請求"
|
||||
usersIncDec: "使用者增減"
|
||||
usersTotal: "使用者總數"
|
||||
usersTotal: "使用者合計"
|
||||
activeUsers: "活躍使用者"
|
||||
notesIncDec: "貼文増減"
|
||||
localNotesIncDec: "本地貼文増減"
|
||||
|
@ -2401,9 +2423,10 @@ _drivecleaner:
|
|||
orderByCreatedAtAsc: "按新增日期降序排列"
|
||||
_webhookSettings:
|
||||
createWebhook: "建立 Webhook"
|
||||
modifyWebhook: "編輯 Webhook"
|
||||
name: "名字"
|
||||
secret: "密鑰"
|
||||
events: "何時運行 Webhook"
|
||||
trigger: "觸發器"
|
||||
active: "已啟用"
|
||||
_events:
|
||||
follow: "當你追隨時"
|
||||
|
@ -2413,6 +2436,25 @@ _webhookSettings:
|
|||
renote: "當被轉發時"
|
||||
reaction: "當獲得反應時"
|
||||
mention: "當被提到時"
|
||||
_systemEvents:
|
||||
abuseReport: "當使用者檢舉時"
|
||||
abuseReportResolved: "當處理了使用者的檢舉時"
|
||||
deleteConfirm: "請問是否要刪除 Webhook?"
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "新增接收檢舉的通知對象"
|
||||
modifyRecipient: "編輯接收檢舉的通知對象"
|
||||
recipientType: "通知對象的種類"
|
||||
_recipientType:
|
||||
mail: "電子郵件"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)"
|
||||
webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)"
|
||||
keywords: "關鍵字"
|
||||
notifiedUser: "被通知的使用者"
|
||||
notifiedWebhook: "使用的 Webhook"
|
||||
deleteConfirm: "確定要刪除通知對象嗎?"
|
||||
_moderationLogTypes:
|
||||
createRole: "新增角色"
|
||||
deleteRole: "刪除角色 "
|
||||
|
@ -2450,6 +2492,12 @@ _moderationLogTypes:
|
|||
deleteAvatarDecoration: "刪除頭像裝飾"
|
||||
unsetUserAvatar: "移除使用者的大頭貼"
|
||||
unsetUserBanner: "移除使用者的橫幅圖像"
|
||||
createSystemWebhook: "建立 SystemWebhook"
|
||||
updateSystemWebhook: "更新 SystemWebhook"
|
||||
deleteSystemWebhook: "刪除 SystemWebhook"
|
||||
createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
|
||||
updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
|
||||
deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
|
||||
_fileViewer:
|
||||
title: "檔案詳細資訊"
|
||||
type: "檔案類型 "
|
||||
|
@ -2581,3 +2629,7 @@ _mediaControls:
|
|||
pip: "畫中畫"
|
||||
playbackRate: "播放速度"
|
||||
loop: "循環播放"
|
||||
_contextMenu:
|
||||
title: "內容功能表"
|
||||
app: "應用程式"
|
||||
native: "瀏覽器的使用者介面"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5
|
32
package.json
32
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "sharkey",
|
||||
"version": "2024.6.0-dev",
|
||||
"version": "2024.7.0-rc",
|
||||
"codename": "shonk",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://activitypub.software/TransFem-org/Sharkey.git"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
|
@ -21,7 +21,7 @@
|
|||
"build-assets": "node ./scripts/build-assets.mjs",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"start": "pnpm check:connect && cd packages/backend && MK_WARNED_ABOUT_CONFIG=true node ./built/boot/entry.js",
|
||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"init": "pnpm migrate",
|
||||
|
@ -51,23 +51,25 @@
|
|||
"cssnano": "6.1.2",
|
||||
"execa": "8.0.1",
|
||||
"fast-glob": "3.3.2",
|
||||
"ignore-walk": "6.0.4",
|
||||
"ignore-walk": "6.0.5",
|
||||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.4.38",
|
||||
"postcss": "8.4.40",
|
||||
"tar": "6.2.1",
|
||||
"terser": "5.30.3",
|
||||
"typescript": "5.4.5",
|
||||
"esbuild": "0.20.2",
|
||||
"glob": "10.3.12"
|
||||
"terser": "5.31.3",
|
||||
"typescript": "5.5.4",
|
||||
"esbuild": "0.23.0",
|
||||
"glob": "11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.12.7",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
||||
"@typescript-eslint/parser": "7.7.1",
|
||||
"@misskey-dev/eslint-plugin": "2.0.2",
|
||||
"@types/node": "20.14.12",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.7.3",
|
||||
"eslint": "8.57.0",
|
||||
"cypress": "13.13.1",
|
||||
"eslint": "9.8.0",
|
||||
"globals": "15.8.0",
|
||||
"ncp": "2.0.0",
|
||||
"start-server-and-test": "2.0.3"
|
||||
"start-server-and-test": "2.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
node_modules
|
||||
/built
|
||||
/.eslintrc.js
|
||||
/@types/**/*
|
|
@ -1,32 +0,0 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||
},
|
||||
extends: [
|
||||
'../shared/.eslintrc.js',
|
||||
],
|
||||
rules: {
|
||||
'import/order': ['warn', {
|
||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
'pathGroups': [
|
||||
{
|
||||
'pattern': '@/**',
|
||||
'group': 'external',
|
||||
'position': 'after'
|
||||
}
|
||||
],
|
||||
}],
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
'name': '__dirname',
|
||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
||||
},
|
||||
{
|
||||
'name': '__filename',
|
||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
20
packages/backend/assets/api-doc.html
Normal file
20
packages/backend/assets/api-doc.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Misskey API</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
id="api-reference"
|
||||
data-url="/api.json"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,24 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Sharkey API</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
46
packages/backend/eslint.config.js
Normal file
46
packages/backend/eslint.config.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import tsParser from '@typescript-eslint/parser';
|
||||
import sharedConfig from '../shared/eslint.config.js';
|
||||
|
||||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/order': ['warn', {
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'object',
|
||||
'type',
|
||||
],
|
||||
pathGroups: [{
|
||||
pattern: '@/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
}],
|
||||
}],
|
||||
'no-restricted-globals': ['error', {
|
||||
name: '__dirname',
|
||||
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||
}, {
|
||||
name: '__filename',
|
||||
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||
}],
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AbuseReportNotification1713656541000 {
|
||||
name = 'AbuseReportNotification1713656541000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "system_webhook" (
|
||||
"id" varchar(32) NOT NULL,
|
||||
"isActive" boolean NOT NULL DEFAULT true,
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"latestSentAt" timestamp with time zone NULL DEFAULT NULL,
|
||||
"latestStatus" integer NULL DEFAULT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
|
||||
"url" varchar(1024) NOT NULL,
|
||||
"secret" varchar(1024) NOT NULL,
|
||||
CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
|
||||
CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
|
||||
|
||||
CREATE TABLE "abuse_report_notification_recipient" (
|
||||
"id" varchar(32) NOT NULL,
|
||||
"isActive" boolean NOT NULL DEFAULT true,
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"method" varchar(64) NOT NULL,
|
||||
"userId" varchar(32) NULL DEFAULT NULL,
|
||||
"systemWebhookId" varchar(32) NULL DEFAULT NULL,
|
||||
CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
);
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
|
||||
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
|
||||
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_method";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_userId";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
|
||||
DROP TABLE "abuse_report_notification_recipient";
|
||||
|
||||
DROP INDEX "IDX_system_webhook_isActive";
|
||||
DROP INDEX "IDX_system_webhook_on";
|
||||
DROP TABLE "system_webhook";
|
||||
`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class MediaSilenceForHosts1716197366117 {
|
||||
name = 'MediaSilenceForHosts1716197366117'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`);
|
||||
}
|
||||
}
|
24
packages/backend/migration/1721666053703-fixDriveUrl.js
Normal file
24
packages/backend/migration/1721666053703-fixDriveUrl.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FixDriveUrl1721666053703 {
|
||||
name = 'FixDriveUrl1721666053703'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(1024), ALTER COLUMN "url" SET NOT NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(1024)`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(1024)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(512)`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(512)`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(512), ALTER COLUMN "url" SET NOT NULL`);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.10.0"
|
||||
"node": "^20.10.0 || ^22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
|
@ -31,7 +31,7 @@
|
|||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
||||
"generate-api-json": "node ./scripts/generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
|
@ -63,45 +63,45 @@
|
|||
"utf-8-validate": "6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.412.0",
|
||||
"@aws-sdk/lib-storage": "3.412.0",
|
||||
"@bull-board/api": "5.17.0",
|
||||
"@bull-board/fastify": "5.17.0",
|
||||
"@bull-board/ui": "5.17.0",
|
||||
"@aws-sdk/client-s3": "3.620.0",
|
||||
"@aws-sdk/lib-storage": "3.620.0",
|
||||
"@bull-board/api": "5.21.1",
|
||||
"@bull-board/fastify": "5.21.1",
|
||||
"@bull-board/ui": "5.21.1",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@fastify/accepts": "4.3.0",
|
||||
"@fastify/cookie": "9.3.1",
|
||||
"@fastify/cors": "9.0.1",
|
||||
"@fastify/express": "3.0.0",
|
||||
"@fastify/http-proxy": "9.5.0",
|
||||
"@fastify/multipart": "8.2.0",
|
||||
"@fastify/static": "7.0.3",
|
||||
"@fastify/multipart": "8.3.0",
|
||||
"@fastify/static": "7.0.4",
|
||||
"@fastify/view": "9.1.0",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.1.0",
|
||||
"@napi-rs/canvas": "^0.1.52",
|
||||
"@nestjs/common": "10.3.8",
|
||||
"@nestjs/core": "10.3.8",
|
||||
"@nestjs/testing": "10.3.8",
|
||||
"@napi-rs/canvas": "^0.1.53",
|
||||
"@nestjs/common": "10.3.10",
|
||||
"@nestjs/core": "10.3.10",
|
||||
"@nestjs/testing": "10.3.10",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "^8.5.0",
|
||||
"@sentry/profiling-node": "^8.5.0",
|
||||
"@simplewebauthn/server": "10.0.0",
|
||||
"@sentry/node": "8.20.0",
|
||||
"@sentry/profiling-node": "8.20.0",
|
||||
"@simplewebauthn/server": "10.0.1",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@swc/cli": "0.3.12",
|
||||
"@swc/core": "1.4.17",
|
||||
"@swc/core": "1.6.6",
|
||||
"@transfem-org/sfm-js": "0.24.5",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.13.0",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
"argon2": "^0.40.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "5.7.8",
|
||||
"bullmq": "5.10.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.3.0",
|
||||
|
@ -113,30 +113,30 @@
|
|||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fast-xml-parser": "^4.4.0",
|
||||
"fastify": "4.26.2",
|
||||
"fastify": "4.28.1",
|
||||
"fastify-multer": "^2.0.3",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"file-type": "19.3.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.0",
|
||||
"glob": "10.3.10",
|
||||
"got": "14.2.1",
|
||||
"got": "14.4.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.4.1",
|
||||
"ip-cidr": "3.1.0",
|
||||
"ip-cidr": "4.0.1",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.0.0",
|
||||
"is-svg": "5.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "24.0.0",
|
||||
"jsdom": "24.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.1.0",
|
||||
"megalodon": "workspace:*",
|
||||
"meilisearch": "0.38.0",
|
||||
"meilisearch": "0.41.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
|
@ -145,24 +145,24 @@
|
|||
"nanoid": "5.0.7",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.13",
|
||||
"nodemailer": "6.9.14",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.2.3",
|
||||
"otpauth": "9.3.1",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.5",
|
||||
"pg": "8.12.0",
|
||||
"pkce-challenge": "4.1.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"pug": "3.0.2",
|
||||
"pug": "3.0.3",
|
||||
"punycode": "2.3.1",
|
||||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.20.10",
|
||||
"re2": "1.21.3",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
|
@ -170,28 +170,27 @@
|
|||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.13.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.33.3",
|
||||
"sharp": "0.33.4",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.22.7",
|
||||
"systeminformation": "5.22.11",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.4.5",
|
||||
"typescript": "5.5.4",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "^9.0.1",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.17.0",
|
||||
"ws": "8.18.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@nestjs/platform-express": "10.3.8",
|
||||
"@nestjs/platform-express": "10.3.10",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"@swc/jest": "0.2.36",
|
||||
"@types/accepts": "1.3.7",
|
||||
|
@ -201,22 +200,21 @@
|
|||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/htmlescape": "^1.1.3",
|
||||
"@types/http-link-header": "1.0.5",
|
||||
"@types/htmlescape": "1.1.3",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.6",
|
||||
"@types/jsonld": "1.5.13",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.14",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth": "0.9.5",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.5",
|
||||
"@types/pg": "8.11.6",
|
||||
"@types/proxy-addr": "^2.0.3",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/punycode": "2.1.4",
|
||||
|
@ -233,19 +231,18 @@
|
|||
"@types/uuid": "^9.0.4",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
||||
"@typescript-eslint/parser": "7.7.1",
|
||||
"aws-sdk-client-mock": "3.0.1",
|
||||
"@types/ws": "8.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"aws-sdk-client-mock": "4.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"execa": "8.0.1",
|
||||
"fkill": "^9.0.0",
|
||||
"execa": "9.3.0",
|
||||
"fkill": "9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.0",
|
||||
"nodemon": "3.1.4",
|
||||
"pid-port": "1.0.0",
|
||||
"simple-oauth2": "5.0.0"
|
||||
"simple-oauth2": "5.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ function execStart() {
|
|||
|
||||
async function killProc() {
|
||||
if (backendProcess) {
|
||||
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
|
||||
backendProcess.kill();
|
||||
await new Promise(resolve => backendProcess.on('exit', resolve));
|
||||
backendProcess = undefined;
|
||||
|
@ -46,6 +47,7 @@ async function killProc() {
|
|||
],
|
||||
{
|
||||
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
||||
serialization: "json",
|
||||
})
|
||||
.on('message', async (message) => {
|
||||
if (message.type === 'exit') {
|
||||
|
|
|
@ -3,11 +3,34 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { loadConfig } from '../built/config.js'
|
||||
import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { execa } from 'execa';
|
||||
import { writeFileSync, existsSync } from "node:fs";
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
async function main() {
|
||||
if (!process.argv.includes('--no-build')) {
|
||||
await execa('pnpm', ['run', 'build'], {
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
});
|
||||
}
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
||||
if (!existsSync('./built')) {
|
||||
throw new Error('`built` directory does not exist.');
|
||||
}
|
||||
|
||||
/** @type {import('../src/config.js')} */
|
||||
const { loadConfig } = await import('../built/config.js');
|
||||
|
||||
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
|
||||
const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
|
|||
import Logger from '@/logger.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const nestLogger = logger.createSubLogger('nest', 'green', false);
|
||||
const nestLogger = logger.createSubLogger('nest', 'green');
|
||||
|
||||
export class NestLogger implements LoggerService {
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
|
|||
EventEmitter.defaultMaxListeners = 128;
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange');
|
||||
const ev = new Xev();
|
||||
|
||||
// We wrap this in a main function, that gets called,
|
||||
|
|
|
@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
|
|||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||
|
||||
const themeColor = chalk.hex('#86b300');
|
||||
|
||||
|
|
|
@ -4,13 +4,36 @@
|
|||
*/
|
||||
|
||||
import cluster from 'node:cluster';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||
import { envOption } from '@/env.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
*/
|
||||
export async function workerMain() {
|
||||
const config = loadConfig();
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
} else if (envOption.onlyQueue) {
|
||||
|
|
|
@ -25,7 +25,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
|||
* 設定ファイルの型
|
||||
*/
|
||||
type Source = {
|
||||
url: string;
|
||||
url?: string;
|
||||
port?: number;
|
||||
socket?: string;
|
||||
chmodSocket?: string;
|
||||
|
@ -33,9 +33,9 @@ type Source = {
|
|||
db: {
|
||||
host: string;
|
||||
port: number;
|
||||
db: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
db?: string;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
disableCache?: boolean;
|
||||
extra?: { [x: string]: string };
|
||||
};
|
||||
|
@ -241,13 +241,17 @@ export function loadConfig(): Config {
|
|||
|
||||
applyEnvOverrides(config);
|
||||
|
||||
const url = tryCreateUrl(config.url);
|
||||
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
|
||||
const version = meta.version;
|
||||
const host = url.host;
|
||||
const hostname = url.hostname;
|
||||
const scheme = url.protocol.replace(/:$/, '');
|
||||
const wsScheme = scheme.replace('http', 'ws');
|
||||
|
||||
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
|
||||
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
|
||||
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
|
||||
|
||||
const externalMediaProxy = config.mediaProxy ?
|
||||
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
|
||||
: null;
|
||||
|
@ -270,7 +274,7 @@ export function loadConfig(): Config {
|
|||
apiUrl: `${scheme}://${host}/api`,
|
||||
authUrl: `${scheme}://${host}/auth`,
|
||||
driveUrl: `${scheme}://${host}/files`,
|
||||
db: config.db,
|
||||
db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
|
||||
dbReplications: config.dbReplications,
|
||||
dbSlaves: config.dbSlaves,
|
||||
meilisearch: config.meilisearch,
|
||||
|
|
405
packages/backend/src/core/AbuseReportNotificationService.ts
Normal file
405
packages/backend/src/core/AbuseReportNotificationService.ts
Normal file
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
|
||||
import { Brackets, In, IsNull, Not } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type {
|
||||
AbuseReportNotificationRecipientRepository,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiAbuseUserReport,
|
||||
MiUser,
|
||||
} from '@/models/_.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.abuseReportNotificationRecipientRepository)
|
||||
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private emailService: EmailService,
|
||||
private metaService: MetaService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||
* 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
|
||||
*
|
||||
* @see RoleService.getModeratorIds
|
||||
* @see GlobalEventService.publishAdminStream
|
||||
*/
|
||||
@bindThis
|
||||
public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
|
||||
if (abuseReports.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moderatorIds = await this.roleService.getModeratorIds(true, true);
|
||||
|
||||
for (const moderatorId of moderatorIds) {
|
||||
for (const abuseReport of abuseReports) {
|
||||
this.globalEventService.publishAdminStream(
|
||||
moderatorId,
|
||||
'newAbuseUserReport',
|
||||
{
|
||||
id: abuseReport.id,
|
||||
targetUserId: abuseReport.targetUserId,
|
||||
reporterId: abuseReport.reporterId,
|
||||
comment: abuseReport.comment,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||
* メールアドレスの送信先は以下の通り.
|
||||
* - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
|
||||
* - metaテーブルに設定されているメールアドレス
|
||||
*
|
||||
* @see EmailService.sendEmail
|
||||
*/
|
||||
@bindThis
|
||||
public async notifyMail(abuseReports: MiAbuseUserReport[]) {
|
||||
if (abuseReports.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
|
||||
.filter(it => it.isActive && it.userProfile?.emailVerified)
|
||||
.map(it => it.userProfile?.email)
|
||||
.filter(x => x != null),
|
||||
);
|
||||
|
||||
// 送信先の鮮度を保つため、毎回取得する
|
||||
const meta = await this.metaService.fetch(true);
|
||||
recipientEMailAddresses.push(
|
||||
...(meta.email ? [meta.email] : []),
|
||||
);
|
||||
|
||||
if (recipientEMailAddresses.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mailAddress of recipientEMailAddresses) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
// TODO: 送信処理はJobQueue化したい
|
||||
return this.emailService.sendEmail(
|
||||
mailAddress,
|
||||
'New Abuse Report',
|
||||
sanitizeHtml(it.comment),
|
||||
sanitizeHtml(it.comment),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
|
||||
*
|
||||
* @see SystemWebhookService.enqueueSystemWebhook
|
||||
*/
|
||||
@bindThis
|
||||
public async notifySystemWebhook(
|
||||
abuseReports: MiAbuseUserReport[],
|
||||
type: 'abuseReport' | 'abuseReportResolved',
|
||||
) {
|
||||
if (abuseReports.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientWebhookIds = await this.fetchWebhookRecipients()
|
||||
.then(it => it
|
||||
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||
.map(it => it.systemWebhookId)
|
||||
.filter(x => x != null));
|
||||
for (const webhookId of recipientWebhookIds) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
return this.systemWebhookService.enqueueSystemWebhook(
|
||||
webhookId,
|
||||
type,
|
||||
it,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通報の通知先一覧を取得する.
|
||||
*
|
||||
* @param {Object} [params] クエリの取得条件
|
||||
* @param {Object} [params.method] 取得する通知先の通知方法
|
||||
* @param {Object} [opts] 動作時の詳細なオプション
|
||||
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||
* @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
|
||||
* @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
|
||||
* @see removeUnauthorizedRecipientUsers
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchRecipients(
|
||||
params?: {
|
||||
ids?: MiAbuseReportNotificationRecipient['id'][],
|
||||
method?: RecipientMethod[],
|
||||
},
|
||||
opts?: {
|
||||
removeUnauthorized?: boolean,
|
||||
joinUser?: boolean,
|
||||
joinSystemWebhook?: boolean,
|
||||
},
|
||||
): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
|
||||
|
||||
if (opts?.joinUser) {
|
||||
query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
|
||||
query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
|
||||
}
|
||||
|
||||
if (opts?.joinSystemWebhook) {
|
||||
query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
|
||||
}
|
||||
|
||||
if (params?.ids) {
|
||||
query.andWhere({ id: In(params.ids) });
|
||||
}
|
||||
|
||||
if (params?.method) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
if (params.method?.includes('email')) {
|
||||
qb.orWhere({ method: 'email', userId: Not(IsNull()) });
|
||||
}
|
||||
if (params.method?.includes('webhook')) {
|
||||
qb.orWhere({ method: 'webhook', userId: IsNull() });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const recipients = await query.getMany();
|
||||
if (recipients.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
|
||||
return (opts?.removeUnauthorized ?? true)
|
||||
? await this.removeUnauthorizedRecipientUsers(recipients)
|
||||
: recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* EMailの通知先一覧を取得する.
|
||||
* リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
|
||||
*
|
||||
* @param {Object} [opts]
|
||||
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||
* @see removeUnauthorizedRecipientUsers
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchEMailRecipients(opts?: {
|
||||
removeUnauthorized?: boolean
|
||||
}): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhookの通知先一覧を取得する.
|
||||
* リレーション先の{@link MiSystemWebhook}も同時に取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知先を作成する.
|
||||
*/
|
||||
@bindThis
|
||||
public async createRecipient(
|
||||
params: {
|
||||
isActive: MiAbuseReportNotificationRecipient['isActive'];
|
||||
name: MiAbuseReportNotificationRecipient['name'];
|
||||
method: MiAbuseReportNotificationRecipient['method'];
|
||||
userId: MiAbuseReportNotificationRecipient['userId'];
|
||||
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiAbuseReportNotificationRecipient> {
|
||||
const id = this.idService.gen();
|
||||
await this.abuseReportNotificationRecipientRepository.insert({
|
||||
...params,
|
||||
id,
|
||||
});
|
||||
|
||||
const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
|
||||
|
||||
this.moderationLogService
|
||||
.log(updater, 'createAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: created,
|
||||
})
|
||||
.then();
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知先を更新する.
|
||||
*/
|
||||
@bindThis
|
||||
public async updateRecipient(
|
||||
params: {
|
||||
id: MiAbuseReportNotificationRecipient['id'];
|
||||
isActive: MiAbuseReportNotificationRecipient['isActive'];
|
||||
name: MiAbuseReportNotificationRecipient['name'];
|
||||
method: MiAbuseReportNotificationRecipient['method'];
|
||||
userId: MiAbuseReportNotificationRecipient['userId'];
|
||||
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiAbuseReportNotificationRecipient> {
|
||||
const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
|
||||
|
||||
await this.abuseReportNotificationRecipientRepository.update(params.id, {
|
||||
isActive: params.isActive,
|
||||
updatedAt: new Date(),
|
||||
name: params.name,
|
||||
method: params.method,
|
||||
userId: params.userId,
|
||||
systemWebhookId: params.systemWebhookId,
|
||||
});
|
||||
|
||||
const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
|
||||
|
||||
this.moderationLogService
|
||||
.log(updater, 'updateAbuseReportNotificationRecipient', {
|
||||
recipientId: params.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知先を削除する.
|
||||
*/
|
||||
@bindThis
|
||||
public async deleteRecipient(
|
||||
id: MiAbuseReportNotificationRecipient['id'],
|
||||
updater: MiUser,
|
||||
) {
|
||||
const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
|
||||
|
||||
await this.abuseReportNotificationRecipientRepository.delete(id);
|
||||
|
||||
this.moderationLogService
|
||||
.log(updater, 'deleteAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: entity,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
/**
|
||||
* モデレータ権限を持たない(*1)通知先ユーザを削除する.
|
||||
*
|
||||
* *1: 以下の両方を満たすものの事を言う
|
||||
* - 通知先にユーザIDが設定されている
|
||||
* - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
|
||||
*
|
||||
* @param recipients 通知先一覧の配列
|
||||
* @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
|
||||
*/
|
||||
@bindThis
|
||||
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
const userRecipients = recipients.filter(it => it.userId !== null);
|
||||
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
|
||||
if (recipientUserIds.size <= 0) {
|
||||
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
|
||||
return recipients;
|
||||
}
|
||||
|
||||
// モデレータ権限の有無で通知先設定を振り分ける
|
||||
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
|
||||
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
for (const recipient of userRecipients) {
|
||||
// eslint-disable-next-line
|
||||
if (authorizedUserIds.includes(recipient.userId!)) {
|
||||
authorizedUserRecipients.push(recipient);
|
||||
} else {
|
||||
unauthorizedUserRecipients.push(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
// モデレータ権限を持たない通知先をDBから削除する
|
||||
if (unauthorizedUserRecipients.length > 0) {
|
||||
await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
|
||||
}
|
||||
const nonUserRecipients = recipients.filter(it => it.userId === null);
|
||||
return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'roleUpdated':
|
||||
case 'roleDeleted':
|
||||
case 'userRoleUnassigned': {
|
||||
// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
|
||||
process.nextTick(async () => {
|
||||
const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
|
||||
userId: Not(IsNull()),
|
||||
});
|
||||
await this.removeUnauthorizedRecipientUsers(recipients);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
138
packages/backend/src/core/AbuseReportService.ts
Normal file
138
packages/backend/src/core/AbuseReportService.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AbuseReportService {
|
||||
constructor(
|
||||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
private idService: IdService,
|
||||
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||
private queueService: QueueService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private apRendererService: ApRendererService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
|
||||
* - 管理者用Redisイベント
|
||||
* - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
|
||||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async report(params: {
|
||||
targetUserId: MiAbuseUserReport['targetUserId'],
|
||||
targetUserHost: MiAbuseUserReport['targetUserHost'],
|
||||
reporterId: MiAbuseUserReport['reporterId'],
|
||||
reporterHost: MiAbuseUserReport['reporterHost'],
|
||||
comment: string,
|
||||
}[]) {
|
||||
const entities = params.map(param => {
|
||||
return {
|
||||
id: this.idService.gen(),
|
||||
targetUserId: param.targetUserId,
|
||||
targetUserHost: param.targetUserHost,
|
||||
reporterId: param.reporterId,
|
||||
reporterHost: param.reporterHost,
|
||||
comment: param.comment,
|
||||
};
|
||||
});
|
||||
|
||||
const reports = Array.of<MiAbuseUserReport>();
|
||||
for (const entity of entities) {
|
||||
const report = await this.abuseUserReportsRepository.insertOne(entity);
|
||||
reports.push(report);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this.abuseReportNotificationService.notifyAdminStream(reports),
|
||||
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
|
||||
this.abuseReportNotificationService.notifyMail(reports),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通報を解決し、その内容を下記の手段で管理者各位に通知する.
|
||||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @param operator 通報を処理したユーザ
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async resolve(
|
||||
params: {
|
||||
reportId: string;
|
||||
forward: boolean;
|
||||
}[],
|
||||
operator: MiUser,
|
||||
) {
|
||||
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||
const reports = await this.abuseUserReportsRepository.findBy({
|
||||
id: In(params.map(it => it.reportId)),
|
||||
});
|
||||
|
||||
const targetUserMap = new Map();
|
||||
for (const report of reports) {
|
||||
const shouldForward = paramsMap.get(report.id)!.forward;
|
||||
|
||||
if (shouldForward && report.targetUserHost != null) {
|
||||
return targetUserMap.set(report.id, await this.usersRepository.findOneByOrFail({ id: report.targetUserId }));
|
||||
} else {
|
||||
return targetUserMap.set(report.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (const report of reports) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const ps = paramsMap.get(report.id)!;
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
resolved: true,
|
||||
assigneeId: operator.id,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
});
|
||||
|
||||
const targetUser = targetUserMap.get(report.id)!;
|
||||
if (targetUser != null) {
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
|
||||
// eslint-disable-next-line
|
||||
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||
const contextAssignedFlag = this.apRendererService.addContext(flag);
|
||||
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
|
||||
}
|
||||
|
||||
this.moderationLogService
|
||||
.log(operator, 'resolveAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
});
|
||||
}
|
||||
|
||||
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ export class AnnouncementService {
|
|||
|
||||
@bindThis
|
||||
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
|
||||
const announcement = await this.announcementsRepository.insert({
|
||||
const announcement = await this.announcementsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: null,
|
||||
title: values.title,
|
||||
|
@ -79,7 +79,7 @@ export class AnnouncementService {
|
|||
silence: values.silence,
|
||||
needConfirmationToRead: values.needConfirmationToRead,
|
||||
userId: values.userId,
|
||||
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
const packed = await this.announcementEntityService.pack(announcement);
|
||||
|
||||
|
|
|
@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
|
||||
const created = await this.avatarDecorationsRepository.insert({
|
||||
const created = await this.avatarDecorationsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
...options,
|
||||
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
|
||||
|
||||
|
|
|
@ -41,17 +41,17 @@ export class ClipService {
|
|||
const currentCount = await this.clipsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
throw new ClipService.TooManyClipsError();
|
||||
}
|
||||
|
||||
const clip = await this.clipsRepository.insert({
|
||||
const clip = await this.clipsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
userId: me.id,
|
||||
name: name,
|
||||
isPublic: isPublic,
|
||||
description: description,
|
||||
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
return clip;
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export class ClipService {
|
|||
const currentCount = await this.clipNotesRepository.countBy({
|
||||
clipId: clip.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||
throw new ClipService.TooManyClipNotesError();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||
import {
|
||||
AbuseReportNotificationRecipientEntityService,
|
||||
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AnnouncementService } from './AnnouncementService.js';
|
||||
|
@ -53,10 +61,11 @@ import { UserFollowingService } from './UserFollowingService.js';
|
|||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { UserListService } from './UserListService.js';
|
||||
import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserRenoteMutingService } from './UserRenoteMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
import { UserAuthService } from './UserAuthService.js';
|
||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||
import { WebhookService } from './WebhookService.js';
|
||||
import { UserWebhookService } from './UserWebhookService.js';
|
||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||
import { UtilityService } from './UtilityService.js';
|
||||
import { FileInfoService } from './FileInfoService.js';
|
||||
|
@ -144,6 +153,8 @@ import type { Provider } from '@nestjs/common';
|
|||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
||||
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
|
||||
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
|
||||
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
||||
const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
|
||||
|
@ -193,10 +204,13 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
|
|||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||
const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
|
||||
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
|
||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
|
@ -225,6 +239,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
|
|||
|
||||
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
|
||||
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
|
||||
const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
|
||||
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
|
||||
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
|
||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
|
||||
|
@ -258,6 +273,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
|
|||
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
|
@ -285,6 +301,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
AbuseReportService,
|
||||
AbuseReportNotificationService,
|
||||
AccountMoveService,
|
||||
AccountUpdateService,
|
||||
AnnouncementService,
|
||||
|
@ -334,10 +352,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
WebhookService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
|
@ -366,6 +387,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
|
||||
AbuseUserReportEntityService,
|
||||
AnnouncementEntityService,
|
||||
AbuseReportNotificationRecipientEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
AuthSessionEntityService,
|
||||
|
@ -399,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
|
@ -422,6 +445,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AbuseReportService,
|
||||
$AbuseReportNotificationService,
|
||||
$AccountMoveService,
|
||||
$AccountUpdateService,
|
||||
$AnnouncementService,
|
||||
|
@ -471,10 +496,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
$WebhookService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
|
@ -503,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
|
||||
$AbuseUserReportEntityService,
|
||||
$AnnouncementEntityService,
|
||||
$AbuseReportNotificationRecipientEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
$AuthSessionEntityService,
|
||||
|
@ -536,6 +565,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
|
@ -560,6 +590,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
exports: [
|
||||
QueueModule,
|
||||
LoggerService,
|
||||
AbuseReportService,
|
||||
AbuseReportNotificationService,
|
||||
AccountMoveService,
|
||||
AccountUpdateService,
|
||||
AnnouncementService,
|
||||
|
@ -609,10 +641,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserRenoteMutingService,
|
||||
UserSearchService,
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
WebhookService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
|
@ -640,6 +675,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
|
||||
AbuseUserReportEntityService,
|
||||
AnnouncementEntityService,
|
||||
AbuseReportNotificationRecipientEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
AuthSessionEntityService,
|
||||
|
@ -673,6 +709,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
|
@ -696,6 +733,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AbuseReportService,
|
||||
$AbuseReportNotificationService,
|
||||
$AccountMoveService,
|
||||
$AccountUpdateService,
|
||||
$AnnouncementService,
|
||||
|
@ -745,10 +784,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserRenoteMutingService,
|
||||
$UserSearchService,
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
$WebhookService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
|
@ -776,6 +818,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
|
||||
$AbuseUserReportEntityService,
|
||||
$AnnouncementEntityService,
|
||||
$AbuseReportNotificationRecipientEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
$AuthSessionEntityService,
|
||||
|
@ -809,6 +852,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
|
|
|
@ -77,7 +77,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
localOnly: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
||||
}, moderator?: MiUser): Promise<MiEmoji> {
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
const emoji = await this.emojisRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
|
@ -91,7 +91,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
if (data.host == null) {
|
||||
this.localEmojisCache.refresh();
|
||||
|
|
|
@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js';
|
|||
import { correctFilename } from '@/misc/correct-filename.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
type AddFileArgs = {
|
||||
/** User who wish to add file */
|
||||
|
@ -127,6 +128,7 @@ export class DriveService {
|
|||
private driveChart: DriveChart,
|
||||
private perUserDriveChart: PerUserDriveChart,
|
||||
private instanceChart: InstanceChart,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
const logger = new Logger('drive', 'blue');
|
||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||
|
@ -220,7 +222,7 @@ export class DriveService {
|
|||
file.size = size;
|
||||
file.storedInternal = false;
|
||||
|
||||
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
return await this.driveFilesRepository.insertOne(file);
|
||||
} else { // use internal storage
|
||||
const accessKey = randomUUID();
|
||||
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||
|
@ -254,7 +256,7 @@ export class DriveService {
|
|||
file.md5 = hash;
|
||||
file.size = size;
|
||||
|
||||
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
return await this.driveFilesRepository.insertOne(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -567,6 +569,7 @@ export class DriveService {
|
|||
sensitive ?? false
|
||||
: false;
|
||||
|
||||
if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||
if (userRoleNSFW) file.isSensitive = true;
|
||||
|
||||
|
@ -594,7 +597,7 @@ export class DriveService {
|
|||
file.type = info.type.mime;
|
||||
file.storedInternal = false;
|
||||
|
||||
file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
file = await this.driveFilesRepository.insertOne(file);
|
||||
} catch (err) {
|
||||
// duplicate key error (when already registered)
|
||||
if (isDuplicateKeyValueError(err)) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
|||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
|
@ -32,6 +33,7 @@ export class EmailService {
|
|||
private loggerService: LoggerService,
|
||||
private utilityService: UtilityService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('email');
|
||||
}
|
||||
|
|
|
@ -56,9 +56,6 @@ export class FanoutTimelineEndpointService {
|
|||
|
||||
@bindThis
|
||||
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
||||
|
||||
|
@ -68,12 +65,11 @@ export class FanoutTimelineEndpointService {
|
|||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||
|
||||
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
||||
const redisResultIds = Array.from(new Set(redisResult.flat(1)));
|
||||
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
|
||||
|
||||
redisResultIds.sort(idCompare);
|
||||
noteIds = redisResultIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
let noteIds = redisResultIds.slice(0, ps.limit);
|
||||
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
||||
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
let filter = ps.noteFilter ?? (_note => true);
|
||||
|
|
|
@ -56,11 +56,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
||||
if (index == null) {
|
||||
const i = await this.instancesRepository.insert({
|
||||
const i = await this.instancesRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
host,
|
||||
firstRetrievedAt: new Date(),
|
||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.federatedInstanceCache.set(host, i);
|
||||
return i;
|
||||
|
|
|
@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
|||
import type { MiSignin } from '@/models/Signin.js';
|
||||
import type { MiPage } from '@/models/Page.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
@ -213,6 +214,10 @@ type SerializedAll<T> = {
|
|||
[K in keyof T]: Serialized<T[K]>;
|
||||
};
|
||||
|
||||
type UndefinedAsNullAll<T> = {
|
||||
[K in keyof T]: T[K] extends undefined ? null : T[K];
|
||||
}
|
||||
|
||||
export interface InternalEventTypes {
|
||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
|
||||
|
@ -232,6 +237,9 @@ export interface InternalEventTypes {
|
|||
webhookCreated: MiWebhook;
|
||||
webhookDeleted: MiWebhook;
|
||||
webhookUpdated: MiWebhook;
|
||||
systemWebhookCreated: MiSystemWebhook;
|
||||
systemWebhookDeleted: MiSystemWebhook;
|
||||
systemWebhookUpdated: MiSystemWebhook;
|
||||
antennaCreated: MiAntenna;
|
||||
antennaDeleted: MiAntenna;
|
||||
antennaUpdated: MiAntenna;
|
||||
|
@ -248,43 +256,45 @@ export interface InternalEventTypes {
|
|||
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||
}
|
||||
|
||||
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
|
||||
|
||||
// name/messages(spec) pairs dictionary
|
||||
export type GlobalEvents = {
|
||||
internal: {
|
||||
name: 'internal';
|
||||
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
|
||||
payload: EventTypesToEventPayload<InternalEventTypes>;
|
||||
};
|
||||
broadcast: {
|
||||
name: 'broadcast';
|
||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||
payload: EventTypesToEventPayload<BroadcastTypes>;
|
||||
};
|
||||
main: {
|
||||
name: `mainStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
|
||||
payload: EventTypesToEventPayload<MainEventTypes>;
|
||||
};
|
||||
drive: {
|
||||
name: `driveStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
|
||||
payload: EventTypesToEventPayload<DriveEventTypes>;
|
||||
};
|
||||
note: {
|
||||
name: `noteStream:${MiNote['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
||||
payload: EventTypesToEventPayload<NoteStreamEventTypes>;
|
||||
};
|
||||
userList: {
|
||||
name: `userListStream:${MiUserList['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||
payload: EventTypesToEventPayload<UserListEventTypes>;
|
||||
};
|
||||
roleTimeline: {
|
||||
name: `roleTimelineStream:${MiRole['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
|
||||
payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
|
||||
};
|
||||
antenna: {
|
||||
name: `antennaStream:${MiAntenna['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
|
||||
payload: EventTypesToEventPayload<AntennaEventTypes>;
|
||||
};
|
||||
admin: {
|
||||
name: `adminStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
|
||||
payload: EventTypesToEventPayload<AdminEventTypes>;
|
||||
};
|
||||
notes: {
|
||||
name: 'notesStream';
|
||||
|
@ -292,11 +302,11 @@ export type GlobalEvents = {
|
|||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
};
|
||||
reversiGame: {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
|
||||
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export class LoggerService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
||||
return new Logger(domain, color, store);
|
||||
public getLogger(domain: string, color?: KEYWORD | undefined) {
|
||||
return new Logger(domain, color);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
|
|||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
||||
import type { DefaultTreeAdapterMap } from 'parse5';
|
||||
import type * as mfm from '@transfem-org/sfm-js';
|
||||
|
||||
const treeAdapter = TreeAdapter.defaultTreeAdapter;
|
||||
const treeAdapter = parse5.defaultTreeAdapter;
|
||||
type Node = DefaultTreeAdapterMap['node'];
|
||||
type ChildNode = DefaultTreeAdapterMap['childNode'];
|
||||
|
||||
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||
|
@ -46,7 +48,7 @@ export class MfmService {
|
|||
|
||||
return text.trim();
|
||||
|
||||
function getText(node: TreeAdapter.Node): string {
|
||||
function getText(node: Node): string {
|
||||
if (treeAdapter.isTextNode(node)) return node.value;
|
||||
if (!treeAdapter.isElementNode(node)) return '';
|
||||
if (node.nodeName === 'br') return '\n';
|
||||
|
@ -58,7 +60,7 @@ export class MfmService {
|
|||
return '';
|
||||
}
|
||||
|
||||
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
||||
function appendChildren(childNodes: ChildNode[]): void {
|
||||
if (childNodes) {
|
||||
for (const n of childNodes) {
|
||||
analyze(n);
|
||||
|
@ -66,14 +68,16 @@ export class MfmService {
|
|||
}
|
||||
}
|
||||
|
||||
function analyze(node: TreeAdapter.Node) {
|
||||
function analyze(node: Node) {
|
||||
if (treeAdapter.isTextNode(node)) {
|
||||
text += node.value;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip comment or document type node
|
||||
if (!treeAdapter.isElementNode(node)) return;
|
||||
if (!treeAdapter.isElementNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (node.nodeName) {
|
||||
case 'br': {
|
||||
|
@ -81,8 +85,7 @@ export class MfmService {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'a':
|
||||
{
|
||||
case 'a': {
|
||||
const txt = getText(node);
|
||||
const rel = node.attrs.find(x => x.name === 'rel');
|
||||
const href = node.attrs.find(x => x.name === 'href');
|
||||
|
@ -90,7 +93,7 @@ export class MfmService {
|
|||
// ハッシュタグ
|
||||
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
||||
text += txt;
|
||||
// メンション
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||
const part = txt.split('@');
|
||||
|
||||
|
@ -102,7 +105,7 @@ export class MfmService {
|
|||
} else if (part.length === 3) {
|
||||
text += txt;
|
||||
}
|
||||
// その他
|
||||
// その他
|
||||
} else {
|
||||
const generateLink = () => {
|
||||
if (!href && !txt) {
|
||||
|
@ -130,8 +133,7 @@ export class MfmService {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'h1':
|
||||
{
|
||||
case 'h1': {
|
||||
text += '**【';
|
||||
appendChildren(node.childNodes);
|
||||
text += '】**\n';
|
||||
|
@ -139,8 +141,7 @@ export class MfmService {
|
|||
}
|
||||
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
{
|
||||
case 'h3': {
|
||||
text += '**';
|
||||
appendChildren(node.childNodes);
|
||||
text += '**\n';
|
||||
|
@ -148,16 +149,14 @@ export class MfmService {
|
|||
}
|
||||
|
||||
case 'b':
|
||||
case 'strong':
|
||||
{
|
||||
case 'strong': {
|
||||
text += '**';
|
||||
appendChildren(node.childNodes);
|
||||
text += '**';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'small':
|
||||
{
|
||||
case 'small': {
|
||||
text += '<small>';
|
||||
appendChildren(node.childNodes);
|
||||
text += '</small>';
|
||||
|
@ -165,8 +164,7 @@ export class MfmService {
|
|||
}
|
||||
|
||||
case 's':
|
||||
case 'del':
|
||||
{
|
||||
case 'del': {
|
||||
text += '~~';
|
||||
appendChildren(node.childNodes);
|
||||
text += '~~';
|
||||
|
@ -174,8 +172,7 @@ export class MfmService {
|
|||
}
|
||||
|
||||
case 'i':
|
||||
case 'em':
|
||||
{
|
||||
case 'em': {
|
||||
text += '<i>';
|
||||
appendChildren(node.childNodes);
|
||||
text += '</i>';
|
||||
|
@ -214,8 +211,7 @@ export class MfmService {
|
|||
case 'p':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
{
|
||||
case 'h6': {
|
||||
text += '\n\n';
|
||||
appendChildren(node.childNodes);
|
||||
break;
|
||||
|
@ -228,8 +224,7 @@ export class MfmService {
|
|||
case 'article':
|
||||
case 'li':
|
||||
case 'dt':
|
||||
case 'dd':
|
||||
{
|
||||
case 'dd': {
|
||||
text += '\n';
|
||||
appendChildren(node.childNodes);
|
||||
break;
|
||||
|
|
|
@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
|||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { HashtagService } from '@/core/HashtagService.js';
|
||||
import { AntennaService } from '@/core/AntennaService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
@ -61,7 +61,6 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
@ -207,7 +206,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
private federatedInstanceService: FederatedInstanceService,
|
||||
private hashtagService: HashtagService,
|
||||
private antennaService: AntennaService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private featuredService: FeaturedService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
|
@ -554,6 +553,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
||||
}
|
||||
|
||||
// if the host is media-silenced, custom emojis are not allowed
|
||||
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
|
||||
|
||||
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||
|
||||
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
||||
|
@ -817,7 +819,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'note', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'note', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
|
@ -856,7 +858,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'reply', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'reply', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
|
@ -896,7 +898,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'renote', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'renote', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
|
@ -1135,7 +1137,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'mention', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'mention', {
|
||||
note: detailPackedNote,
|
||||
});
|
||||
}
|
||||
|
@ -1186,7 +1188,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
const mentions = extractMentions(tokens);
|
||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
||||
))).filter(isNotNull);
|
||||
))).filter(x => x != null);
|
||||
|
||||
// Drop duplicate users
|
||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||
|
@ -1281,10 +1283,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
// 自分自身のHTL
|
||||
if (note.userHost == null) {
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
|||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
@ -51,7 +51,6 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited';
|
||||
|
@ -203,7 +202,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
private notificationService: NotificationService,
|
||||
private relayService: RelayService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private apRendererService: ApRendererService,
|
||||
|
@ -388,6 +387,9 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
||||
}
|
||||
|
||||
// if the host is media-silenced, custom emojis are not allowed
|
||||
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
|
||||
|
||||
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
|
||||
|
||||
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
||||
|
@ -631,7 +633,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'note', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'note', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
|
@ -666,7 +668,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'edited', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'edited', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
|
@ -801,7 +803,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'edited', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'edited', {
|
||||
note: detailPackedNote,
|
||||
});
|
||||
}
|
||||
|
@ -838,7 +840,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
const mentions = extractMentions(tokens);
|
||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
||||
))).filter(isNotNull) as MiUser[];
|
||||
))).filter(x => x !== null) as MiUser[];
|
||||
|
||||
// Drop duplicate users
|
||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||
|
@ -933,10 +935,13 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
// 自分自身のHTL
|
||||
if (note.userHost == null) {
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
|||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import {
|
||||
DeliverJobData,
|
||||
EndedPollNotificationJobData,
|
||||
InboxJobData,
|
||||
RelationshipJobData,
|
||||
UserWebhookDeliverJobData,
|
||||
SystemWebhookDeliverJobData,
|
||||
} from '../queue/types.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||
|
||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
|
@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
|
|||
export type DbQueue = Bull.Queue;
|
||||
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||
export type ObjectStorageQueue = Bull.Queue;
|
||||
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
||||
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||
|
||||
const $system: Provider = {
|
||||
provide: 'queue:system',
|
||||
|
@ -63,9 +71,15 @@ const $objectStorage: Provider = {
|
|||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $webhookDeliver: Provider = {
|
||||
provide: 'queue:webhookDeliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
|
||||
const $userWebhookDeliver: Provider = {
|
||||
provide: 'queue:userWebhookDeliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $systemWebhookDeliver: Provider = {
|
||||
provide: 'queue:systemWebhookDeliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
|
@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
|
|||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
$userWebhookDeliver,
|
||||
$systemWebhookDeliver,
|
||||
],
|
||||
exports: [
|
||||
$system,
|
||||
|
@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
|
|||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
$userWebhookDeliver,
|
||||
$systemWebhookDeliver,
|
||||
],
|
||||
})
|
||||
export class QueueModule implements OnApplicationShutdown {
|
||||
|
@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||
) {}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
|
@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
this.dbQueue.close(),
|
||||
this.relationshipQueue.close(),
|
||||
this.objectStorageQueue.close(),
|
||||
this.webhookDeliverQueue.close(),
|
||||
this.userWebhookDeliverQueue.close(),
|
||||
this.systemWebhookDeliverQueue.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,16 +8,34 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
import type {
|
||||
DbJobData,
|
||||
DeliverJobData,
|
||||
RelationshipJobData,
|
||||
SystemWebhookDeliverJobData,
|
||||
ThinUser,
|
||||
UserWebhookDeliverJobData,
|
||||
} from '../queue/types.js';
|
||||
import type {
|
||||
DbQueue,
|
||||
DeliverQueue,
|
||||
EndedPollNotificationQueue,
|
||||
InboxQueue,
|
||||
ObjectStorageQueue,
|
||||
RelationshipQueue,
|
||||
SystemQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
SystemWebhookDeliverQueue,
|
||||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
|
@ -32,7 +50,8 @@ export class QueueService {
|
|||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||
) {
|
||||
this.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
|
@ -490,9 +509,13 @@ export class QueueService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
const data = {
|
||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
|
@ -503,7 +526,33 @@ export class QueueService {
|
|||
eventId: randomUUID(),
|
||||
};
|
||||
|
||||
return this.webhookDeliverQueue.add(webhook.id, data, {
|
||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SystemWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
||||
const data: SystemWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
to: webhook.url,
|
||||
secret: webhook.secret,
|
||||
createdAt: Date.now(),
|
||||
eventId: randomUUID(),
|
||||
};
|
||||
|
||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
|
|
|
@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
|||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
|
||||
const FALLBACK = '\u2764';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
@ -107,6 +108,8 @@ export class ReactionService {
|
|||
|
||||
@bindThis
|
||||
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
|
@ -120,11 +123,16 @@ export class ReactionService {
|
|||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// Check if note is Renote
|
||||
if (isRenote(note) && !isQuote(note)) {
|
||||
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
|
||||
}
|
||||
|
||||
let reaction = _reaction ?? FALLBACK;
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '\u2764';
|
||||
} else if (_reaction) {
|
||||
} else if (_reaction != null) {
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||
|
@ -145,6 +153,11 @@ export class ReactionService {
|
|||
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
|
||||
// for media silenced host, custom emoji reactions are not allowed
|
||||
if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
// リアクションとして使う権限がない
|
||||
reaction = FALLBACK;
|
||||
|
@ -217,8 +230,6 @@ export class ReactionService {
|
|||
}
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
this.perUserReactionsChart.update(user, note);
|
||||
}
|
||||
|
|
|
@ -53,11 +53,11 @@ export class RelayService {
|
|||
|
||||
@bindThis
|
||||
public async addRelay(inbox: string): Promise<MiRelay> {
|
||||
const relay = await this.relaysRepository.insert({
|
||||
const relay = await this.relaysRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
inbox,
|
||||
status: 'requesting',
|
||||
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
|
|
|
@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
const game = await this.reversiGamesRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
user1Id: parentId,
|
||||
user2Id: childId,
|
||||
|
@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
noIrregularRules: options.noIrregularRules,
|
||||
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
||||
where: { id: x.identifiers[0].id },
|
||||
relations: ['user1', 'user2'],
|
||||
}));
|
||||
}, { relations: ['user1', 'user2'] });
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game);
|
||||
|
|
|
@ -48,6 +48,7 @@ export type RolePolicies = {
|
|||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
canUpdateBioMedia: boolean;
|
||||
pinLimit: number;
|
||||
antennaLimit: number;
|
||||
wordMuteLimit: number;
|
||||
|
@ -78,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
alwaysMarkNsfw: false,
|
||||
canUpdateBioMedia: true,
|
||||
pinLimit: 5,
|
||||
antennaLimit: 5,
|
||||
wordMuteLimit: 200,
|
||||
|
@ -379,6 +381,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||
|
@ -414,14 +417,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
|
||||
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
||||
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||
roleId: In(moderatorRoles.map(r => r.id)),
|
||||
}) : [];
|
||||
const moderatorRoles = includeAdmins
|
||||
? roles.filter(r => r.isModerator || r.isAdministrator)
|
||||
: roles.filter(r => r.isModerator);
|
||||
|
||||
// TODO: isRootなアカウントも含める
|
||||
return assigns.map(a => a.userId);
|
||||
const assigns = moderatorRoles.length > 0
|
||||
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
||||
: [];
|
||||
|
||||
const now = Date.now();
|
||||
const result = [
|
||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||
...new Set(
|
||||
assigns
|
||||
.filter(it =>
|
||||
(excludeExpire)
|
||||
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
||||
: true,
|
||||
)
|
||||
.map(a => a.userId),
|
||||
),
|
||||
];
|
||||
|
||||
return result.sort((x, y) => x.localeCompare(y));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -475,12 +496,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
const created = await this.roleAssignmentsRepository.insert({
|
||||
const created = await this.roleAssignmentsRepository.insertOne({
|
||||
id: this.idService.gen(now),
|
||||
expiresAt: expiresAt,
|
||||
roleId: roleId,
|
||||
userId: userId,
|
||||
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.rolesRepository.update(roleId, {
|
||||
lastUsedAt: new Date(),
|
||||
|
@ -488,14 +509,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||
|
||||
if (role.isPublic) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
|
||||
if (role.isPublic && user.host === null) {
|
||||
this.notificationService.createNotification(userId, 'roleAssigned', {
|
||||
roleId: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
if (moderator) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
this.moderationLogService.log(moderator, 'assignRole', {
|
||||
roleId: roleId,
|
||||
roleName: role.name,
|
||||
|
@ -562,7 +584,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
@bindThis
|
||||
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
|
||||
const date = new Date();
|
||||
const created = await this.rolesRepository.insert({
|
||||
const created = await this.rolesRepository.insertOne({
|
||||
id: this.idService.gen(date.getTime()),
|
||||
updatedAt: date,
|
||||
lastUsedAt: date,
|
||||
|
@ -580,7 +602,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||
displayOrder: values.displayOrder,
|
||||
policies: values.policies,
|
||||
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('roleCreated', created);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import UsersChart from '@/core/chart/charts/users.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
|
||||
@Injectable()
|
||||
export class SignupService {
|
||||
|
@ -36,6 +37,7 @@ export class SignupService {
|
|||
private usedUsernamesRepository: UsedUsernamesRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private userService: UserService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
|
@ -155,7 +157,8 @@ export class SignupService {
|
|||
}));
|
||||
});
|
||||
|
||||
this.usersChart.update(account, true);
|
||||
this.usersChart.update(account, true).then();
|
||||
this.userService.notifySystemWebhook(account, 'userCreated').then();
|
||||
|
||||
return { account, secret };
|
||||
}
|
||||
|
|
233
packages/backend/src/core/SystemWebhookService.ts
Normal file
233
packages/backend/src/core/SystemWebhookService.ts
Normal file
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import Logger from '@/logger.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class SystemWebhookService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private activeSystemWebhooksFetched = false;
|
||||
private activeSystemWebhooks: MiSystemWebhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
@Inject(DI.systemWebhooksRepository)
|
||||
private systemWebhooksRepository: SystemWebhooksRepository,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private loggerService: LoggerService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
this.logger = this.loggerService.getLogger('webhook');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchActiveSystemWebhooks() {
|
||||
if (!this.activeSystemWebhooksFetched) {
|
||||
this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
|
||||
isActive: true,
|
||||
});
|
||||
this.activeSystemWebhooksFetched = true;
|
||||
}
|
||||
|
||||
return this.activeSystemWebhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook の一覧を取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchSystemWebhooks(params?: {
|
||||
ids?: MiSystemWebhook['id'][];
|
||||
isActive?: MiSystemWebhook['isActive'];
|
||||
on?: MiSystemWebhook['on'];
|
||||
}): Promise<MiSystemWebhook[]> {
|
||||
const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
|
||||
if (params) {
|
||||
if (params.ids && params.ids.length > 0) {
|
||||
query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
|
||||
}
|
||||
if (params.isActive !== undefined) {
|
||||
query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
|
||||
}
|
||||
if (params.on && params.on.length > 0) {
|
||||
query.andWhere(':on <@ systemWebhook.on', { on: params.on });
|
||||
}
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook を作成する.
|
||||
*/
|
||||
@bindThis
|
||||
public async createSystemWebhook(
|
||||
params: {
|
||||
isActive: MiSystemWebhook['isActive'];
|
||||
name: MiSystemWebhook['name'];
|
||||
on: MiSystemWebhook['on'];
|
||||
url: MiSystemWebhook['url'];
|
||||
secret: MiSystemWebhook['secret'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiSystemWebhook> {
|
||||
const id = this.idService.gen();
|
||||
await this.systemWebhooksRepository.insert({
|
||||
...params,
|
||||
id,
|
||||
});
|
||||
|
||||
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||
this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
|
||||
this.moderationLogService
|
||||
.log(updater, 'createSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook: webhook,
|
||||
})
|
||||
.then();
|
||||
|
||||
return webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook を更新する.
|
||||
*/
|
||||
@bindThis
|
||||
public async updateSystemWebhook(
|
||||
params: {
|
||||
id: MiSystemWebhook['id'];
|
||||
isActive: MiSystemWebhook['isActive'];
|
||||
name: MiSystemWebhook['name'];
|
||||
on: MiSystemWebhook['on'];
|
||||
url: MiSystemWebhook['url'];
|
||||
secret: MiSystemWebhook['secret'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiSystemWebhook> {
|
||||
const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
|
||||
await this.systemWebhooksRepository.update(beforeEntity.id, {
|
||||
updatedAt: new Date(),
|
||||
isActive: params.isActive,
|
||||
name: params.name,
|
||||
on: params.on,
|
||||
url: params.url,
|
||||
secret: params.secret,
|
||||
});
|
||||
|
||||
const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
|
||||
this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
|
||||
this.moderationLogService
|
||||
.log(updater, 'updateSystemWebhook', {
|
||||
systemWebhookId: beforeEntity.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook を削除する.
|
||||
*/
|
||||
@bindThis
|
||||
public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
|
||||
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||
await this.systemWebhooksRepository.delete(id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
|
||||
this.moderationLogService
|
||||
.log(updater, 'deleteSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook をWebhook配送キューに追加する
|
||||
* @see QueueService.systemWebhookDeliver
|
||||
*/
|
||||
@bindThis
|
||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||
const webhookEntity = typeof webhook === 'string'
|
||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||
: webhook;
|
||||
if (!webhookEntity || !webhookEntity.isActive) {
|
||||
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookEntity.on.includes(type)) {
|
||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'systemWebhookCreated': {
|
||||
if (body.isActive) {
|
||||
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'systemWebhookUpdated': {
|
||||
if (body.isActive) {
|
||||
const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
|
||||
} else {
|
||||
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||
}
|
||||
} else {
|
||||
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'systemWebhookDeleted': {
|
||||
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ import Logger from '@/logger.js';
|
|||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
|
@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
|
@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
|||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
|
@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
private metaService: MetaService,
|
||||
private notificationService: NotificationService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
|
@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit {
|
|||
});
|
||||
|
||||
// 通知を作成
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
}, followee.id);
|
||||
if (follower.host === null) {
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
}, followee.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyFollowed) return;
|
||||
|
@ -331,7 +333,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'follow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'follow', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
|
@ -345,7 +347,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'followed', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'followed', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
|
@ -398,7 +400,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
|
@ -517,7 +519,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followerId: follower.id,
|
||||
});
|
||||
|
||||
const followRequest = await this.followRequestsRepository.insert({
|
||||
const followRequest = await this.followRequestsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
|
@ -531,7 +533,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followeeHost: followee.host,
|
||||
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
||||
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
// Publish receiveRequest event
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
|
@ -740,7 +742,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||
user: packedFollowee,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
|||
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||
userListId: list.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
throw new UserListService.TooManyUsersError();
|
||||
}
|
||||
|
||||
|
|
52
packages/backend/src/core/UserRenoteMutingService.ts
Normal file
52
packages/backend/src/core/UserRenoteMutingService.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import type { RenoteMutingsRepository } from '@/models/_.js';
|
||||
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserRenoteMutingService {
|
||||
constructor(
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
|
||||
await this.renoteMutingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
muterId: user.id,
|
||||
muteeId: target.id,
|
||||
});
|
||||
|
||||
await this.cacheService.renoteMutingsCache.refresh(user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unmute(mutings: MiRenoteMuting[]): Promise<void> {
|
||||
if (mutings.length === 0) return;
|
||||
|
||||
await this.renoteMutingsRepository.delete({
|
||||
id: In(mutings.map(m => m.id)),
|
||||
});
|
||||
|
||||
const muterIds = [...new Set(mutings.map(m => m.muterId))];
|
||||
for (const muterId of muterIds) {
|
||||
await this.cacheService.renoteMutingsCache.refresh(muterId);
|
||||
}
|
||||
}
|
||||
}
|
205
packages/backend/src/core/UserSearchService.ts
Normal file
205
packages/backend/src/core/UserSearchService.ts
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
function defaultActiveThreshold() {
|
||||
return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserSearchService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザ名とホスト名によるユーザ検索を行う.
|
||||
*
|
||||
* - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
|
||||
* 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
|
||||
* 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
|
||||
* 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
|
||||
* 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
|
||||
* - ログインしていない場合は、以下の順序で検索が行われる.
|
||||
* 1. 一定期間以内に更新されたユーザ
|
||||
* 2. 一定期間以内に更新されていないユーザ
|
||||
* - それぞれの検索結果はユーザ名の昇順でソートされる.
|
||||
* - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
|
||||
* (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
|
||||
* - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
|
||||
* - ユーザ名の検索は大文字小文字を区別しない.
|
||||
* - ホスト名の検索は大文字小文字を区別しない.
|
||||
* - 検索結果は最大で {@link opts.limit} 件までとなる.
|
||||
*
|
||||
* ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
|
||||
*
|
||||
* @param params 検索条件.
|
||||
* @param opts 関数の動作を制御するオプション.
|
||||
* @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
|
||||
* @see {@link UserSearchService#buildSearchUserQueries}
|
||||
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
|
||||
*/
|
||||
@bindThis
|
||||
public async search(
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
},
|
||||
opts?: {
|
||||
limit?: number,
|
||||
detail?: boolean,
|
||||
},
|
||||
me?: MiUser | null,
|
||||
): Promise<Packed<'User'>[]> {
|
||||
const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
|
||||
|
||||
let resultSet = new Set<MiUser['id']>();
|
||||
const limit = opts?.limit ?? 10;
|
||||
for (const query of queries) {
|
||||
const ids = await query
|
||||
.select('user.id')
|
||||
.limit(limit - resultSet.size)
|
||||
.orderBy('user.usernameLower', 'ASC')
|
||||
.getRawMany<{ user_id: MiUser['id'] }>()
|
||||
.then(res => res.map(x => x.user_id));
|
||||
|
||||
resultSet = new Set([...resultSet, ...ids]);
|
||||
if (resultSet.size >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
|
||||
[...resultSet].slice(0, limit),
|
||||
me,
|
||||
{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
|
||||
* @param me
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private buildSearchUserQueries(
|
||||
me: MiUser,
|
||||
params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
},
|
||||
) {
|
||||
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||
|
||||
const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
}));
|
||||
inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
// 自分自身がヒットするとしたらここ
|
||||
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
activeUserQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
|
||||
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
inactiveUserQuery.setParameters(followingUserQuery.getParameters());
|
||||
|
||||
return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
|
||||
}
|
||||
|
||||
/**
|
||||
* ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private buildSearchUserNoLoginQueries(params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
activeThreshold?: Date,
|
||||
}) {
|
||||
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
|
||||
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
|
||||
|
||||
const activeUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
}));
|
||||
|
||||
const inactiveUserQuery = this.generateUserQueryBuilder(params)
|
||||
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
|
||||
|
||||
return [activeUserQuery, inactiveUserQuery];
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
|
||||
* @param params
|
||||
* @private
|
||||
*/
|
||||
@bindThis
|
||||
private generateUserQueryBuilder(params: {
|
||||
username?: string | null,
|
||||
host?: string | null,
|
||||
}): SelectQueryBuilder<MiUser> {
|
||||
const userQuery = this.usersRepository.createQueryBuilder('user');
|
||||
|
||||
if (params.username) {
|
||||
userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
|
||||
}
|
||||
|
||||
if (params.host) {
|
||||
if (params.host === this.config.hostname || params.host === '.') {
|
||||
userQuery.andWhere('user.host IS NULL');
|
||||
} else {
|
||||
userQuery.andWhere('user.host LIKE :host', {
|
||||
host: sqlLikeEscape(params.host.toLowerCase()) + '%',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
userQuery.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
return userQuery;
|
||||
}
|
||||
}
|
|
@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
|
|||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -50,4 +53,23 @@ export class UserService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
|
||||
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
|
||||
*
|
||||
* @see SystemWebhookService.enqueueSystemWebhook
|
||||
*/
|
||||
@bindThis
|
||||
public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
|
||||
const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
|
||||
const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
|
||||
for (const webhookId of recipientWebhookIds) {
|
||||
await this.systemWebhookService.enqueueSystemWebhook(
|
||||
webhookId,
|
||||
type,
|
||||
packedUser,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
99
packages/backend/src/core/UserWebhookService.ts
Normal file
99
packages/backend/src/core/UserWebhookService.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UserWebhookService implements OnApplicationShutdown {
|
||||
private activeWebhooksFetched = false;
|
||||
private activeWebhooks: MiWebhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveWebhooks() {
|
||||
if (!this.activeWebhooksFetched) {
|
||||
this.activeWebhooks = await this.webhooksRepository.findBy({
|
||||
active: true,
|
||||
});
|
||||
this.activeWebhooksFetched = true;
|
||||
}
|
||||
|
||||
return this.activeWebhooks;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'webhookCreated': {
|
||||
if (body.active) {
|
||||
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'webhookUpdated': {
|
||||
if (body.active) {
|
||||
const i = this.activeWebhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'webhookDeleted': {
|
||||
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
|
@ -42,6 +42,12 @@ export class UtilityService {
|
|||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
|
||||
if (!silencedHosts || host == null) return false;
|
||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public concatNoteContentsForKeyWordCheck(content: {
|
||||
cw?: string | null;
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService implements OnApplicationShutdown {
|
||||
private webhooksFetched = false;
|
||||
private webhooks: MiWebhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveWebhooks() {
|
||||
if (!this.webhooksFetched) {
|
||||
this.webhooks = await this.webhooksRepository.findBy({
|
||||
active: true,
|
||||
});
|
||||
this.webhooksFetched = true;
|
||||
}
|
||||
|
||||
return this.webhooks;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'webhookUpdated':
|
||||
if (body.active) {
|
||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
case 'webhookDeleted':
|
||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
|
|||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { concat, unique } from '@/misc/prelude/array.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { getApIds } from './type.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import type { ApObject } from './type.js';
|
||||
|
@ -41,7 +40,7 @@ export class ApAudienceService {
|
|||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||
)).filter(isNotNull);
|
||||
)).filter(x => x != null);
|
||||
|
||||
if (toGroups.public.length > 0) {
|
||||
return {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue