merge upstream

This commit is contained in:
Hazelnoot 2025-03-25 16:14:53 -04:00
commit d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions

View file

@ -265,6 +265,11 @@ allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -1,5 +1,5 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
# Sharkey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌──────────────────────────────┐
@ -9,14 +9,14 @@
# First of all, let me tell you a story that may possibly be
# boring to you and possibly important to you.
#
# Misskey is licensed under the AGPLv3 license. This license is
# Sharkey is licensed under the AGPLv3 license. This license is
# known to be often misunderstood. Please read the following
# instructions carefully and select the appropriate option so
# that you do not negligently cause a license violation.
#
# --------
# Option 1: If you host Misskey AS-IS (without any changes to
# Option 1: If you host Sharkey AS-IS (without any changes to
# the source code. forks are not included).
#
# Step 1: Congratulations! You don't need to do anything.
@ -28,8 +28,8 @@
# this repository. Strictly speaking, it doesn't have
# to be a Git repository, but you'll probably use Git!
#
# Step 1: Build and run the Misskey server first.
# Step 2: Open <https://your.misskey.example/admin/settings> in
# Step 1: Build and run the Sharkey server first.
# Step 2: Open <https://your.sharkey.example/admin/settings> in
# your browser with the administrator account.
# Step 3: Enter the URL of your Git repository in the
# "Repository URL" field.
@ -37,7 +37,7 @@
# --------
# Option 3: If neither of the above applies to you.
# (In this case, the source code should be published
# on the Misskey interface. IT IS NOT ENOUGH TO
# on the Sharkey interface. IT IS NOT ENOUGH TO
# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
# E-MAIL OR OTHER MEANS. If you are not satisfied
# with this, it is recommended that you read the
@ -73,11 +73,11 @@ url: https://example.tld/
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey requires a reverse proxy to support HTTPS connections.
# Sharkey requires a reverse proxy to support HTTPS connections.
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# | User | ---> || Proxy (443) | ---> | Sharkey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
@ -85,7 +85,7 @@ url: https://example.tld/
# An encrypted connection with HTTPS is highly recommended
# because tokens may be transferred in GET requests.
# The port that your Misskey server should listen on.
# The port that your Sharkey server should listen on.
port: 3000
# ┌──────────────────────────┐
@ -336,6 +336,11 @@ attachLdSignatureForRelays: true
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -1,5 +1,5 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
# Sharkey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌──────────────────────────────┐
@ -9,14 +9,14 @@
# First of all, let me tell you a story that may possibly be
# boring to you and possibly important to you.
#
# Misskey is licensed under the AGPLv3 license. This license is
# Sharkey is licensed under the AGPLv3 license. This license is
# known to be often misunderstood. Please read the following
# instructions carefully and select the appropriate option so
# that you do not negligently cause a license violation.
#
# --------
# Option 1: If you host Misskey AS-IS (without any changes to
# Option 1: If you host Sharkey AS-IS (without any changes to
# the source code. forks are not included).
#
# Step 1: Congratulations! You don't need to do anything.
@ -28,8 +28,8 @@
# this repository. Strictly speaking, it doesn't have
# to be a Git repository, but you'll probably use Git!
#
# Step 1: Build and run the Misskey server first.
# Step 2: Open <https://your.misskey.example/admin/settings> in
# Step 1: Build and run the Sharkey server first.
# Step 2: Open <https://your.sharkey.example/admin/settings> in
# your browser with the administrator account.
# Step 3: Enter the URL of your Git repository in the
# "Repository URL" field.
@ -37,7 +37,7 @@
# --------
# Option 3: If neither of the above applies to you.
# (In this case, the source code should be published
# on the Misskey interface. IT IS NOT ENOUGH TO
# on the Sharkey interface. IT IS NOT ENOUGH TO
# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY
# E-MAIL OR OTHER MEANS. If you are not satisfied
# with this, it is recommended that you read the
@ -65,10 +65,10 @@
# Password to initiate setting up admin account.
# It will not be used after the initial setup is complete.
#
# Be sure to change this when you set up Misskey via the Internet.
# Be sure to change this when you set up Sharkey via the Internet.
#
# The provider of the service who sets up Misskey on behalf of the customer should
# set this value to something unique when generating the Misskey config file,
# The provider of the service who sets up Sharkey on behalf of the customer should
# set this value to something unique when generating the Sharkey config file,
# and provide it to the customer.
#
# setupPassword: example_password_please_change_this_or_you_will_get_hacked
@ -86,11 +86,11 @@ url: https://example.tld/
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey requires a reverse proxy to support HTTPS connections.
# Sharkey requires a reverse proxy to support HTTPS connections.
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# | User | ---> || Proxy (443) | ---> | Sharkey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
@ -98,13 +98,13 @@ url: https://example.tld/
# An encrypted connection with HTTPS is highly recommended
# because tokens may be transferred in GET requests.
# The port that your Misskey server should listen on.
# The port that your Sharkey server should listen on.
port: 3000
# the address to bind to, defaults to "every address"
# address: '0.0.0.0'
# You can also use UNIX domain socket.
# socket: /path/to/misskey.sock
# socket: /path/to/sharkey.sock
# chmodSocket: '777'
# ┌──────────────────────────┐
@ -358,7 +358,7 @@ proxyRemoteFiles: true
# Movie Thumbnail Generation URL
# There is no reference implementation.
# For example, Misskey will point to the following URL:
# For example, Sharkey will point to the following URL:
# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
#videoThumbnailGenerator: https://example.com
@ -379,6 +379,11 @@ attachLdSignatureForRelays: true
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -7,7 +7,9 @@
"ghcr.io/devcontainers/features/node:1": {
"version": "22.11.0"
},
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
"ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "10.6.1"
}
},
"forwardPorts": [3000],
"postCreateCommand": "/bin/bash .devcontainer/init.sh",

View file

@ -7,8 +7,6 @@ sudo apt-get update
sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
git config --global --add safe.directory /workspace
git submodule update --init
corepack install
corepack enable
pnpm config set store-dir /home/node/.local/share/pnpm/store
pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml

2
.npmrc
View file

@ -1,2 +1,4 @@
@transfem-org:registry=https://activitypub.software/api/v4/packages/npm/
engine-strict = true
save-exact = true
shell-emulator = true

View file

@ -1,3 +1,136 @@
## 2025.3.2
### General
- Feat: チャットがリニューアルして復活しました(beta)
- 既存のDM機能よりも便利で効率的な実装になっています
- チャットを受け付ける相手を制限可能です
- 誰でも / フォローユーザーのみ / フォロワーのみ / 相互のみ / 受け付けない から選択できます
- 自分からメッセージを送った相手とは上記の設定に関わらずチャット可能です
- チャット機能を開放するかどうかをロールで制御可能です
- ルームを作成して、複数人でのチャットも可能です
- 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です
- 参加中のルームをミュートして通知が来ないように設定可能です
- メッセージにはリアクションも可能です
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
### Client
- Feat: 設定の管理が強化されました
- 内部処理が一新され、安定性とパフォーマンスが向上しました
- 全てのクライアント設定がエクスポート(バックアップ)/インポート対象に含まれるようになりました
- プラグイン、テーマ、クライアントに追加されたすべてのアカウント情報も含まれるようになりました
- 自動で設定データをサーバーにバックアップできるように
- 設定→設定のプロファイル→自動バックアップ で有効にできます
- 新しいデバイスからログインしたり、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能)
- 任意の設定項目をデバイス間で同期できるように
- 設定項目の「...」メニュー→「デバイス間で同期」
- 同期をオンにした際にサーバーに保存された値とローカルの値が競合する場合はどちらを優先するか選択できます
- 任意の設定項目を初期値にリセットできるように
- 設定項目の「...」メニュー→「初期値にリセット」
- アカウントごとに設定値が分離される設定とそうでないクライアント設定が混在していた(かつ分離するかどうかを設定不可だった)のを、基本的に一律でクライアント全体に適用されるようにし、個別でアカウントごとに異なる設定を行えるように
- 設定項目の「...」メニュー→「アカウントで上書き」をオンにすることで、設定値をそのアカウントでだけ適用するようにできます
- ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました
- 再度ログインすればサーバーのバックアップから設定データを復元可能です
- エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました
- Feat: 画面を重ねて表示するオプションを実装(実験的)
- 設定 → その他 → 実験的機能 → Enable stacking router view
- Enhance: プラグインの管理が強化されました
- インストール/アンインストール/設定の変更時にリロード不要になりました
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
- Enhance: テーマ設定画面のデザインを改善
- Enhance: 投稿フォームの設定メニューを改良
- 投稿フォームをリセットできるように
- 文字数カウントを復活
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
### Server
- Enhance 全体的なパフォーマンス向上
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
## 2025.3.1
### General
- pnpmをv10に更新
- Corepackを削除
### Client
- Feat: 設定の検索を追加(実験的)
- Enhance: 設定項目の再配置
### Server
- Fix: DBマイグレーション際にシステムアカウントのユーザーID判定が正しくない問題を修正
- Fix: user.featured列が状況によってJSON文字列になっていたのを修正
## 2025.3.0
### General
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように
書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。
- Fix: システムアカウントが削除できる問題を修正
### Client
- Enhance: モデレーターがセンシティブ設定を変更する際に確認ダイアログを出すように
- Enhance: 「UIのアニメーションを減らす」で画面上のエフェクトも減らせるように
- Enhance: 投稿フォームにおける、メディアの添付可能個数のカウントを反転しました
- これまでの表示は`添付可能残り個数/上限数`でしたが、`添付個数/上限数`としました
- Fix: フォローされたときのメッセージがちらつくことがある問題を修正
- Fix: 投稿ダイアログがサイズ限界を超えた際にスクロールできない問題を修正
### Server
- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正
- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/895)
## 2025.2.1
### General
- Feat: アクセストークン発行時に通知するように
- Feat: 実験的なGoogleAnalyticsサポートを追加
- 依存関係の更新
### Client
- Feat: 投稿フォームで画像をプレビュー可能に
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
- Enhance: クライアントエラー画面の多言語対応
- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441'
- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 )
- Enhance: リアクションする際に確認ダイアログを表示できるように
- Enhance: コントロールパネルのユーザ検索で入力された情報をページ遷移で損なわないように `#15437`
- Enhance: CWの注釈で入力済みの文字数を表示
- Enhance: ノート検索ページのデザイン調整
(Cherry-picked from https://github.com/taiyme/misskey/pull/273)
- Fix: ノートページで、クリップ一覧が表示されないことがある問題を修正
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`
- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正
- Fix: Play の再読込時に UI が以前の状態を引き継いでしまう問題を修正 `#14378`
- Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 )
- Fix: ユーザのサジェスト中に@を入力してもサジェスト結果が消えないように `#14385`
- Fix: CWの注釈が100文字を超えている場合、ート投稿ボタンを非アクティブに
- Fix: テーマ選択で現在のテーマが初期表示されていない問題を修正
- 翻訳の更新
### Server
- Enhance: 成り済まし対策として、ActivityPub照会された時にリモートのリダイレクトを拒否できるように (config.disallowExternalApRedirect)
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正
- Fix: HTTPプロキシとその除外設定を行った状態でカスタム絵文字の一括インポートをしたとき、除外設定が効かないのを修正( #8766 )
- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886)
- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように
- Fix: `update-meta`でobjectStoragePrefixにS3_SAFEかつURL-safeでない文字列を使えないように
- Fix: クリップの説明欄を更新する際に空にできない問題を修正
- Fix: フォロワーではないユーザーにリートもしくは返信された場合にートのDeleteアクティビティが送られていない問題を修正
## 2025.2.0
### General

View file

@ -281,7 +281,6 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
query?: Record<string, string>;
loginRequired?: boolean;
hash?: string;
globalCacheKey?: string;
children?: RouteDef[];
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View file

@ -233,7 +233,7 @@ describe('After user setup', () => {
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click();
cy.contains('Hello, Misskey!');
cy.contains('Hello, Misskey!', { timeout: 15000 });
});
it('open note form with hotkey', () => {

View file

@ -1012,6 +1012,8 @@ sourceCode: "الشفرة المصدرية"
flip: "اقلب"
lastNDays: "آخر {n} أيام"
surrender: "ألغِ"
postForm: "أنشئ ملاحظة"
information: "عن"
_delivery:
stop: "مُعلّق"
_initialAccountSetting:
@ -1584,3 +1586,7 @@ _offlineScreen:
_remoteLookupErrors:
_noSuchObject:
title: "غير موجود"
_search:
searchScopeAll: "الكل"
searchScopeLocal: "المحلي"
searchScopeUser: "مستخدم محدد"

View file

@ -852,6 +852,8 @@ replies: "জবাব"
renotes: "রিনোট"
sourceCode: "সোর্স কোড"
flip: "উল্টান"
postForm: "নোট লিখুন"
information: "আপনার সম্পর্কে"
_delivery:
stop: "স্থগিত করা হয়েছে"
_type:
@ -1348,3 +1350,6 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "পাওয়া যায়নি"
_search:
searchScopeAll: "সবগুলো"
searchScopeLocal: "স্থানীয়"

View file

@ -66,7 +66,7 @@ copyFolderId: "Copiar ID de la carpeta"
copyProfileUrl: "Copiar adreça URL del perfil"
searchUser: "Cercar un usuari"
searchThisUsersNotes: "Cercar les publicacions de l'usuari"
reply: "Respon"
reply: "Respostes"
loadMore: "Carregar més"
showMore: "Veure més"
showLess: "Mostrar menys"
@ -111,7 +111,7 @@ followRequests: "Peticions de seguiment"
unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji"
renote: "Impulsar "
renote: "Impulsar"
unrenote: "Anul·la l'impuls"
renoted: "S'ha impulsat"
renotedToX: "Impulsat per {name}."
@ -260,7 +260,7 @@ noCustomEmojis: "No hi ha emojis personalitzats"
noJobs: "No hi ha feines"
federating: "Federant"
blocked: "Bloquejat"
suspended: "Suspés"
suspended: "Anul·lar subscripció "
all: "tot"
subscribing: "Subscrit a"
publishing: "S'està publicant"
@ -646,7 +646,7 @@ disablePlayer: "Tanca el reproductor de vídeo"
expandTweet: "Expandir post"
themeEditor: "Editor de temes"
description: "Descripció"
describeFile: "Afegir subtitulació"
describeFile: "Afegeix una descripció "
enterFileDescription: "Escriu un peu de foto"
author: "Autor"
leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?"
@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} està parlant sobre \"{word}\""
makeActive: "Activar"
display: "Veure"
copy: "Copiar"
copiedToClipboard: "Copiat al porta papers"
metrics: "Mètriques"
overview: "Visió General"
logs: "Registres"
@ -1114,7 +1115,7 @@ forceShowAds: "Mostra els anuncis sempre "
addMemo: "Afegir recordatori"
editMemo: "Editar recordatori"
reactionsList: "Reaccions"
renotesList: "Impulsos"
renotesList: "Llistat d'impulsos "
notificationDisplay: "Notificacions"
leftTop: "Dalt a l'esquerra "
rightTop: "Dalt a la dreta "
@ -1139,7 +1140,7 @@ channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista d
thisChannelArchived: "Aquest Canal ha sigut arxivat."
displayOfNote: "Mostrar notes"
initialAccountSetting: "Configuració del perfil"
youFollowing: "Seguint"
youFollowing: "Segueixes "
preventAiLearning: "Descartar l'ús d'aprenentatge automàtic (IA Generativa)"
preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automàtic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta."
options: "Opcions"
@ -1189,8 +1190,8 @@ currentAnnouncements: "Informes actuals"
pastAnnouncements: "Informes passats"
youHaveUnreadAnnouncements: "Tens informes per llegir."
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
replies: "Respon"
renotes: "Impulsar "
replies: "Respostes"
renotes: "Impulsos"
loadReplies: "Mostrar les respostes"
loadConversation: "Mostrar la conversació "
pinnedList: "Llista fixada"
@ -1309,6 +1310,69 @@ availableRoles: "Roles disponibles "
acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills."
federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador."
federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors."
confirmOnReact: "Confirmar en reaccionar"
reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?"
markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?"
unmarkAsSensitiveConfirm: "Vols deixar de marcar com a sensible aquest contingut?"
preferences: "Preferències "
accessibility: "Accessibilitat "
preferencesProfile: "Perfil de configuració "
copyPreferenceId: "Copiar l'ID de la configuració "
resetToDefaultValue: "Restaura al valor per defecte "
overrideByAccount: "Anul·lar per compte"
untitled: "Sense títol "
noName: "No hi ha un nom disponible "
skip: "Ometre "
restore: "Restaurar "
syncBetweenDevices: "Sincronització entre dispositius"
preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu"
preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?"
preferenceSyncConflictChoiceServer: "Valors de configuració del servidor"
preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu "
preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització "
paste: "Pegar"
emojiPalette: "Calaix d'emojis"
postForm: "Formulari de publicació"
textCount: "Nombre de caràcters "
information: "Informació"
_emojiPalette:
palettes: "Calaixos d'emojis"
enableSyncBetweenDevicesForPalettes: "Activa la sincronització dels calaixos d'emojis entre dispositius"
paletteForMain: "Calaix d'emojis principal"
paletteForReaction: "Calaix d'emojis per reaccions"
_settings:
driveBanner: "Pots gestionar i configurar el Disc, comprovar el seu ús i establir una configuració per a la càrrega d'arxius."
pluginBanner: "Els complements poden fer-se servir per ampliar les funcionalitats del client. Els complements poden instal·lar-se, configurar-se individualment i gestionar-se."
notificationsBanner: "Pots configurar el tipus i l'abast de les notificacions que es rebran del servidor, també les notificacions emergents."
api: "API"
webhook: "Webhook"
serviceConnection: "Relació entre serveis"
serviceConnectionBanner: "Pots configurar i gestionar tokens d'accés i webhooks per integrar serveis i aplicacions externes."
accountData: "Dades del compte"
accountDataBanner: "Exportació/Importació i gestió d'arxius amb dades del compte."
muteAndBlockBanner: "Pots configurar i gestionar els continguts que desitges amagar i restringir les accions de determinats usuaris."
accessibilityBanner: "Els clients poden personalitzar-se i configurar-se per un ús òptim en funció de la seva visió i comportament."
privacyBanner: "Pots establir la configuració de privacitat del compte, com el grau de visibilitat del teu contingut, la facilitat per trobar-ho i si es pot aprovar els seguidors."
securityBanner: "Configura les opcions relacionades amb la seguretat del teu compte com ara contrasenyes, mètodes per iniciar sessió, aplicacions d'autentificació i claus d'accés."
preferencesBanner: "Pots configurar el comportament general del client segons les teves preferències."
appearanceBanner: "Pots configurar les preferències relacionades amb la visualització i l'aspecte del client segons el teu parer."
soundsBanner: "Configuració dels sons que reproduirà el client."
timelineAndNote: "Línia de temps i nota"
makeEveryTextElementsSelectable: "Fes que tots els elements del text siguin seleccionables"
makeEveryTextElementsSelectable_description: "L'activació pot reduir la usabilitat en determinades ocasions."
_preferencesProfile:
profileName: "Nom del perfil"
profileNameDescription: "Estableix un nom que identifiqui aquest dispositiu."
profileNameDescription2: "Per exemple: \"PC Principal\", \"Smartphone\", etc"
_preferencesBackup:
autoBackup: "Còpia de seguretat automàtica "
restoreFromBackup: "Restaurar des d'una còpia de seguretat"
noBackupsFoundTitle: "No s'ha trobat cap còpia de seguretat"
noBackupsFoundDescription: "No s'han trobat còpies de seguretat creades automàticament, però si has desat, manualment, un arxiu de còpia de seguretat, pots importar-lo i carregar-lo."
selectBackupToRestore: "Seleccionar la còpia de seguretat que vols restaurar"
youNeedToNameYourProfileToEnableAutoBackup: "Has de posar-li un nom al teu perfil per poder activar les còpies de seguretat automàtiques."
autoPreferencesBackupIsNotEnabledForThisDevice: "La còpia de seguretat automàtica no es troba activada en aquest dispositiu."
backupFound: "Còpia de seguretat de la configuració trobada"
_accountSettings:
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."
@ -1319,6 +1383,7 @@ _accountSettings:
makeNotesHiddenBefore: "Fes que les notes antigues siguin privades"
makeNotesHiddenBeforeDescription: "Mentres aquesta funció estigui activada les notes que hagin superat una data i hora fixada o hagi passat el temps establert només seran visibles per a tu. Si la desactives es restablirà també l'estat públic de les notes."
mayNotEffectForFederatedNotes: "Això pot ser que no afecti les notes federades."
mayNotEffectSomeSituations: "Aquestes restriccions són simplificades. Pot ser que no s'apliquin en determinades situacions, com quan es modera o visualitza un servidor remot."
notesHavePassedSpecifiedPeriod: "Notes publicades durant un període de temps especificat."
notesOlderThanSpecifiedDateAndTime: "Notes més antigues de la data i temps especificat "
_abuseUserReport:
@ -1330,7 +1395,7 @@ _abuseUserReport:
resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament."
_delivery:
status: "Estat d'entrega "
stop: "Suspés"
stop: "Anul·lar subscripció "
resume: "Torna a enviar"
_type:
none: "S'està publicant"
@ -1967,6 +2032,7 @@ _theme:
installed: "{name} Instal·lat "
installedThemes: "Temes instal·lats "
builtinThemes: "Temes integrats"
instanceTheme: "Tema de la instància "
alreadyInstalled: "Aquest tema ja es troba instal·lat "
invalid: "El format d'aquest tema no és correcte"
make: "Crear un tema"
@ -2006,7 +2072,7 @@ _theme:
hashtag: "Etiqueta"
mention: "Menció"
mentionMe: "Mencions (jo)"
renote: "Renotar"
renote: "Impulsar"
modalBg: "Fons del modal"
divider: "Divisor"
scrollbarHandle: "Maneta de la barra de desplaçament"
@ -2440,13 +2506,15 @@ _notification:
flushNotification: "Netejar notificacions"
exportOfXCompleted: "Completada l'exportació de {x}"
login: "Algú ha iniciat sessió "
createToken: "Token d'accés generat"
createTokenDescription: "Si no saps què és, esborra el token des de {text}."
_types:
all: "Tots"
note: "Notes noves"
follow: "Segueix-me"
mention: "Menció"
reply: "Respostes"
renote: "Renotar"
renote: "Impulsos"
quote: "Citar"
reaction: "Reaccions"
pollEnded: "Enquesta terminada"
@ -2456,12 +2524,13 @@ _notification:
achievementEarned: "Assoliment desbloquejat"
exportCompleted: "Exportació completada"
login: "Iniciar sessió"
createToken: "Creació de tokens d'accés "
test: "Prova la notificació"
app: "Notificacions d'aplicacions"
_actions:
followBack: "També et segueix"
reply: "Respondre"
renote: "Renotar"
renote: "Impulsar"
_deck:
alwaysShowMainColumn: "Mostrar sempre la columna principal"
columnAlign: "Alinea les columnes"
@ -2483,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "Usa una interfície senzilla per a les pàgines navegades"
usedAsMinWidthWhenFlexible: "L'amplada mínima es farà servir quan \"Ajust automàtic de l'amplada\" estigui activat"
flexible: "Ajust automàtic de l'amplada"
enableSyncBetweenDevicesForProfiles: "Activar la sincronització de la informació de perfils de dispositiu a dispositiu"
_columns:
main: "Principal"
widgets: "Ginys"
@ -2590,6 +2660,7 @@ _moderationLogTypes:
deletePage: "Esborrar la pàgina"
deleteFlash: "Esborrar el guió"
deleteGalleryPost: "Esborrar la publicació de la galeria"
updateProxyAccountDescription: "Actualitzar descripció del compte proxy"
_fileViewer:
title: "Detall del fitxer"
type: "Tipus de fitxer"
@ -2603,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho."
_plugin:
title: "Vols instal·lar aquest afegit?"
metaTitle: "Informació de l'afegit "
_theme:
title: "Vols instal·lar aquest tema?"
metaTitle: "Informació del tema"
_meta:
base: "Paleta de colors base"
_vendorInfo:
@ -2822,8 +2891,6 @@ _remoteLookupErrors:
_responseInvalid:
title: "La resposta no és correcta "
description: "Hem pogut comunicar-nos amb aquest servidor, però les dades rebudes no són correctes."
_responseInvalidIdHostNotMatch:
description: "El domini de l'adreça introduïda no és el mateix que el domini de l'adreça final obtinguda. Si estàs consultant continguts remots mitjançant servidors tercers, torna a fer la consulta fent servir l'adreça que es pot obtenir en el servidor origen."
_noSuchObject:
title: "No s'ha trobat"
description: "No es pot trobar el recurs sol·licitat, si us plau comprova l'adreça una altra vegada."
@ -2840,3 +2907,23 @@ _captcha:
_unknown:
title: "Error CAPTCHA"
text: "S'ha produït un error inesperat."
_bootErrors:
title: "Hi ha hagut en error en carregar"
serverError: "Si el problema persisteix després d'esperar una mica i recarregar, posa't en contacte amb l'administrador del servidor amb el següent codi d'error."
solution: "Per intentar resoldre el problema pots fer el següent."
solution1: "Actualitza el navegador i el sistema operatiu a l'última versió "
solution2: "Desactiva els adblockers"
solution3: "Esborra la memòria cau del navegador"
solution4: "(Navegador Tor) configura dom.webaudio.enabled a true"
otherOption: "Altres opcions"
otherOption1: "Esborrar la configuració i la memòria cau del client"
otherOption2: "Iniciar client senzill"
otherOption3: "Iniciar l'eina de reparació "
_search:
searchScopeAll: "Tot"
searchScopeLocal: "Local"
searchScopeServer: "Instància "
searchScopeUser: "Especificar usuari"
pleaseEnterServerHost: "Introdueix l'adreça de la instància "
pleaseSelectUser: "Selecciona un usuari"
serverHostPlaceholder: "Ex: misskey.example.com"

View file

@ -5,6 +5,7 @@ introMisskey: "Vítejte! Misskey je otevřený a decentralizovaný microblogový
poweredByMisskeyDescription: "{name} je jeden ze serverů využívající open source platformu <b>Misskey<b> (nazývaná \"Misskey instance\")."
monthAndDay: "{day}. {month}."
search: "Vyhledávání"
reset: "Obnovit"
notifications: "Oznámení"
username: "Uživatelské jméno"
password: "Heslo"
@ -168,6 +169,9 @@ addAccount: "Přidat účet"
reloadAccountsList: "Obnovit list účtů"
loginFailed: "Přihlášení se nezdařilo."
showOnRemote: "Více na původním profilu"
continueOnRemote: "Pokračujte na původní profil"
chooseServerOnMisskeyHub: "Vyberete si server z Misskey Hubu"
inputHostName: "Zadejte doménu"
general: "Obecně"
wallpaper: "Obrázek na pozadí"
setWallpaper: "Nastavení obrázku na pozadí"
@ -192,6 +196,7 @@ perHour: "za hodinu"
perDay: "za den"
stopActivityDelivery: "Přestat zasílat aktivitu"
blockThisInstance: "Blokovat tuto instanci"
silenceThisInstance: "Utišit tuto instanci"
operations: "Operace"
software: "Software"
version: "Verze"
@ -365,8 +370,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "Aktivovat hCaptchu"
hcaptchaSiteKey: "Klíč stránky"
hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
mcaptcha: "mCaptcha"
enableMcaptcha: "Aktivovat mCaptchu"
mcaptchaSiteKey: "Klíč stránky"
mcaptchaSecretKey: "Tajný Klíč (Secret Key)"
mcaptchaInstanceUrl: "URL mCaptcha serveru"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Zapnout ReCAPTCHu"
recaptchaSiteKey: "Klíč stránky"
@ -1094,6 +1102,8 @@ sourceCode: "Zdrojový kód"
flip: "Otočit"
lastNDays: "Posledních {n} dnů"
surrender: "Zrušit"
postForm: "Formulář pro odeslání"
information: "Informace"
_delivery:
stop: "Suspendováno"
_type:
@ -2024,3 +2034,7 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Nenalezeno"
_search:
searchScopeAll: "Vše"
searchScopeLocal: "Místní"
searchScopeUser: "Upřesnit uživatele"

View file

@ -49,7 +49,7 @@ pin: "An dein Profil anheften"
unpin: "Von deinem Profil lösen"
copyContent: "Inhalt kopieren"
copyLink: "Link kopieren"
copyRemoteLink: "Renote-Link kopieren"
copyRemoteLink: "Remote-Link kopieren"
copyLinkRenote: "Renote-Link kopieren"
delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten"
@ -863,7 +863,7 @@ administration: "Verwaltung"
accounts: "Benutzerkonten"
switch: "Wechseln"
noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert."
noInquiryUrlWarning: "Keine gültige URL."
noInquiryUrlWarning: "Keine gültige Kontakt-URL."
noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert."
configure: "Konfigurieren"
postToGallery: "Neuen Galeriebeitrag erstellen"
@ -1308,6 +1308,10 @@ pleaseSelectAccount: "Bitte Konto auswählen"
availableRoles: "Verfügbare Rollen"
federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren."
federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren."
postForm: "Notizfenster"
information: "Über"
_settings:
webhook: "Webhook"
_accountSettings:
requireSigninToViewContents: "Anmeldung erfordern, um Inhalte anzuzeigen"
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
@ -1383,6 +1387,9 @@ _initialTutorial:
title: "Was sind Notizen?"
description: "Beiträge auf Misskey heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert."
reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen."
renote: "Du kannst diese Notiz in deiner eigenen Chronik teilen. Du kannst sie auch mit deinen Kommentaren zitieren."
reaction: "Du kannst der Notiz Reaktionen hinzufügen. Weitere Einzelheiten werden auf der nächsten Seite erläutert."
menu: "Du kannst Details zu Notizen anzeigen, Links kopieren und verschiedene andere Aktionen durchführen."
_reaction:
title: "Was sind Reaktionen?"
description: "Auf Notizen kann mit verschiedenen Emojis reagiert werden. Reaktionen ermöglichen es dir, Nuancen auszudrücken, die mit einem einfachen „Gefällt mir“ vielleicht nicht ausgedrückt werden können."
@ -1401,13 +1408,21 @@ _initialTutorial:
_visibility:
description: "Du kannst einschränken, wer deine Notiz sehen kann."
public: "Deine Notiz wird für alle Nutzer sichtbar sein."
direct: "Die Notiz wird nur für den angegebenen Benutzer veröffentlicht und der Empfänger wird benachrichtigt. Kann anstelle von Direktnachrichten verwendet werden."
doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!"
doNotSendConfidencialOnDirect2: "Die Administratoren des Servers können den Inhalt der Notiz sehen. Sei vorsichtig mit sensiblen Informationen, wenn du Direktnachrichten an Benutzer auf nicht vertrauenswürdigen Servern sendest."
localOnly: "Wenn du eine Notiz mit dieser Einstellung veröffentlichst, wird sie nicht an andere Server weitergeleitet. Benutzer auf anderen Servern können diese Notizen nicht direkt sehen, unabhängig von den obigen Anzeigeeinstellungen."
_cw:
title: "Inhaltswarnung"
description: "Anstelle des Textes wird das angezeigt, was du im Abschnitt „Anmerkungen“ angibst. Drücke auf „Inhalt anzeigen“, um den vollständigen Text zu sehen."
_exampleNote:
cw: "Das wird dich bestimmt hungrig machen!"
note: "Ich hatte gerade einen Donut mit Schokoladenüberzug 🍩😋"
_howToMakeAttachmentsSensitive:
title: "Wie markiert man Anhänge als sensibel?"
tryThisFile: "Versuche, das angehängte Bild als sensibel zu markieren!"
_exampleNote:
note: "Ups, ich habe es vergeigt, den Natto-Deckel zu öffnen..."
method: "Um einen Anhang als sensibel zu kennzeichnen, klicke auf das Vorschaubild der Datei, um das Menü zu öffnen, und klicke auf „Als sensibel markieren“."
sensitiveSucceeded: "Wenn du Dateien anhängst, stelle bitte die Sensibilität entsprechend der Serverrichtlinien ein."
doItToContinue: "Markiere die angehängte Datei als sensibel, um fortzufahren."
@ -1431,6 +1446,7 @@ _serverSettings:
fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden."
fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen"
fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. "
reactionsBufferingDescription: "Wenn diese Option aktiviert ist, kann sie die Leistung beim Erstellen von Reaktionen erheblich verbessern und die Belastung der Datenbank verringern. Allerdings steigt die Speichernutzung von Redis."
openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
_accountMigration:
@ -1835,6 +1851,7 @@ _plugin:
installWarn: "Installiere bitte nur vertrauenswürdige Plugins."
manage: "Plugins verwalten"
viewSource: "Quelltext anzeigen"
viewLog: "Protokoll anzeigen"
_preferencesBackups:
list: "Erstellte Backups"
saveNew: "Neu erstellen"
@ -1864,6 +1881,8 @@ _aboutMisskey:
contributors: "Hauptmitwirkende"
allContributors: "Alle Mitwirkenden"
source: "Quellcode"
original: "Original"
thisIsModifiedVersion: "{name} verwendet eine modifizierte Version des ursprünglichen Misskey."
translation: "Misskey übersetzen"
donate: "An Misskey spenden"
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
@ -1989,6 +2008,8 @@ _soundSettings:
driveFileTypeWarn: "Diese Datei wird nicht unterstützt"
driveFileTypeWarnDescription: "Bitte wähle eine Audiodatei"
driveFileDurationWarn: "Audio zu lang."
driveFileDurationWarnDescription: "Lange Töne kann die Verwendung von Misskey stören. Trotzdem fortfahren?"
driveFileError: "Audio konnte nicht geladen werden. Bitte ändere die Einstellung."
_ago:
future: "Zukunft"
justNow: "Gerade eben"
@ -2000,6 +2021,10 @@ _ago:
monthsAgo: "vor {n} Monat(en)"
yearsAgo: "vor {n} Jahr(en)"
invalid: "Ungültig"
_timeIn:
seconds: "In {n}s"
minutes: "In {n} Min."
hours: "In {n} Std."
_time:
second: "Sekunde(n)"
minute: "Minute(n)"
@ -2105,6 +2130,7 @@ _auth:
permissionAsk: "Diese Anwendung fordert folgende Berechtigungen"
pleaseGoBack: "Bitte kehre zur Anwendung zurück"
callback: "Es wird zur Anwendung zurückgekehrt"
accepted: "Zugriff gewährt"
denied: "Zugriff verweigert"
pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren."
_antennaSources:
@ -2215,6 +2241,7 @@ _profile:
changeBanner: "Banner ändern"
verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt."
avatarDecorationMax: "Du kannst bis zu {max} Dekorationen hinzufügen."
followedMessage: "Nachricht, wenn dir jemand folgt"
followedMessageDescription: "Du kannst eine kurze Nachricht festlegen, die dem Empfänger angezeigt wird, wenn er dir folgt."
_exportOrImport:
allNotes: "Alle Notizen"
@ -2343,8 +2370,11 @@ _notification:
sendTestNotification: "Testbenachrichtigung senden"
notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus"
reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt"
likedBySomeUsers: "{n} Benutzer mochten deine Notiz"
renotedBySomeUsers: "Renote von {n} Benutzern"
followedBySomeUsers: "Von {n} Benutzern gefolgt"
flushNotification: "Benachrichtigungen löschen"
exportOfXCompleted: "Der Export von {x} ist abgeschlossen"
login: "Neue Anmeldung erfolgt"
_types:
all: "Alle"
@ -2360,7 +2390,9 @@ _notification:
followRequestAccepted: "Akzeptierte Follow-Anfragen"
roleAssigned: "Rolle zugewiesen"
achievementEarned: "Errungenschaft freigeschaltet"
login: "Anmelden"
exportCompleted: "Der Export ist abgeschlossen"
login: "Anmeldung"
test: "Test-Benachrichtigungen"
app: "Benachrichtigungen von Apps"
_actions:
followBack: "folgt dir nun auch"
@ -2370,6 +2402,7 @@ _deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spaltenausrichtung"
addColumn: "Spalte hinzufügen"
newNoteNotificationSettings: "Benachrichtigungseinstellungen für neue Notizen"
configureColumn: "Spalteneinstellungen"
swapLeft: "Mit linker Spalte tauschen"
swapRight: "Mit rechter Spalte tauschen"
@ -2408,6 +2441,7 @@ _drivecleaner:
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings:
createWebhook: "Webhook erstellen"
modifyWebhook: "Webhook bearbeiten"
name: "Name"
secret: "Secret"
trigger: "Auslöser"
@ -2420,12 +2454,22 @@ _webhookSettings:
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"
deleteConfirm: "Bist du sicher, dass du den Webhook löschen willst?"
_abuseReport:
_notificationRecipient:
createRecipient: "Meldungsempfänger hinzufügen"
modifyRecipient: "Bearbeite einen Empfänger für Meldungen"
recipientType: "Art der Benachrichtigung"
_recipientType:
mail: "Email"
webhook: "Webhook"
_captions:
mail: "Die Benachrichtigung wird bei Eingang einer Meldung an die E-Mail-Adressen der Moderatoren gesendet"
webhook: "Sendet eine Benachrichtigung an den System Webhook, wenn eine Meldung eingegangen ist oder gelöst wurde"
keywords: "Schlüsselwort"
notifiedUser: "Zu benachrichtigender Benutzer"
notifiedWebhook: "Zu verwendender Webhook"
deleteConfirm: "Bist du sicher, dass du den Empfänger der Benachrichtigung entfernen möchtest?"
_moderationLogTypes:
createRole: "Rolle erstellt"
deleteRole: "Rolle gelöscht"
@ -2465,6 +2509,7 @@ _moderationLogTypes:
createSystemWebhook: "System-Webhook erstellt"
updateSystemWebhook: "System-Webhook aktualisiert"
deleteSystemWebhook: "System-Webhook gelöscht"
deleteAccount: "Benutzerkonto gelöscht"
deletePage: "Seite gelöscht"
deleteGalleryPost: "Galeriebeitrag gelöscht"
_fileViewer:
@ -2480,10 +2525,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers."
_plugin:
title: "Möchtest du dieses Plugin installieren?"
metaTitle: "Plugininformation"
_theme:
title: "Möchten du dieses Farbschema installieren?"
metaTitle: "Farbschemainfo"
_meta:
base: "Farbschemavorlage"
_vendorInfo:
@ -2573,3 +2616,7 @@ _remoteLookupErrors:
_noSuchObject:
title: "Nicht gefunden"
description: "Die angeforderte Ressource konnte nicht gefunden werden, bitte überprüfe die URI erneut."
_search:
searchScopeAll: "Alle"
searchScopeLocal: "Lokal"
searchScopeUser: "Spezifischer Benutzer"

View file

@ -288,6 +288,8 @@ cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω
icon: "Εικονίδιο"
replies: "Απάντηση"
renotes: "Κοινοποίηση σημειώματος"
postForm: "Φόρμα δημοσίευσης"
information: "Πληροφορίες"
_email:
_follow:
title: "Έχετε ένα νέο ακόλουθο"
@ -397,3 +399,5 @@ _moderationLogTypes:
suspend: "Αποβολή"
_reversi:
total: "Σύνολο"
_search:
searchScopeLocal: "Τοπικό"

View file

@ -132,7 +132,7 @@ reaction: "Reactions"
reactions: "Reactions"
emojiPicker: "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."
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"
@ -586,7 +586,7 @@ popout: "Pop-out"
volume: "Volume"
masterVolume: "Master volume"
notUseSound: "Disable sound"
useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
useSoundOnlyWhenActive: "Output sounds only if Misskey is active"
details: "Details"
renoteDetails: "Renote details"
chooseEmoji: "Select an emoji"
@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} said something about \"{word}\""
makeActive: "Activate"
display: "Display"
copy: "Copy"
copiedToClipboard: "Copied to clipboard"
metrics: "Metrics"
overview: "Overview"
logs: "Logs"
@ -1261,7 +1262,7 @@ copyReplayData: "Copy replay data"
ranking: "Ranking"
lastNDays: "Last {n} days"
backToTitle: "Go back to title"
hemisphere: "Where are you located"
hemisphere: "Where you live"
withSensitive: "Include notes with sensitive files"
userSaysSomethingSensitive: "Post by {name} contains sensitive content"
enableHorizontalSwipe: "Swipe to switch tabs"
@ -1309,6 +1310,69 @@ availableRoles: "Available roles"
acknowledgeNotesAndEnable: "Turn on after understanding the precautions."
federationSpecified: "This server is operated in an allowlist federation. Interacting with servers other than those designated by the administrator is not allowed."
federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers."
confirmOnReact: "Confirm when reacting"
reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?"
markAsSensitiveConfirm: "Do you want to set this media as sensitive?"
unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for this media?"
preferences: "Preferences"
accessibility: "Accessibility"
preferencesProfile: "Preferences profile"
copyPreferenceId: "Copy the preference ID"
resetToDefaultValue: "Revert to default"
overrideByAccount: "Override by the account"
untitled: "Untitled"
noName: "No name"
skip: "Skip"
restore: "Restore"
syncBetweenDevices: "Sync between devices"
preferenceSyncConflictTitle: "The configured value exists on the server."
preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?"
preferenceSyncConflictChoiceServer: "Configured value on server"
preferenceSyncConflictChoiceDevice: "Configured value on device"
preferenceSyncConflictChoiceCancel: "Cancel enabling sync"
paste: "Paste"
emojiPalette: "Emoji palette"
postForm: "Posting form"
textCount: "Character count"
information: "About"
_emojiPalette:
palettes: "Palette"
enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices"
paletteForMain: "Main palette"
paletteForReaction: "Reaction palette"
_settings:
driveBanner: "You can manage and configure the drive, check usage, and configure file upload settings."
pluginBanner: "You can extend client features with plugins. You can install plugins, configure and manage individually."
notificationsBanner: "You can configure the types and range of notifications from the server and push notifications."
api: "API"
webhook: "Webhook"
serviceConnection: "Service integration"
serviceConnectionBanner: "Manage and configure access tokens and Webhooks to integrate with external apps or services."
accountData: "Account data"
accountDataBanner: "Export and import to manage account data."
muteAndBlockBanner: "You can configure and manage settings to hide content and restrict actions from specific users."
accessibilityBanner: "You can personalize the client's visuals and behavior, and configure settings to optimize usage."
privacyBanner: "You can configure settings related to account privacy, such as content visibility, discoverability, and follow approval."
securityBanner: "You can configure settings related to account security, such as password, login methods, authentication apps, and Passkeys."
preferencesBanner: "You can configure the overall behavior of the client according to your preferences."
appearanceBanner: "You can configure the appearance and display settings for the client according to your preferences."
soundsBanner: "You can configure the sound settings for playback in the client."
timelineAndNote: "Timeline and note"
makeEveryTextElementsSelectable: "Make all text elements selectable"
makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations."
_preferencesProfile:
profileName: "Profile name"
profileNameDescription: "Set a name that identifies this device."
profileNameDescription2: "Example: \"Main PC\", \"Smartphone\""
_preferencesBackup:
autoBackup: "Auto backup"
restoreFromBackup: "Restore from backup"
noBackupsFoundTitle: "No backups found"
noBackupsFoundDescription: "No auto-created backups were found, but if you have manually saved a backup file, you can import and restore it."
selectBackupToRestore: "Select a backup to restore"
youNeedToNameYourProfileToEnableAutoBackup: "A profile name must be set to enable auto backup."
autoPreferencesBackupIsNotEnabledForThisDevice: "Settings auto backup is not enabled on this device."
backupFound: "Settings backup is found"
_accountSettings:
requireSigninToViewContents: "Require sign-in to view contents"
requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
@ -1319,6 +1383,7 @@ _accountSettings:
makeNotesHiddenBefore: "Make past notes private"
makeNotesHiddenBeforeDescription: "While this feature is enabled, notes that are past the set date and time or have been visible only to you. When it is deactivated, the note publication status will also be restored."
mayNotEffectForFederatedNotes: "Notes federated to a remote server may not be affected."
mayNotEffectSomeSituations: "These restrictions are simplified. They may not apply in some situations, such as when viewing on a remote server or during moderation."
notesHavePassedSpecifiedPeriod: "Note that the specified time has passed"
notesOlderThanSpecifiedDateAndTime: "Notes before the specified date and time"
_abuseUserReport:
@ -1967,6 +2032,7 @@ _theme:
installed: "{name} has been installed"
installedThemes: "Installed themes"
builtinThemes: "Built-in themes"
instanceTheme: "Server theme"
alreadyInstalled: "This theme is already installed"
invalid: "The format of this theme is invalid"
make: "Make a theme"
@ -2440,6 +2506,8 @@ _notification:
flushNotification: "Clear notifications"
exportOfXCompleted: "Export of {x} has been completed"
login: "Someone logged in"
createToken: "An access token has been created"
createTokenDescription: "If you have no idea, delete the access token through \"{text}\"."
_types:
all: "All"
note: "New notes"
@ -2456,6 +2524,7 @@ _notification:
achievementEarned: "Achievement unlocked"
exportCompleted: "The export has been completed"
login: "Sign In"
createToken: "Create access token"
test: "Notification test"
app: "Notifications from linked apps"
_actions:
@ -2483,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
flexible: "Auto-adjust width"
enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices"
_columns:
main: "Main"
widgets: "Widgets"
@ -2590,6 +2660,7 @@ _moderationLogTypes:
deletePage: "Page deleted"
deleteFlash: "Play deleted"
deleteGalleryPost: "Gallery post deleted"
updateProxyAccountDescription: "Update the description of the proxy account"
_fileViewer:
title: "File details"
type: "File type"
@ -2603,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation."
_plugin:
title: "Do you want to install this plugin?"
metaTitle: "Plugin information"
_theme:
title: "Do you want to install this theme?"
metaTitle: "Theme information"
_meta:
base: "Base color scheme"
_vendorInfo:
@ -2645,7 +2714,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."
@ -2737,7 +2806,7 @@ _roleSelectDialog:
_customEmojisManager:
_gridCommon:
copySelectionRows: "Copy selected rows"
copySelectionRanges: "Copy selected ranges"
copySelectionRanges: "Copy selection"
deleteSelectionRows: "Delete selected rows"
deleteSelectionRanges: "Delete rows in the selection"
searchSettings: "Search settings"
@ -2807,8 +2876,8 @@ _selfXssPrevention:
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
description3: "For more information, please refer to this. {link}"
_followRequest:
recieved: "Received"
sent: "Sent"
recieved: "Received request"
sent: "Sent request"
_remoteLookupErrors:
_federationNotAllowed:
title: "Unable to communicate with this server"
@ -2822,8 +2891,6 @@ _remoteLookupErrors:
_responseInvalid:
title: "Response is invalid"
description: "It could communicate with this server, but the data obtained was incorrect."
_responseInvalidIdHostNotMatch:
description: "The domain of the entered URI differs from the domain of the final obtained URI. If you are looking up remote content through a third-party server, please look up again using a URI that can be obtained from the origin server."
_noSuchObject:
title: "Not found"
description: "The requested resource was not found, please recheck the URI."
@ -2840,3 +2907,23 @@ _captcha:
_unknown:
title: "CAPTCHA error"
text: "An unexpected error occurred."
_bootErrors:
title: "Failed to load"
serverError: "If the problem persists after waiting a moment and reloading, please contact the server administrator with the following Error ID."
solution: "The following may solve the problem."
solution1: "Update your browser and OS to the latest version"
solution2: "Disable ad blocker"
solution3: "Clear the browser cache"
solution4: "Set the dom.webaudio.enabled to true for Tor Browser"
otherOption: "Other options"
otherOption1: "Delete client settings and cache"
otherOption2: "Start the simple client"
otherOption3: "Launch the repair tool"
_search:
searchScopeAll: "All"
searchScopeLocal: "Local"
searchScopeServer: "Specific server"
searchScopeUser: "Specific user"
pleaseEnterServerHost: "Enter the server host"
pleaseSelectUser: "Select user"
serverHostPlaceholder: "Example: misskey.example.com"

View file

@ -605,6 +605,7 @@ descendingOrder: "Descendente"
scratchpad: "Scratch pad"
scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript. Puede escribir, ejecutar y verificar los resultados que interactúan con Misskey."
uiInspector: "Inspector de UI"
uiInspectorDescription: "Puedes visualizar una lista de elementos UI presentes en la memoria. Los componentes de la interfaz de usuario son generados por las funciones UI:C:"
output: "Salida"
script: "Script"
disablePagesScript: "Deshabilitar AiScript en Páginas"
@ -693,6 +694,7 @@ regexpError: "Error de la expresión regular"
regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}"
instanceMute: "Instancias silenciadas"
userSaysSomething: "{name} dijo algo"
userSaysSomethingAbout: "{name} dijo algo sobre {word}"
makeActive: "Activar"
display: "Apariencia"
copy: "Copiar"
@ -861,6 +863,7 @@ administration: "Administrar"
accounts: "Cuentas"
switch: "Cambiar"
noMaintainerInformationWarning: "No se ha establecido la información del administrador"
noInquiryUrlWarning: "No se ha guardado la URL de consulta."
noBotProtectionWarning: "La protección contra los bots no está configurada"
configure: "Configurar"
postToGallery: "Crear una nueva publicación en la galería"
@ -925,6 +928,7 @@ followersVisibility: "Visibilidad de seguidores"
continueThread: "Ver la continuación del hilo"
deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?"
incorrectPassword: "La contraseña es incorrecta"
incorrectTotp: "La contraseña de un solo uso es incorrecta o ha caducado."
voteConfirm: "¿Confirma su voto a {choice}?"
hide: "Ocultar"
useDrawerReactionPickerForMobile: "Mostrar panel de reacciones en móviles"
@ -1053,6 +1057,7 @@ thisPostMayBeAnnoyingHome: "Publicar en línea de tiempo 'Inicio'"
thisPostMayBeAnnoyingCancel: "detener"
thisPostMayBeAnnoyingIgnore: "Publicar de todos modos"
collapseRenotes: "Colapsar renotas que ya hayas visto"
collapseRenotesDescription: "Contrae notas a las que ya has reaccionado o renotado "
internalServerError: "Error interno del servidor"
internalServerErrorDescription: "El servidor tuvo un error inesperado."
copyErrorInfo: "Copiar detalles del error"
@ -1091,6 +1096,7 @@ retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?"
retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente "
enableChartsForRemoteUser: "Generar gráficas de usuarios remotos."
enableChartsForFederatedInstances: "Generar gráficos de servidores remotos"
enableStatsForFederatedInstances: "Activar las estadísticas de las instancias remotas federadas"
showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas"
reactionsDisplaySize: "Tamaño de las reacciones"
limitWidthOfReaction: "Limitar ancho de las reacciones"
@ -1139,6 +1145,7 @@ preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los text
options: "Opción"
specifyUser: "Especificar usuario"
lookupConfirm: "¿Quiere informarse?"
openTagPageConfirm: "¿Quieres abrir la página de etiquetas?"
specifyHost: "Especificar Host"
failedToPreviewUrl: "No se pudo generar la vista previa"
update: "Actualizar"
@ -1267,15 +1274,35 @@ useBackupCode: "Usar códigos de respaldo"
launchApp: "Ejecutar la app"
useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reproduce audio y vídeo"
keepOriginalFilename: "Mantener el nombre original del archivo"
keepOriginalFilenameDescription: "Si desactivas esta opción, los nombres de los archivos serán remplazados por una cadena de caracteres aleatoria cuando subas los archivos."
noDescription: "No hay descripción"
alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien"
inquiry: "Contacto"
tryAgain: "Por favor , inténtalo de nuevo"
confirmWhenRevealingSensitiveMedia: "Confirmación cuando se revele contenido sensible"
sensitiveMediaRevealConfirm: "Esto puede contener contenido sensible. ¿Estás seguro/a de querer mostrarlo?"
createdLists: "Listas creadas"
createdAntennas: "Antenas creadas"
fromX: "De {x}"
genEmbedCode: "Obtener el código para incrustar"
noteOfThisUser: "Notas de este usuario"
clipNoteLimitExceeded: "No se pueden añadir más notas a este clip."
performance: "Rendimiento"
modified: "Modificado"
discard: "Descartar"
thereAreNChanges: "Hay {n} cambio(s)"
signinWithPasskey: "Iniciar sesión con clave de acceso"
unknownWebAuthnKey: "Esto no se ha registrado llave maestra."
passkeyVerificationFailed: "La verificación de la clave de acceso ha fallado."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificación de la clave de acceso ha sido satisfactoria pero se ha deshabilitado el inicio de sesión sin contraseña."
messageToFollower: "Mensaje a seguidores"
target: "Para"
federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
postForm: "Formulario"
information: "Información"
_settings:
webhook: "Webhook"
_accountSettings:
requireSigninToViewContents: "Se requiere iniciar sesión para ver el contenido"
requireSigninToViewContentsDescription1: "Requiere iniciar sesión para ver todas las notas y otros contenidos que hayas creado. Se espera que esto evite que los rastreadores recopilen información."
@ -2495,10 +2522,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación."
_plugin:
title: "¿Quieres instalar este plugin?"
metaTitle: "Información del plugin"
_theme:
title: "¿Quieres instalar este tema?"
metaTitle: "Información del tema"
_meta:
base: "Esquema de color base"
_vendorInfo:
@ -2560,6 +2585,13 @@ _mediaControls:
pip: "Picture in Picture"
playbackRate: "Velocidad de reproducción"
loop: "Reproducción en bucle"
_followRequest:
recieved: "Petición de seguimiento recibida"
sent: "Petición de seguimiento enviada"
_remoteLookupErrors:
_noSuchObject:
title: "No se encuentra"
_search:
searchScopeAll: "Todo"
searchScopeLocal: "Local"
searchScopeUser: "Especificar usuario"

View file

@ -1277,6 +1277,8 @@ prohibitedWordsForNameOfUser: "Mots interdits pour les noms d'utilisateur·rices
lockdown: "Verrouiller"
pleaseSelectAccount: "Sélectionner un compte"
availableRoles: "Rôles disponibles"
postForm: "Formulaire de publication"
information: "Informations"
_abuseUserReport:
forward: "Transférer"
forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme."
@ -2294,10 +2296,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation."
_plugin:
title: "Voulez-vous installer cette extension ?"
metaTitle: "Informations sur l'extension"
_theme:
title: "Voulez-vous installer ce thème ?"
metaTitle: "Informations sur le thème"
_meta:
base: "Palette de couleurs de base"
_vendorInfo:
@ -2364,3 +2364,7 @@ _embedCodeGen:
_remoteLookupErrors:
_noSuchObject:
title: "Non trouvé"
_search:
searchScopeAll: "Tous"
searchScopeLocal: "Local"
searchScopeUser: "Spécifier l'utilisateur·rice"

View file

@ -1261,6 +1261,10 @@ performance: "Kinerja"
modified: "Diubah"
thereAreNChanges: "Ada {n} perubahan"
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
postForm: "Buat catatan"
information: "Informasi"
_settings:
webhook: "Webhook"
_abuseUserReport:
accept: "Setuju"
reject: "Tolak"
@ -2489,10 +2493,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan."
_plugin:
title: "Apakah kamu ingin memasang plugin ini?"
metaTitle: "Informasi plugin"
_theme:
title: "Apakah kamu ingin memasang tema ini?"
metaTitle: "Informasi tema"
_meta:
base: "Skema warna dasar"
_vendorInfo:
@ -2610,3 +2612,7 @@ _mediaControls:
_remoteLookupErrors:
_noSuchObject:
title: "Tidak dapat ditemukan"
_search:
searchScopeAll: "Semua"
searchScopeLocal: "Lokal"
searchScopeUser: "Pengguna spesifik"

575
locales/index.d.ts vendored
View file

@ -1174,10 +1174,6 @@ export interface Locale extends ILocale {
*
*/
"saved": string;
/**
*
*/
"messaging": string;
/**
*
*/
@ -1227,9 +1223,9 @@ export interface Locale extends ILocale {
*/
"noMoreHistory": string;
/**
*
*
*/
"startMessaging": string;
"startChat": string;
/**
* {n}
*/
@ -1982,14 +1978,6 @@ export interface Locale extends ILocale {
* The text in clipboard is long. Would you like to attach it as a text file?
*/
"attachAsFileQuestion": string;
/**
*
*/
"noMessagesYet": string;
/**
*
*/
"newMessageExists": string;
/**
*
*/
@ -2810,6 +2798,10 @@ export interface Locale extends ILocale {
*
*/
"copy": string;
/**
*
*/
"copiedToClipboard": string;
/**
*
*/
@ -4972,7 +4964,7 @@ export interface Locale extends ILocale {
*/
"disableStreamingTimeline": string;
/**
*
*
*/
"useGroupedNotifications": string;
/**
@ -5255,6 +5247,415 @@ export interface Locale extends ILocale {
*
*/
"federationDisabled": string;
/**
*
*/
"confirmOnReact": string;
/**
* " {emoji} "
*/
"reactAreYouSure": ParameterizedString<"emoji">;
/**
*
*/
"markAsSensitiveConfirm": string;
/**
*
*/
"unmarkAsSensitiveConfirm": string;
/**
*
*/
"preferences": string;
/**
*
*/
"accessibility": string;
/**
*
*/
"preferencesProfile": string;
/**
* IDをコピー
*/
"copyPreferenceId": string;
/**
*
*/
"resetToDefaultValue": string;
/**
*
*/
"overrideByAccount": string;
/**
*
*/
"untitled": string;
/**
*
*/
"noName": string;
/**
*
*/
"skip": string;
/**
*
*/
"restore": string;
/**
*
*/
"syncBetweenDevices": string;
/**
*
*/
"preferenceSyncConflictTitle": string;
/**
*
*/
"preferenceSyncConflictText": string;
/**
*
*/
"preferenceSyncConflictChoiceServer": string;
/**
*
*/
"preferenceSyncConflictChoiceDevice": string;
/**
*
*/
"preferenceSyncConflictChoiceCancel": string;
/**
*
*/
"paste": string;
/**
*
*/
"emojiPalette": string;
/**
* 稿
*/
"postForm": string;
/**
*
*/
"textCount": string;
/**
*
*/
"information": string;
/**
*
*/
"chat": string;
/**
*
*/
"migrateOldSettings": string;
/**
*
*/
"migrateOldSettings_description": string;
"_chat": {
/**
*
*/
"noMessagesYet": string;
/**
*
*/
"newMessage": string;
/**
*
*/
"individualChat": string;
/**
*
*/
"individualChat_description": string;
/**
*
*/
"roomChat": string;
/**
*
*
*/
"roomChat_description": string;
/**
*
*/
"createRoom": string;
/**
*
*/
"inviteUserToChat": string;
/**
*
*/
"yourRooms": string;
/**
*
*/
"joiningRooms": string;
/**
*
*/
"invitations": string;
/**
*
*/
"noInvitations": string;
/**
*
*/
"history": string;
/**
*
*/
"noHistory": string;
/**
*
*/
"noRooms": string;
/**
*
*/
"inviteUser": string;
/**
*
*/
"join": string;
/**
*
*/
"ignore": string;
/**
* 退
*/
"leave": string;
/**
*
*/
"members": string;
/**
*
*/
"searchMessages": string;
/**
*
*/
"home": string;
/**
*
*/
"muteThisRoom": string;
/**
*
*/
"cannotChatWithTheUser": string;
/**
* 使
*/
"cannotChatWithTheUser_description": string;
/**
*
*/
"chatWithThisUser": string;
/**
*
*/
"thisUserAllowsChatOnlyFromFollowers": string;
/**
*
*/
"thisUserAllowsChatOnlyFromFollowing": string;
/**
*
*/
"thisUserAllowsChatOnlyFromMutualFollowing": string;
/**
*
*/
"thisUserNotAllowedChatAnyone": string;
/**
*
*/
"chatAllowedUsers": string;
/**
*
*/
"chatAllowedUsers_note": string;
"_chatAllowedUsers": {
/**
*
*/
"everyone": string;
/**
*
*/
"followers": string;
/**
*
*/
"following": string;
/**
*
*/
"mutual": string;
/**
*
*/
"none": string;
};
};
"_emojiPalette": {
/**
*
*/
"palettes": string;
/**
*
*/
"enableSyncBetweenDevicesForPalettes": string;
/**
* 使
*/
"paletteForMain": string;
/**
* 使
*/
"paletteForReaction": string;
};
"_settings": {
/**
* 使
*/
"driveBanner": string;
/**
*
*/
"pluginBanner": string;
/**
*
*/
"notificationsBanner": string;
/**
* API
*/
"api": string;
/**
* Webhook
*/
"webhook": string;
/**
*
*/
"serviceConnection": string;
/**
* Webhookの管理と設定が行えます
*/
"serviceConnectionBanner": string;
/**
*
*/
"accountData": string;
/**
* /
*/
"accountDataBanner": string;
/**
*
*/
"muteAndBlockBanner": string;
/**
* 使
*/
"accessibilityBanner": string;
/**
*
*/
"privacyBanner": string;
/**
*
*/
"securityBanner": string;
/**
*
*/
"preferencesBanner": string;
/**
*
*/
"appearanceBanner": string;
/**
*
*/
"soundsBanner": string;
/**
*
*/
"timelineAndNote": string;
/**
*
*/
"makeEveryTextElementsSelectable": string;
/**
*
*/
"makeEveryTextElementsSelectable_description": string;
};
"_preferencesProfile": {
/**
*
*/
"profileName": string;
/**
*
*/
"profileNameDescription": string;
/**
* : PC
*/
"profileNameDescription2": string;
};
"_preferencesBackup": {
/**
*
*/
"autoBackup": string;
/**
*
*/
"restoreFromBackup": string;
/**
*
*/
"noBackupsFoundTitle": string;
/**
*
*/
"noBackupsFoundDescription": string;
/**
*
*/
"selectBackupToRestore": string;
/**
*
*/
"youNeedToNameYourProfileToEnableAutoBackup": string;
/**
*
*/
"autoPreferencesBackupIsNotEnabledForThisDevice": string;
/**
*
*/
"backupFound": string;
};
"_accountSettings": {
/**
*
@ -5292,6 +5693,10 @@ export interface Locale extends ILocale {
*
*/
"mayNotEffectForFederatedNotes": string;
/**
*
*/
"mayNotEffectSomeSituations": string;
/**
*
*/
@ -7057,6 +7462,10 @@ export interface Locale extends ILocale {
*
*/
"canImportUserLists": string;
/**
*
*/
"canChat": string;
/**
* Can view the bubble timeline
*/
@ -7706,6 +8115,10 @@ export interface Locale extends ILocale {
*
*/
"builtinThemes": string;
/**
*
*/
"instanceTheme": string;
/**
*
*/
@ -7966,6 +8379,10 @@ export interface Locale extends ILocale {
*
*/
"reaction": string;
/**
*
*/
"chatMessage": string;
};
"_soundSettings": {
/**
@ -8538,6 +8955,14 @@ export interface Locale extends ILocale {
*
*/
"write:report-abuse": string;
/**
*
*/
"write:chat": string;
/**
*
*/
"read:chat": string;
/**
* Approve new users
*/
@ -8551,7 +8976,7 @@ export interface Locale extends ILocale {
*/
"write:admin:nsfw-user": string;
/**
* Mark users an not NSFW
* Mark users as not NSFW
*/
"write:admin:unnsfw-user": string;
/**
@ -9628,6 +10053,14 @@ export interface Locale extends ILocale {
*
*/
"login": string;
/**
*
*/
"createToken": string;
/**
* {text}
*/
"createTokenDescription": ParameterizedString<"text">;
"_types": {
/**
*
@ -9689,6 +10122,10 @@ export interface Locale extends ILocale {
*
*/
"login": string;
/**
*
*/
"createToken": string;
/**
*
*/
@ -9818,6 +10255,10 @@ export interface Locale extends ILocale {
* 調
*/
"flexible": string;
/**
*
*/
"enableSyncBetweenDevicesForProfiles": string;
"_columns": {
/**
*
@ -10226,6 +10667,10 @@ export interface Locale extends ILocale {
* 稿
*/
"deleteGalleryPost": string;
/**
*
*/
"updateProxyAccountDescription": string;
/**
* Approved
*/
@ -10243,7 +10688,7 @@ export interface Locale extends ILocale {
*/
"setRemoteInstanceNSFW": string;
/**
* Set remote instance as NSFW
* Unset remote instance as NSFW
*/
"unsetRemoteInstanceNSFW": string;
/**
@ -10367,20 +10812,12 @@ export interface Locale extends ILocale {
*
*/
"title": string;
/**
*
*/
"metaTitle": string;
};
"_theme": {
/**
*
*/
"title": string;
/**
*
*/
"metaTitle": string;
};
"_meta": {
/**
@ -11114,11 +11551,11 @@ export interface Locale extends ILocale {
};
"_followRequest": {
/**
*
* Received
*/
"recieved": string;
/**
*
* Sent
*/
"sent": string;
};
@ -11160,13 +11597,7 @@ export interface Locale extends ILocale {
*/
"title": string;
/**
*
*/
"description": string;
};
"_responseInvalidIdHostNotMatch": {
/**
* URIのドメインと最終的に得られたURIのドメインとが異なりますURIを使用して照会し直してください
* URIを使用して照会し直してください
*/
"description": string;
};
@ -11224,6 +11655,82 @@ export interface Locale extends ILocale {
};
};
};
"_bootErrors": {
/**
*
*/
"title": string;
/**
* Error IDを添えてサーバー管理者に連絡してください
*/
"serverError": string;
/**
*
*/
"solution": string;
/**
* OSを最新バージョンに更新する
*/
"solution1": string;
/**
*
*/
"solution2": string;
/**
*
*/
"solution3": string;
/**
* (Tor Browser) dom.webaudio.enabledをtrueに設定する
*/
"solution4": string;
/**
*
*/
"otherOption": string;
/**
*
*/
"otherOption1": string;
/**
*
*/
"otherOption2": string;
/**
*
*/
"otherOption3": string;
};
"_search": {
/**
*
*/
"searchScopeAll": string;
/**
*
*/
"searchScopeLocal": string;
/**
*
*/
"searchScopeServer": string;
/**
*
*/
"searchScopeUser": string;
/**
*
*/
"pleaseEnterServerHost": string;
/**
*
*/
"pleaseSelectUser": string;
/**
* : misskey.example.com
*/
"serverHostPlaceholder": string;
};
/**
* Approvals
*/

View file

@ -126,7 +126,7 @@ pinnedNote: "Nota in primo piano"
pinned: "Fissa sul profilo"
you: "Tu"
clickToShow: "Contenuto occultato, cliccare solo se si intende vedere"
sensitive: "Allegato esplicito"
sensitive: "Esplicito"
add: "Aggiungi"
reaction: "Reazioni"
reactions: "Reazioni"
@ -228,7 +228,7 @@ jobQueue: "Coda di lavoro"
cpuAndMemory: "CPU e Memoria"
network: "Rete"
disk: "Disco"
instanceInfo: "Informazioni sull'istanza"
instanceInfo: "Informazioni sul server"
statistics: "Statistiche"
clearQueue: "Svuota coda"
clearQueueConfirmTitle: "Vuoi davvero svuotare la coda?"
@ -445,7 +445,7 @@ exploreFediverse: "Esplora il Fediverso"
popularTags: "Hashtag popolari"
userList: "Liste"
about: "Informazioni"
aboutMisskey: "Informazioni di Misskey"
aboutMisskey: "A proposito di Misskey"
administrator: "Amministratore"
token: "Token"
2fa: "Autenticazione a due fattori"
@ -606,7 +606,7 @@ scratchpad: "ScratchPad"
scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey."
uiInspector: "UI Inspector"
uiInspectorDescription: "Puoi visualizzare un elenco di elementi UI presenti in memoria. I componenti dell'interfaccia utente vengono generati dalle funzioni Ui:C:."
output: "Uscita"
output: "Output"
script: "Script"
disablePagesScript: "Disabilita AiScript nelle pagine"
updateRemoteUser: "Aggiorna dati dal profilo remoto"
@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\""
makeActive: "Attiva"
display: "Visualizza"
copy: "Copia"
copiedToClipboard: "Copiato negli appunti"
metrics: "Statistiche"
overview: "Anteprima"
logs: "Log"
@ -766,7 +767,7 @@ driveUsage: "Utilizzazione del Drive"
noCrawle: "Rifiuta l'indicizzazione dai robot."
noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc."
lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow."
alwaysMarkSensitive: "Segnare gli allegati come espliciti come opzione predefinita"
alwaysMarkSensitive: "Segnare automaticamente come espliciti gli allegati"
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
disableShowingAnimatedImages: "Disabilita le immagini animate"
highlightSensitiveMedia: "Evidenzia i media espliciti"
@ -893,7 +894,7 @@ searchResult: "Risultati della Ricerca"
hashtags: "Hashtag"
troubleshooting: "Risoluzione problemi"
useBlurEffect: "Utilizza effetto sfocatura"
learnMore: "Più dettagli"
learnMore: "Per saperne di più"
misskeyUpdated: "Misskey è stato aggiornato!"
whatIsNew: "Informazioni sull'aggiornamento"
translate: "Traduci"
@ -901,7 +902,7 @@ translatedFrom: "Traduzione da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
aiChanMode: "Modalità Ai"
devMode: "Modalità sviluppatori"
devMode: "Modalità sviluppo"
keepCw: "Mostra i contenuti espliciti"
pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
@ -973,7 +974,7 @@ check: "Verifica"
driveCapOverrideLabel: "Modificare la capienza del Drive per questo profilo"
driveCapOverrideCaption: "Se viene specificato meno di 0, viene annullato."
requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso con un profilo amministratore."
isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema"
isSystemAccount: "Si tratta di un profilo creato e gestito automaticamente dal sistema."
typeToConfirm: "Digita {x} per continuare"
deleteAccount: "Eliminazione profilo"
document: "Documentazione"
@ -1049,7 +1050,7 @@ permissionDeniedError: "Errore, attività non autorizzata"
permissionDeniedErrorDescription: "Non si dispone dell'autorizzazione per eseguire questa operazione."
preset: "Preimpostato"
selectFromPresets: "Seleziona preimpostato"
achievements: "Obiettivi raggiunti"
achievements: "Conquiste"
gotInvalidResponseError: "Risposta del server non valida"
gotInvalidResponseErrorDescription: "Il server potrebbe essere irraggiungibile o in manutenzione. Riprova più tardi."
thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva"
@ -1090,7 +1091,7 @@ notesSearchNotAvailable: "Non è possibile cercare tra le Note."
license: "Licenza"
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
myClips: "Le mie Clip"
drivecleaner: "Drive cleaner"
drivecleaner: "Pulizia del Drive"
retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
@ -1309,6 +1310,69 @@ availableRoles: "Ruoli disponibili"
acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento."
federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione."
federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server."
confirmOnReact: "Confermare le reazioni"
reactAreYouSure: "Vuoi davvero reagire con {emoji} ?"
markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?"
unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?"
preferences: "Preferenze"
accessibility: "Accessibilità"
preferencesProfile: "Profilo preferenze"
copyPreferenceId: "Copia ID preferenze"
resetToDefaultValue: "Ripristina a predefinito"
overrideByAccount: "Sovrascrivere col profilo"
untitled: "Senza titolo"
noName: "Senza nome"
skip: "Salta"
restore: "Ripristina"
syncBetweenDevices: "Sincronizzazione tra i dispositivi"
preferenceSyncConflictTitle: "Sul server esiste già il valore impostato"
preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?"
preferenceSyncConflictChoiceServer: "Valore del server"
preferenceSyncConflictChoiceDevice: "Valore del dispositivo"
preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione"
paste: "Incolla"
emojiPalette: "Tavolozza emoji"
postForm: "Finestra di pubblicazione"
textCount: "Il numero di caratteri"
information: "Informazioni"
_emojiPalette:
palettes: "Tavolozza"
enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi"
paletteForMain: "Tavolozza principale"
paletteForReaction: "Tavolozza per reazioni"
_settings:
driveBanner: "Permette di gestire e configurare il Drive, controllare il consumo di spazio e configurare il caricamento dei file."
pluginBanner: "Consentono di migliorare le funzionalità. Le estensioni si possono configurare e gestire singolarmente."
notificationsBanner: "Puoi impostare il tipo di notifiche da ricevere dal server e anche le notifiche push."
api: "API"
webhook: "Webhook"
serviceConnection: "Integrazione servizi"
serviceConnectionBanner: "Puoi gestire i codici di accesso e i Webhook per collegare App o servizi esterni."
accountData: "Dati del profilo"
accountDataBanner: "Puoi gestire i dati del tuo profilo, esportando e importando."
muteAndBlockBanner: "Puoi configurare la visibiltà dei contenuti e limitare le attività provenienti da profili specifici."
accessibilityBanner: "Puoi personalizzare e migliorare la lettura sul tuo dispositivo in modo che sia più chiaro e reattivo."
privacyBanner: "Puoi configurare la privacy del tuo profilo, come la visibilità delle Note, la visibilità del profilo nelle ricerche e l'approvazione delle relazioni tra profili."
securityBanner: "Puoi gestire la sicurezza del tuo account, la password, i modi di accesso, la generazione di codici OTP per accesso multi fattore (MFA/2FA) e la passkey."
preferencesBanner: "Puoi personalizzare il comportamento del tuo dispositivo."
appearanceBanner: "Puoi personalizzare l'aspetto nel dispositivo, in base alle tue preferenze."
soundsBanner: "Puoi personalizzare i suoni emessi dagli eventi sul tuo dispositivo."
timelineAndNote: "Note e Timeline"
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
_preferencesProfile:
profileName: "Nome del profilo"
profileNameDescription: "Impostare il nome che indentifica questo dispositivo."
profileNameDescription2: "Es: \"PC principale\" o \"Cellulare\""
_preferencesBackup:
autoBackup: "Backup automatico"
restoreFromBackup: "Ripristinare da backup"
noBackupsFoundTitle: "Nessun backup trovato"
noBackupsFoundDescription: "Impossibile trovare un backup creato automaticamente. Se se hai salvato il file di backup manualmente, puoi importarlo e ripristinarlo."
selectBackupToRestore: "Seleziona un backup da ripristinare"
youNeedToNameYourProfileToEnableAutoBackup: "Per abilitare i backup automatici, è necessario indicare il nome del profilo."
autoPreferencesBackupIsNotEnabledForThisDevice: "Su questo dispositivo non è stato attivato il backup automatico delle preferenze."
backupFound: "Esiste il Backup delle preferenze"
_accountSettings:
requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione"
requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler."
@ -1319,6 +1383,7 @@ _accountSettings:
makeNotesHiddenBefore: "Nascondi le Note pubblicate in precedenza"
makeNotesHiddenBeforeDescription: "Mentre questa funzione è abilitata, le Note antecedenti al momento impostato, saranno visibili soltanto a te (private). Disabilitandola nuovamente, verrà ripristinata anche la visibilità pubblica della Nota."
mayNotEffectForFederatedNotes: "Le Note già federate su server remoti potrebbero non essere modificate."
mayNotEffectSomeSituations: "Queste restrizioni sono semplificate. In alcuni casi, potrebbero anche non avvenire. Ad esempio visionando un server remoto o durante la moderazione."
notesHavePassedSpecifiedPeriod: "Note antecedenti al periodo specificato"
notesOlderThanSpecifiedDateAndTime: "Note antecedenti al momento specificato"
_abuseUserReport:
@ -1445,9 +1510,9 @@ _initialTutorial:
description: "Queste sono solamente alcune delle funzionalità principali di Misskey. Per ulteriori informazioni, {link}."
_timelineDescription:
home: "Nella Timeline Home, la tua cronologia principale, puoi vedere le Note provenienti dai profili che segui (Following)."
local: "La Timeline Locale, è una cronologia di Note pubblicate da tutti i profili iscritti su questo server."
social: "La Timeline Sociale, unisce in ordine cronologico l'elenco di Note presenti nella Timeline Home e quella Locale."
global: "La Timeline Federata ti consente di vedere le Note pubblicate dai profili di tutti gli altri server federati a questo."
local: "La Timeline Locale è un flusso di Note pubblicate dai profili iscritti a questo server."
social: "La Timeline Sociale elenca, in ordine cronologico, il flusso di Note nella Timeline Home e Locale."
global: "Nella Timeline Federata trovi il flusso di Note provenienti da profili iscritti ad altri server, federati a questo."
_serverRules:
description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio."
_serverSettings:
@ -1471,8 +1536,8 @@ _serverSettings:
_accountMigration:
moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto"
moveFromLabel: "Profilo da cui migrare #{n}"
moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
moveFromLabel: "Profilo da cui migrare n. {n}"
moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@vecchia.istanza.it"
moveTo: "Migrare questo profilo verso un un altro"
moveToLabel: "Profilo verso cui migrare"
moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata."
@ -1548,13 +1613,13 @@ _achievements:
title: "Principiante III"
description: "Hai totalizzato 15 accessi!"
_login30:
title: "Misskist I"
title: "Missalcolista I"
description: "Hai totalizzato 30 accessi!"
_login60:
title: "Misskeist II"
title: "Missalcolista II"
description: "Hai totalizzato 60 accessi!"
_login100:
title: "Misskeist III"
title: "Missalcolista III"
description: "Hai totalizzato 100 accessi!"
flavor: "Violent Misskeist"
_login200:
@ -1640,10 +1705,10 @@ _achievements:
description: "Hai superato i 1.000 profili Follower"
_collectAchievements30:
title: "Collezionista di successi"
description: "Hai raggiunto 30 obiettivi"
description: "Hai raggiunto 30 conquiste"
_viewAchievements3min:
title: "Mi piacciono i risultati"
description: "Guarda la tua collezione di obiettivi per almeno 3 minuti"
description: "Ammira la tua collezione di conquiste per almeno 3 minuti"
_iLoveMisskey:
title: "I LOVE Misskey"
description: "Pubblica «I ♥ #Misskey»"
@ -1908,7 +1973,7 @@ _registry:
domain: "Dominio"
createKey: "Crea chiave"
_aboutMisskey:
about: "Misskey è un software libero e open source, sviluppato da syuilo dal 2014."
about: "Misskey è software libero, open source, sviluppato da Syuilo fin dal lontano 2014."
contributors: "Principali sostenitori"
allContributors: "Tutti i sostenitori"
source: "Codice sorgente"
@ -1967,6 +2032,7 @@ _theme:
installed: "{name} è installato"
installedThemes: "Temi installati"
builtinThemes: "Temi integrati"
instanceTheme: "Tema dell'istanza"
alreadyInstalled: "Questo tema è già installato"
invalid: "Il formato tema non è valido"
make: "Crea un tema"
@ -2235,7 +2301,7 @@ _widgets:
userList: "Elenco utenti"
_userList:
chooseList: "Seleziona una lista"
clicker: "Cliccaggio"
clicker: "Cliccheria"
birthdayFollowings: "Compleanni del giorno"
_cw:
hide: "Nascondere"
@ -2298,7 +2364,7 @@ _profile:
metadataContent: "Contenuto"
changeAvatar: "Modifica immagine profilo"
changeBanner: "Cambia intestazione"
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo."
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo.\nPer verificare il profilo tramite la spunta di conferma, devi inserire la url alla pagina che contiene un link al tuo profilo Misskey. Deve avere attributo rel='me'."
avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni."
followedMessage: "Messaggio, quando qualcuno ti segue"
followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono."
@ -2440,6 +2506,8 @@ _notification:
flushNotification: "Azzera le notifiche"
exportOfXCompleted: "Abbiamo completato l'esportazione di {x}"
login: "Autenticazione avvenuta"
createToken: "È stato creato un token di accesso"
createTokenDescription: "In caso contrario, eliminare il token di accesso tramite ({text})."
_types:
all: "Tutto"
note: "Nuove Note"
@ -2456,6 +2524,7 @@ _notification:
achievementEarned: "Risultato raggiunto"
exportCompleted: "Esportazione completata"
login: "Accessi"
createToken: "Creare un token di accesso"
test: "Notifiche di test"
app: "Notifiche da applicazioni"
_actions:
@ -2483,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice"
usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima"
flexible: "Larghezza flessibile"
enableSyncBetweenDevicesForProfiles: "Abilita la sincronizzazione delle informazioni profilo tra dispositivi"
_columns:
main: "Principale"
widgets: "Riquadri"
@ -2501,8 +2571,8 @@ _disabledTimeline:
title: "Timeline disabilitata"
description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
_drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente"
orderBySizeDesc: "Dal file più grosso al più piccolo"
orderByCreatedAtAsc: "Dal file più vecchio al più recente"
_webhookSettings:
createWebhook: "Creazione Webhook"
modifyWebhook: "Modifica Webhook"
@ -2590,6 +2660,7 @@ _moderationLogTypes:
deletePage: "Pagina eliminata"
deleteFlash: "Play eliminato"
deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy"
_fileViewer:
title: "Dettagli del file"
type: "Tipo di file"
@ -2603,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile."
_plugin:
title: "Vuoi davvero installare questo componente aggiuntivo?"
metaTitle: "Informazioni sul componente aggiuntivo"
_theme:
title: "Vuoi davvero installare questa variazione grafica?"
metaTitle: "Informazioni sulla variazione grafica"
_meta:
base: "Combinazione base di colori"
_vendorInfo:
@ -2807,8 +2876,8 @@ _selfXssPrevention:
description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra."
description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}"
_followRequest:
recieved: "Ricezione richiesta di Follow"
sent: "Richiesta di Follow, inviata"
recieved: "Richieste in ingresso"
sent: "Richieste in uscita"
_remoteLookupErrors:
_federationNotAllowed:
title: "Server irraggiungibile"
@ -2822,8 +2891,6 @@ _remoteLookupErrors:
_responseInvalid:
title: "Risposta non valida"
description: "La comunicazione col server è andata a buon fine, ma abbiamo ricevuto dati non validi."
_responseInvalidIdHostNotMatch:
description: "L'indirizzo immesso non coincide con la URL finale. Interrogando i server per un contenuto remoto, assicurarsi di utilizzare la URL finale e non quella di un server intermedio."
_noSuchObject:
title: "Non trovato"
description: "La risorsa richiesta non è stata trovata. Verificare nuovamente la URL."
@ -2840,3 +2907,23 @@ _captcha:
_unknown:
title: "Errore CAPTCHA"
text: "Si è verificato un errore imprevisto."
_bootErrors:
title: "Caricamento non riuscito"
serverError: "Dopo una breve attesa, e dopo aver ricaricato la pagina, se il problema persiste, contatta l'amministrazione comunicando il seguente ID di errore."
solution: "Di seguito, alcune probabili soluzioni al problema."
solution1: "Aggiornare browser e il sistema operativo all'ultima versione"
solution2: "Disattivare gli adblocker"
solution3: "Cancellare la cache del browser"
solution4: "(Per chi utilizza il Browser Tor) Impostare dom.webaudio.enabled = vero"
otherOption: "Altre opzioni"
otherOption1: "Nelle impostazioni, cancellare le impostazioni del client e svuotare la cache"
otherOption2: "Avviare il client predefinito"
otherOption3: "Avviare lo strumento di riparazione"
_search:
searchScopeAll: "Tutte"
searchScopeLocal: "Locale"
searchScopeServer: "Specifiche del server"
searchScopeUser: "Profilo specifico"
pleaseEnterServerHost: "Inserire il nome host"
pleaseSelectUser: "Per favore, seleziona un profilo"
serverHostPlaceholder: "Es: misskey.example.com"

View file

@ -289,7 +289,6 @@ deleteAreYouSure: "「{x}」を削除しますか?"
resetAreYouSure: "リセットしますか?"
areYouSure: "よろしいですか?"
saved: "保存しました"
messaging: "チャット"
upload: "アップロード"
keepOriginalUploading: "オリジナル画像を保持"
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
@ -302,7 +301,7 @@ uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がか
explore: "みつける"
messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません"
startMessaging: "チャットを開始"
startChat: "チャットを始める"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
agree: "同意する"
@ -491,8 +490,6 @@ noteOf: "{user}のノート"
quoteAttached: "引用付き"
quoteQuestion: "引用として添付しますか?"
attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?"
noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "続行する前に、登録またはログインが必要です"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
@ -698,6 +695,7 @@ userSaysSomethingAbout: "{name}が「{word}」について何かを言いまし
makeActive: "アクティブにする"
display: "表示"
copy: "コピー"
copiedToClipboard: "クリップボードにコピーされました"
metrics: "メトリクス"
overview: "概要"
logs: "ログ"
@ -1238,7 +1236,7 @@ releaseToRefresh: "離してリロード"
refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
useGroupedNotifications: "通知をグルーピングして表示する"
useGroupedNotifications: "通知をグルーピング"
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
doReaction: "リアクションする"
@ -1309,6 +1307,116 @@ availableRoles: "利用可能なロール"
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?"
preferences: "環境設定"
accessibility: "アクセシビリティ"
preferencesProfile: "設定のプロファイル"
copyPreferenceId: "設定IDをコピー"
resetToDefaultValue: "初期値に戻す"
overrideByAccount: "アカウントで上書き"
untitled: "無題"
noName: "名前はありません"
skip: "スキップ"
restore: "復元"
syncBetweenDevices: "デバイス間で同期"
preferenceSyncConflictTitle: "サーバーに設定値が存在します"
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
preferenceSyncConflictChoiceServer: "サーバーの設定値"
preferenceSyncConflictChoiceDevice: "デバイスの設定値"
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
paste: "ペースト"
emojiPalette: "絵文字パレット"
postForm: "投稿フォーム"
textCount: "文字数"
information: "情報"
chat: "チャット"
migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
_chat:
noMessagesYet: "まだメッセージはありません"
newMessage: "新しいメッセージ"
individualChat: "個人チャット"
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
roomChat: "ルームチャット"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
createRoom: "ルームを作成"
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
yourRooms: "作成したルーム"
joiningRooms: "参加中のルーム"
invitations: "招待"
noInvitations: "招待はありません"
history: "履歴"
noHistory: "履歴はありません"
noRooms: "ルームはありません"
inviteUser: "ユーザーを招待"
join: "参加"
ignore: "無視"
leave: "ルームから退出"
members: "メンバー"
searchMessages: "メッセージを検索"
home: "ホーム"
muteThisRoom: "このルームをミュート"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
chatWithThisUser: "チャットする"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
_chatAllowedUsers:
everyone: "誰でも"
followers: "自分のフォロワーのみ"
following: "自分がフォローしているユーザーのみ"
mutual: "相互フォローのユーザーのみ"
none: "誰も許可しない"
_emojiPalette:
palettes: "パレット"
enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期を有効にする"
paletteForMain: "メインで使用するパレット"
paletteForReaction: "リアクションで使用するパレット"
_settings:
driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"
pluginBanner: "プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。"
notificationsBanner: "サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。"
api: "API"
webhook: "Webhook"
serviceConnection: "サービス連携"
serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。"
accountData: "アカウントのデータ"
accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できます。"
muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。"
accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。"
privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。"
securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。"
preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定が行えます。"
appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
timelineAndNote: "タイムラインとノート"
makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする"
makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。"
_preferencesProfile:
profileName: "プロファイル名"
profileNameDescription: "このデバイスを識別する名前を設定してください。"
profileNameDescription2: "例: 「メインPC」、「スマホ」など"
_preferencesBackup:
autoBackup: "自動バックアップ"
restoreFromBackup: "バックアップから復元"
noBackupsFoundTitle: "バックアップが見つかりませんでした"
noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。"
selectBackupToRestore: "復元するバックアップを選択してください"
youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
backupFound: "設定のバックアップが見つかりました"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@ -1320,6 +1428,7 @@ _accountSettings:
makeNotesHiddenBefore: "過去のノートを非公開化する"
makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。"
notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
@ -1817,6 +1926,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
canChat: "チャットを許可"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
@ -2001,6 +2111,7 @@ _theme:
installed: "{name}をインストールしました"
installedThemes: "インストールされたテーマ"
builtinThemes: "標準のテーマ"
instanceTheme: "サーバーのテーマ"
alreadyInstalled: "そのテーマは既にインストールされています"
invalid: "テーマの形式が間違っています"
make: "テーマを作る"
@ -2069,6 +2180,7 @@ _sfx:
noteMy: "ノート(自分)"
notification: "通知"
reaction: "リアクション選択時"
chatMessage: "チャットのメッセージ"
_soundSettings:
driveFile: "ドライブの音声を使用"
@ -2221,6 +2333,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する"
"write:chat": "チャットを操作する"
"read:chat": "チャットを閲覧する"
_auth:
shareAccessTitle: "アプリへのアクセス許可"
@ -2500,6 +2614,8 @@ _notification:
flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが完了しました"
login: "ログインがありました"
createToken: "アクセストークンが作成されました"
createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。"
_types:
all: "すべて"
@ -2517,6 +2633,7 @@ _notification:
achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
createToken: "アクセストークンの作成"
test: "通知のテスト"
app: "連携アプリからの通知"
@ -2546,6 +2663,7 @@ _deck:
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
flexible: "幅を自動調整"
enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
_columns:
main: "メイン"
@ -2660,6 +2778,7 @@ _moderationLogTypes:
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer:
title: "ファイルの詳細"
@ -2675,10 +2794,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。"
_plugin:
title: "このプラグインをインストールしますか?"
metaTitle: "プラグイン情報"
_theme:
title: "このテーマをインストールしますか?"
metaTitle: "テーマ情報"
_meta:
base: "基本のカラースキーム"
_vendorInfo:
@ -2907,9 +3024,7 @@ _remoteLookupErrors:
description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。"
_responseInvalid:
title: "レスポンスが不正です"
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。"
_responseInvalidIdHostNotMatch:
description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
_noSuchObject:
title: "見つかりません"
description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。"
@ -2927,3 +3042,25 @@ _captcha:
_unknown:
title: "CAPTCHAエラー"
text: "想定外のエラーが発生しました。"
_bootErrors:
title: "読み込みに失敗しました"
serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。"
solution: "以下を行うと解決する可能性があります。"
solution1: "ブラウザおよびOSを最新バージョンに更新する"
solution2: "アドブロッカーを無効にする"
solution3: "ブラウザのキャッシュをクリアする"
solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
otherOption: "その他のオプション"
otherOption1: "クライアント設定とキャッシュを削除"
otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動"
_search:
searchScopeAll: "全て"
searchScopeLocal: "ローカル"
searchScopeServer: "サーバー指定"
searchScopeUser: "ユーザー指定"
pleaseEnterServerHost: "サーバーのホストを入力してください"
pleaseSelectUser: "ユーザーを選択してください"
serverHostPlaceholder: "例: misskey.example.com"

View file

@ -5,6 +5,7 @@ introMisskey: "ようお越しMisskeyは、オープンソースの分散型
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
monthAndDay: "{month}月 {day}日"
search: "探す"
reset: "リセット"
notifications: "通知"
username: "ユーザー名"
password: "パスワード"
@ -48,6 +49,7 @@ pin: "ピン留めしとく"
unpin: "ピン留めやめる"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
copyRemoteLink: "リモートのリンクをコピーするで?"
copyLinkRenote: "リノートのリンクをコピーするで?"
delete: "ほかす"
deleteAndEdit: "ほかして直す"
@ -684,11 +686,15 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
wordMuteDescription: "指定した語句が入ってるノートを最小化するで。最小化されたノートをクリックしたら、表示できるようになるで。"
hardWordMute: "ハードワードミュート"
showMutedWord: "ミュートされたワードを表示するで"
hardWordMuteDescription: "指定した語句が入ってるノートを隠すで。ワードミュートとちゃうて、ノートは完全に表示されんようになるで。"
regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
instanceMute: "サーバーミュート"
userSaysSomething: "{name}が何か言うとるわ"
userSaysSomethingAbout: "{name}が「{word}」についてなんか言うてたで"
makeActive: "使うで"
display: "表示"
copy: "コピー"
@ -1301,6 +1307,14 @@ lockdown: "ロックダウン"
pleaseSelectAccount: "アカウント選んでや"
availableRoles: "使えるロール"
acknowledgeNotesAndEnable: "注意事項をわかった上でオンにする。"
federationSpecified: "このサーバーはホワイトリスト連合で運用されてるで。管理者が指定したサーバー以外とはやり取りできひんで。"
federationDisabled: "このサーバーは連合が無効化されてるで。他のサーバーのユーザーとやり取りすることはできひんで。"
confirmOnReact: "ツッコむときに確認とる"
reactAreYouSure: "\" {emoji} \" でツッコむ?"
postForm: "投稿フォーム"
information: "情報"
_settings:
webhook: "Webhook"
_accountSettings:
requireSigninToViewContents: "ログインしてもらってからコンテンツ見てもらう"
requireSigninToViewContentsDescription1: "あなたが作成した全部のノートとかのコンテンツを見れるようにするのにログインがいるようにするで。クローラーにいろいろ収集されるんを防げるかもしれん。"
@ -2432,6 +2446,8 @@ _notification:
flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが終わったわ"
login: "ログインしとったで"
createToken: "アクセストークンが作成されたで"
createTokenDescription: "心当たりないんやったら「{text}」でアクセストークンを削除してやって。"
_types:
all: "すべて"
note: "あんたらの新規投稿"
@ -2595,10 +2611,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。"
_plugin:
title: "このプラグイン、インストールする?"
metaTitle: "プラグイン情報"
_theme:
title: "このテーマインストールする?"
metaTitle: "テーマ情報"
_meta:
base: ""
_vendorInfo:
@ -2718,6 +2732,66 @@ _contextMenu:
app: "アプリ"
appWithShift: "Shiftキーでアプリ"
native: "ブラウザのUI"
_gridComponent:
_error:
requiredValue: "この値は必須項目やで"
columnTypeNotSupport: "正規表現によるバリデーションはtype:textのカラムだけサポートしてるで"
patternNotMatch: "この値は{pattern}のパターンに一致しいひんで"
notUnique: "この値は一意でなあかんで"
_roleSelectDialog:
notSelected: "選択されとらんで"
_customEmojisManager:
_gridCommon:
copySelectionRows: "選択行をコピーするで"
copySelectionRanges: "選択範囲をコピーするで"
deleteSelectionRows: "選択行を削除するで"
deleteSelectionRanges: "選択範囲の値をクリアするで"
searchSettings: "検索設定"
searchSettingCaption: "検索条件を詳しく設定するで。"
searchLimit: "表示件数"
sortOrder: "並び順"
registrationLogs: "登録ログ"
registrationLogsCaption: "絵文字更新・削除時のログが表示されるで。更新・削除操作をしたり、ページを遷移・リロードしたら消えるから気ぃつけてな。"
alertEmojisRegisterFailedDescription: "絵文字の更新・削除に失敗したで。詳細は登録ログを確認してな。"
_logs:
showSuccessLogSwitch: "成功ログを表示するで"
failureLogNothing: "失敗ログはあらへん。"
logNothing: "失敗ログはあらへん。"
_remote:
selectionRowDetail: "選択行の詳細やで"
importSelectionRows: "選択行をインポートするで"
importSelectionRangesRows: "選択範囲の行をインポートするで"
importEmojisButton: "チェックされた絵文字をインポートするで"
confirmImportEmojisTitle: "絵文字のインポートするで"
confirmImportEmojisDescription: "リモートから受信した{count}個の絵文字をインポートするで。絵文字のライセンスには十分気ぃつけてな。実行してもええか?"
_local:
tabTitleList: "登録済み絵文字一覧"
tabTitleRegister: "絵文字の登録"
_list:
emojisNothing: "登録された絵文字はないで。"
markAsDeleteTargetRows: "選択行を削除対象にするで"
markAsDeleteTargetRanges: "選択範囲の行を削除対象にするで"
alertUpdateEmojisNothingDescription: "変更された絵文字はないで。"
alertDeleteEmojisNothingDescription: "削除対象の絵文字はないで。"
confirmMovePage: "ページを移動してもええんか?"
confirmChangeView: "表示を変更してもええんか?"
confirmUpdateEmojisDescription: "{count}個の絵文字を更新するで。実行してもええか?"
confirmDeleteEmojisDescription: "チェックがつけられた{count}個の絵文字を削除するで。ほんまにええか?"
confirmResetDescription: "今までやった変更が全部リセットされるで。"
confirmMovePageDesciption: "このページの絵文字に変更が加えられてるで。\n保存せずページを移動してまうと、このページで加えた変更が全てパーになるで。"
dialogSelectRoleTitle: "絵文字に設定されたロールで検索"
_register:
uploadSettingTitle: "アップロード設定"
uploadSettingDescription: "この画面で絵文字アップロードするときの動きを設定できるで。"
directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する"
directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。"
emojiInputAreaCaption: "どれかの方法で登録する絵文字を選択して。"
emojiInputAreaList1: "この枠に画像ファイルかディレクトリをドラッグ&ドロップ"
emojiInputAreaList2: "このリンクをクリックしてPCから選択する"
emojiInputAreaList3: "このリンクをクリックしてドライブから選択する"
confirmRegisterEmojisDescription: "リストに表示されてる絵文字を新たなカスタム絵文字として登録するで。ほんまにええか? (サーバーがしんどくなるから、一回で登録できる絵文字は{count}件までやで)"
confirmClearEmojisDescription: "編集内容をほかして、リストに表示されている絵文字をクリアするで。ほんまにええか?"
confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードするで。ほんまにええか?"
_embedCodeGen:
title: "埋め込みコードをカスタム"
header: "ヘッダー出す"
@ -2754,8 +2828,35 @@ _remoteLookupErrors:
_responseInvalid:
title: "レスポンスがおかしいで"
description: "このサーバーと通信することはできたけど、もらったデータがおかしかったで。"
_responseInvalidIdHostNotMatch:
description: "入力されたURIのドメインと最終的に得られたURIのドメインとが違うで。第三者のサーバーを介してリモートのコンテンツを照会してるんやったら、発信元のサーバーで取得できるURIを使って照会し直して。"
_noSuchObject:
title: "見つからへんね"
description: "求められたリソースが見つからんかったで。URIをもっかい確かめてや。"
_captcha:
verify: "CAPTCHAしばいたって"
testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できるで。\n詳細は下記ページを確認してな。"
_error:
_requestFailed:
title: "CAPTCHAのリクエストに失敗してもうた"
text: "しばらく後で実行するか、設定をもっかい確認してや。"
_verificationFailed:
title: "CAPTCHAのリクエストに失敗してもうた"
text: "設定がほんまに合ってるかもっかい確認してや。"
_unknown:
title: "CAPTCHAエラー"
text: "思いもせんかったエラーが起きたわ。"
_bootErrors:
title: "読み込みに失敗したで"
serverError: "少し待ってからリロードしてもまだ問題が解決されんのやったら、以下のError IDを添えてサーバー管理者に連絡して。"
solution: "以下のことやったら解決するかもやで。"
solution1: "ブラウザとかOSを最新バージョンに更新する"
solution2: "アドブロッカーを無効にする"
solution3: "ブラウザのキャッシュをクリアする"
solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
otherOption: "ほかのオプション"
otherOption1: "クライアント設定とキャッシュをほかす"
otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動"
_search:
searchScopeAll: "みんな"
searchScopeLocal: "ローカル"
searchScopeUser: "ユーザー指定"

View file

@ -655,6 +655,7 @@ replies: "답하기"
renotes: "리노트"
attach: "옇기"
surrender: "아이예"
information: "정보"
_delivery:
stop: "고만 보내예"
_type:
@ -843,3 +844,6 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "몬 찾앗십니다"
_search:
searchScopeAll: "말캉"
searchScopeUser: "사용자 지정"

View file

@ -1309,6 +1309,50 @@ availableRoles: "사용 가능한 역할"
acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다."
federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다."
federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다."
confirmOnReact: "리액션할 때 확인"
reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?"
markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?"
unmarkAsSensitiveConfirm: "이 미디어의 민감한 미디어 지정을 해제하시겠습니까?"
preferences: "환경설정"
accessibility: "접근성"
preferencesProfile: "설정 프로필"
copyPreferenceId: "설정한 ID를 복사"
resetToDefaultValue: "기본값으로 되돌리기"
overrideByAccount: "계정으로 덮어쓰기"
untitled: "제목 없음"
noName: "이름이 없습니다."
skip: "건너뛰기"
restore: "복원"
syncBetweenDevices: "장치간 동기화"
preferenceSyncConflictTitle: "서버에 설정값이 존재합니다."
preferenceSyncConflictChoiceServer: "서버 설정값"
preferenceSyncConflictChoiceDevice: "장치 설정값"
paste: "붙여넣기"
emojiPalette: "이모지 팔레트"
postForm: "글 입력란"
information: "정보"
_emojiPalette:
palettes: "팔레트"
paletteForMain: "메인으로 사용할 팔레트"
paletteForReaction: "리액션으로 사용할 팔레트"
_settings:
api: "API"
webhook: "Webhook"
serviceConnection: "서비스 연동"
accountData: "계정 데이터"
_preferencesProfile:
profileName: "프로필 이름"
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
profileNameDescription2: "예: '메인PC', '스마트폰' 등"
_preferencesBackup:
autoBackup: "자동 백업"
restoreFromBackup: "백업으로 복구"
noBackupsFoundTitle: "백업을 찾을 수 없습니다"
noBackupsFoundDescription: "자동으로 생성된 백업은 찾을 수 없었지만, 수동으로 백업 파일을 저장한 경우 해당 파일을 가져와 복원할 수 있습니다."
selectBackupToRestore: "복원할 백업을 선택하세요"
youNeedToNameYourProfileToEnableAutoBackup: "자동 백업을 활성화하려면 프로필 이름을 설정해야 합니다."
autoPreferencesBackupIsNotEnabledForThisDevice: "이 장치에서 설정 자동 백업이 활성화되어 있지 않습니다."
backupFound: "설정 백업이 발견되었습니다"
_accountSettings:
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인을 필수로 설정하기"
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
@ -1967,6 +2011,7 @@ _theme:
installed: "{name} 테마가 설치되었습니다"
installedThemes: "설치된 테마"
builtinThemes: "표준 테마"
instanceTheme: "서버 테마"
alreadyInstalled: "이미 설치된 테마입니다"
invalid: "테마 형식이 올바르지 않습니다"
make: "테마 만들기"
@ -2440,6 +2485,8 @@ _notification:
flushNotification: "알림 이력을 초기화"
exportOfXCompleted: "{x} 추출에 성공했습니다."
login: "로그인 알림이 있습니다"
createToken: "액세스 토큰이 생성되었습니다"
createTokenDescription: "만약 기억이 나지 않는다면 '{text}'를 통해 액세스 토큰을 삭제해 주세요."
_types:
all: "전부"
note: "사용자의 새 글"
@ -2590,6 +2637,7 @@ _moderationLogTypes:
deletePage: "페이지를 삭제"
deleteFlash: "Play를 삭제"
deleteGalleryPost: "갤러리 포스트를 삭제"
updateProxyAccountDescription: "프록시 계정의 설명 업데이트"
_fileViewer:
title: "파일 상세"
type: "파일 유형"
@ -2603,10 +2651,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "제공자를 신뢰할 수 있는 경우에만 설치하십시오."
_plugin:
title: "이 플러그인을 설치하시겠습니까?"
metaTitle: "플러그인 정보"
_theme:
title: "이 테마를 설치하시겠습니까?"
metaTitle: "테마 정보"
_meta:
base: "기본 컬러 스키마"
_vendorInfo:
@ -2822,8 +2868,6 @@ _remoteLookupErrors:
_responseInvalid:
title: "유효하지 않은 반응입니다."
description: "이 서버와 통신할 수 있지만, 데이터가 올바르지 않습니다."
_responseInvalidIdHostNotMatch:
description: "입력된 URI과 실제 URI가 다릅니다. 제 3자 서버를 통한 리모트 컨텐츠를 조회하는 경우, 원래 서버 측에서 받아올 수 있는 URI를 사용하여 조회하시길 바랍니다."
_noSuchObject:
title: "찾을 수 없습니다"
description: "요구된 리소스를 찾을 수 없습니다. URI를 다시 한 번 확인해보세요."
@ -2840,3 +2884,23 @@ _captcha:
_unknown:
title: "CAPTCHA 에러"
text: "알 수 없는 에러가 발생했습니다."
_bootErrors:
title: "로딩이 실패함"
serverError: "잠시 기다렸다가 다시 로드해도 여전히 문제가 해결되지 않으면 아래 Error ID와 함께 서버 관리자에게 연락해 주세요."
solution: "다음과 같은 방법으로 해결할 수 있습니다."
solution1: "브라우저 및 OS를 최신 버전으로 업데이트하기"
solution2: "광고 차단 비활성화하기"
solution3: "브라우저 캐시 지우기"
solution4: "(Tor Browser) dom.webaudio.enabled를 true로 설정하세요"
otherOption: "기타 옵션"
otherOption1: "클라이언트 설정 및 캐시 삭제"
otherOption2: "간편 클라이언트 실행"
otherOption3: "복구 툴 실행"
_search:
searchScopeAll: "전체"
searchScopeLocal: "로컬"
searchScopeServer: "서버 지정"
searchScopeUser: "사용자 지정"
pleaseEnterServerHost: "서버의 호스트를 입력해 주세요."
pleaseSelectUser: "유저를 선택해주세요"
serverHostPlaceholder: "예: misskey.example.com"

View file

@ -394,6 +394,7 @@ searchByGoogle: "ຄົ້ນຫາ"
file: "ໄຟລ໌"
replies: "ຕອບ​ກັບ"
renotes: "Renote"
information: "ກ່ຽວກັບ"
_delivery:
stop: "ໂຈະ"
_type:
@ -477,3 +478,5 @@ _moderationLogTypes:
_remoteLookupErrors:
_noSuchObject:
title: "ບໍ່ພົບ"
_search:
searchScopeAll: "ທັງໝົດ"

View file

@ -462,6 +462,7 @@ loggedInAsBot: "Momenteel als bot ingelogd"
icon: "Avatar"
replies: "Antwoord"
renotes: "Herdelen"
information: "Over"
_delivery:
stop: "Opgeschort"
_type:
@ -540,3 +541,5 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Niet gevonden"
_search:
searchScopeAll: "Alle"

View file

@ -463,6 +463,7 @@ icon: "Avatar"
replies: "Svar"
renotes: "Renote"
surrender: "Avbryt"
information: "Informasjon"
_delivery:
stop: "Suspendert"
_initialAccountSetting:
@ -730,3 +731,5 @@ _moderationLogTypes:
_remoteLookupErrors:
_noSuchObject:
title: "Ikke funnet"
_search:
searchScopeAll: "Alle"

View file

@ -1044,6 +1044,8 @@ flip: "Odwróć"
lastNDays: "W ciągu ostatnich {n} dni"
surrender: "Odrzuć"
gameRetry: "Spróbuj ponownie"
postForm: "Formularz tworzenia wpisu"
information: "Informacje"
_delivery:
stop: "Zawieszono"
_type:
@ -1583,3 +1585,6 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Nie znaleziono"
_search:
searchScopeAll: "Wszystkie"
searchScopeLocal: "Lokalne"

View file

@ -1301,6 +1301,10 @@ lockdown: "Lockdown"
pleaseSelectAccount: "Selecione uma conta"
availableRoles: "Cargos disponíveis"
acknowledgeNotesAndEnable: "Ative após compreender as precauções."
postForm: "Campo de postagem"
information: "Informações"
_settings:
webhook: "Webhook"
_accountSettings:
requireSigninToViewContents: "Exigir cadastro para ver o conteúdo"
requireSigninToViewContentsDescription1: "Exigir cadastro para ver todas as notas e outro conteúdo que você criou. Isso previne 'crawlers' de coletar os seus dados."
@ -2595,10 +2599,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação."
_plugin:
title: "Deseja instalar esse plugin?"
metaTitle: "Informações do plugin"
_theme:
title: "Deseja instalar esse tema?"
metaTitle: "Informações do tema"
_meta:
base: "Paleta de cores base"
_vendorInfo:
@ -2754,8 +2756,10 @@ _remoteLookupErrors:
_responseInvalid:
title: "Resposta inválida"
description: "Foi possível comunicar com o servidor, porém os dados obtidos foram incorretos."
_responseInvalidIdHostNotMatch:
description: "O domínio do endereço inserido difere do domínio do endereço final. Se você estiver pesquisando por um servidor de terceiros, tente buscar novamente com um endereço que pode ser obtido através do servidor original."
_noSuchObject:
title: "Não encontrado"
description: "O recurso solicitado não foi encontrado, confira o endereço."
_search:
searchScopeAll: "Todos"
searchScopeLocal: "Local"
searchScopeUser: "Usuário específico"

View file

@ -646,6 +646,7 @@ show: "Arată"
icon: "Avatar"
replies: "Răspunde"
renotes: "Re-notează"
information: "Despre"
_delivery:
stop: "Suspendat"
_type:
@ -736,3 +737,5 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Nu a fost găsit"
_search:
searchScopeAll: "Tot"

View file

@ -1181,6 +1181,10 @@ keepOriginalFilenameDescription: "Если вы выключите данную
alwaysConfirmFollow: "Всегда подтверждать подписку"
inquiry: "Связаться"
messageToFollower: "Сообщение подписчикам"
postForm: "Форма отправки"
information: "Описание"
_settings:
webhook: "Вебхук"
_delivery:
stop: "Заморожено"
_type:
@ -2147,3 +2151,7 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Не найдено"
_search:
searchScopeAll: "Все"
searchScopeLocal: "Местная"
searchScopeUser: "Указанный пользователь"

View file

@ -917,6 +917,8 @@ renotes: "Preposlať"
sourceCode: "Zdrojový kód"
flip: "Preklopiť"
lastNDays: "Posledných {n} dní"
postForm: "Napísať poznámku"
information: "Informácie"
_delivery:
stop: "Zmrazené"
_type:
@ -1449,3 +1451,6 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Nenájdené"
_search:
searchScopeAll: "Všetko"
searchScopeLocal: "Lokálne"

View file

@ -562,6 +562,7 @@ inquiry: "Kontakt"
tryAgain: "Försök igen senare"
signinWithPasskey: "Logga in med nyckel"
unknownWebAuthnKey: "Okänd nyckel"
information: "Om"
_delivery:
stop: "Suspenderad"
_type:
@ -707,3 +708,5 @@ _reversi:
white: "Vit"
_selfXssPrevention:
warning: "VARNING"
_search:
searchScopeAll: "Allt"

View file

@ -1292,6 +1292,10 @@ prohibitedWordsForNameOfUser: "คำนี้ไม่สามารถใช
prohibitedWordsForNameOfUserDescription: "หากมีสตริงใดๆ ในรายการนี้ปรากฏอยู่ในชื่อของผู้ใช้ ชื่อนั้นจะถูกปฏิเสธ ผู้ใช้ที่มีสิทธิ์แต่ผู้ดูแลระบบนั้นจะไม่ได้รับผลกระทบใดๆจากข้อจำกัดนี้ค่ะ"
yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม"
yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ"
postForm: "แบบฟอร์มการโพสต์"
information: "เกี่ยวกับ"
_settings:
webhook: "Webhook"
_abuseUserReport:
forward: "ส่ง​ต่อ"
forwardDescription: "ส่งรายงานไปยังเซิร์ฟเวอร์ระยะไกลโดยใช้บัญชีระบบที่ไม่ระบุตัวตน"
@ -2569,10 +2573,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง"
_plugin:
title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?"
metaTitle: "ข้อมูลส่วนเสริม"
_theme:
title: "ต้องการติดตั้งธีมนี้ใช่ไหม?"
metaTitle: "ข้อมูลธีม"
_meta:
base: "โทนสีพื้นฐาน"
_vendorInfo:
@ -2709,3 +2711,7 @@ _embedCodeGen:
_remoteLookupErrors:
_noSuchObject:
title: "ไม่พบหน้าที่ต้องการ"
_search:
searchScopeAll: "ทั้งหมด"
searchScopeLocal: "ท้องถิ่น"
searchScopeUser: "ผู้ใช้เฉพาะ"

View file

@ -460,3 +460,5 @@ _deck:
_moderationLogTypes:
suspend: "askıya al"
resetPassword: "Şifre sıfırlama"
_search:
searchScopeAll: "Tümü"

View file

@ -909,6 +909,8 @@ renotes: "Поширити"
sourceCode: "Вихідний код"
flip: "Перевернути"
lastNDays: "Останні {n} днів"
postForm: "Створення нотатки"
information: "Інформація"
_delivery:
stop: "Призупинено"
_type:
@ -1624,3 +1626,6 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Не знайдено"
_search:
searchScopeAll: "Всі"
searchScopeLocal: "Локальна"

View file

@ -841,6 +841,7 @@ icon: "Avatar"
replies: "Javob berish"
renotes: "Qayta qayd etish"
flip: "Teskari"
information: "Haqida"
_delivery:
stop: "To'xtatilgan"
_type:
@ -1094,3 +1095,6 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Topilmadi"
_search:
searchScopeAll: "Barcha"
searchScopeLocal: "Mahalliy"

View file

@ -1119,6 +1119,8 @@ pullDownToRefresh: "Kéo xuống để làm mới"
cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải có chú thích."
lastNDays: "{n} ngày trước"
surrender: "Từ chối"
postForm: "Mẫu đăng"
information: "Giới thiệu"
_delivery:
stop: "Đã vô hiệu hóa"
_type:
@ -1930,3 +1932,7 @@ _reversi:
_remoteLookupErrors:
_noSuchObject:
title: "Không tìm thấy"
_search:
searchScopeAll: "Tất cả"
searchScopeLocal: "Máy chủ này"
searchScopeUser: "Người dùng chỉ định"

View file

@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} 说了关于「{word}」的什么"
makeActive: "启用"
display: "显示"
copy: "复制"
copiedToClipboard: "已复制到剪贴板"
metrics: "指标"
overview: "概览"
logs: "日志"
@ -746,7 +747,7 @@ confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。
public: "公开"
private: "私密"
i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。"
manageAccessTokens: "管理 Access Tokens"
manageAccessTokens: "管理访问令牌"
accountInfo: "账户信息"
notesCount: "帖子数量"
repliesCount: "回复数量"
@ -1309,6 +1310,69 @@ availableRoles: "可用角色"
acknowledgeNotesAndEnable: "理解注意事项后再开启。"
federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。"
federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
confirmOnReact: "发送回应前需要确认"
reactAreYouSure: "要用「{emoji}」进行回应吗?"
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
preferences: "设置"
accessibility: "辅助功能"
preferencesProfile: "设置的配置"
copyPreferenceId: "复制设置 ID"
resetToDefaultValue: "重置为默认值"
overrideByAccount: "用账户覆盖"
untitled: "未命名"
noName: "没有名字"
skip: "跳过"
restore: "恢复"
syncBetweenDevices: "设备间同步"
preferenceSyncConflictTitle: "服务器上已存在设定值"
preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?"
preferenceSyncConflictChoiceServer: "服务器上的设定值"
preferenceSyncConflictChoiceDevice: "设备上的设定值"
preferenceSyncConflictChoiceCancel: "取消同步"
paste: "粘贴"
emojiPalette: "表情符号调色板"
postForm: "投稿窗口"
textCount: "字数"
information: "关于"
_emojiPalette:
palettes: "调色板"
enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
paletteForMain: "主调色板"
paletteForReaction: "回应用调色板"
_settings:
driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。"
pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。"
notificationsBanner: "可在此设置从服务器接收的通知的种类和范围,以及推送通知的设置。"
api: "API"
webhook: "Webhook"
serviceConnection: "连接服务"
serviceConnectionBanner: "可在此管理用于连接外部应用或服务的访问令牌及 Webhook。"
accountData: "账户数据"
accountDataBanner: "可在此导入或导出帐户数据的存档。"
muteAndBlockBanner: "可在此设置隐藏内容,或限制指定用户能进行的操作。"
accessibilityBanner: "可在此设置客户端的显示及动态效果等辅助设置。"
privacyBanner: "可在此设置如内容可见性、可发现性、批准关注请求等账户隐私设置。"
securityBanner: "可在此设置如密码、登入方式、验证器、Passkey 等账户安全性设置。"
preferencesBanner: "可在此设置客户端的整体运作行为。"
appearanceBanner: "可在此设置客户端的外观及显示方式。"
soundsBanner: "可在此设置客户端播放的声音。"
timelineAndNote: "时间线和帖子"
makeEveryTextElementsSelectable: "使所有的文字均可选择"
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
_preferencesProfile:
profileName: "配置名"
profileNameDescription: "请指定用于识别此设备的名称"
profileNameDescription2: "如「PC」、「手机」等"
_preferencesBackup:
autoBackup: "自动备份"
restoreFromBackup: "从备份恢复"
noBackupsFoundTitle: "没有找到备份"
noBackupsFoundDescription: "没有找到自动备份。若有手动保存备份文件,可将其导入来恢复。"
selectBackupToRestore: "请选择要恢复的备份"
youNeedToNameYourProfileToEnableAutoBackup: "需指定配置名以开启自动备份。"
autoPreferencesBackupIsNotEnabledForThisDevice: "此设备未开启自动备份"
backupFound: "已找到备份"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
@ -1319,6 +1383,7 @@ _accountSettings:
makeNotesHiddenBefore: "将过去的帖子设为私密"
makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。"
mayNotEffectForFederatedNotes: "与远程服务器联合的帖子在远端可能会没有效果。"
mayNotEffectSomeSituations: "此限制功能非常简单,在与远程服务器联合等情形时可能不适用。"
notesHavePassedSpecifiedPeriod: "超过指定时间的帖子"
notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子"
_abuseUserReport:
@ -1967,6 +2032,7 @@ _theme:
installed: "{name} 已安装"
installedThemes: "已安装的主题"
builtinThemes: "标准主题"
instanceTheme: "服务器主题"
alreadyInstalled: "此主题已经安装"
invalid: "主题格式错误"
make: "制作主题"
@ -2440,6 +2506,8 @@ _notification:
flushNotification: "重置通知历史"
exportOfXCompleted: "已完成 {x} 的导出"
login: "有新的登录"
createToken: "访问令牌已创建"
createTokenDescription: "如果不明白其用途,请遵循「{text}」的指示删除访问令牌。"
_types:
all: "全部"
note: "用户的新帖子"
@ -2456,6 +2524,7 @@ _notification:
achievementEarned: "取得的成就"
exportCompleted: "已完成导出"
login: "登录"
createToken: "创建访问令牌"
test: "测试通知"
app: "关联应用的通知"
_actions:
@ -2483,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "用简易UI表示非根页面"
usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度"
flexible: "自适应宽度"
enableSyncBetweenDevicesForProfiles: "启用个人资料信息跨设备同步"
_columns:
main: "主列"
widgets: "小工具"
@ -2590,6 +2660,7 @@ _moderationLogTypes:
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除了图库稿件"
updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer:
title: "文件信息"
type: "文件类型"
@ -2603,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "请在安装前确保来源可靠"
_plugin:
title: "要安装此插件吗?"
metaTitle: "插件信息"
_theme:
title: "要安装此主题吗?"
metaTitle: "主题信息"
_meta:
base: "基本配色方案"
_vendorInfo:
@ -2777,8 +2846,8 @@ _customEmojisManager:
_register:
uploadSettingTitle: "上传设置"
uploadSettingDescription: "可以在此页面设置上传表情符号时的行为。"
directoryToCategoryLabel: "目录名请输入「category」"
directoryToCategoryCaption: "拖放目录时,目录名请输入「category」"
directoryToCategoryLabel: "将目录名设为「category」"
directoryToCategoryCaption: "拖放目录时,将目录名设置为「category」"
emojiInputAreaCaption: "请使用其中一种方法选择要注册的表情符号。"
emojiInputAreaList1: "在此区域内拖放图像文件或者目录"
emojiInputAreaList2: "单击此链接以从电脑中选择"
@ -2822,8 +2891,6 @@ _remoteLookupErrors:
_responseInvalid:
title: "响应无效"
description: "成功与此服务器通信,但返回的数据无效。"
_responseInvalidIdHostNotMatch:
description: "输入 URI 的域名和最终取得的 URI 的域名不同。如果是通过第三方服务器获取远程内容,请使用可以从原始服务器获取内容的 URI 再试一次。"
_noSuchObject:
title: "未找到"
description: "未找到请求的资源。请再次检查 URI。"
@ -2840,3 +2907,23 @@ _captcha:
_unknown:
title: "CAPTCHA 错误"
text: "发生意外错误。"
_bootErrors:
title: "加载失败"
serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。"
solution: "以下方法或许可以解决问题:"
solution1: "将浏览器及操作系统更新到最新版本"
solution2: "禁用广告屏蔽插件"
solution3: "清除浏览器缓存"
solution4: "Tor Browser将 dom.webaudio.enabled 设定为 true"
otherOption: "其它选项"
otherOption1: "清除客户端设定与缓存"
otherOption2: "使用简易客户端"
otherOption3: "启动修复工具"
_search:
searchScopeAll: "全部"
searchScopeLocal: "本地"
searchScopeServer: "指定服务器"
searchScopeUser: "指定用户"
pleaseEnterServerHost: "请填写服务器主机名"
pleaseSelectUser: "请选择用户"
serverHostPlaceholder: "如misskey.example.com"

View file

@ -103,7 +103,7 @@ serverIsDead: "伺服器沒有回應。請稍等片刻再試。"
youShouldUpgradeClient: "請重新載入以使用新版客戶端顯示此頁面。"
enterListName: "輸入清單名稱"
privacy: "隱私"
makeFollowManuallyApprove: "手動審核追隨請求"
makeFollowManuallyApprove: "追隨需要核准"
defaultNoteVisibility: "預設可見性"
follow: "追隨"
followRequest: "追隨請求"
@ -368,7 +368,7 @@ normal: "正常"
instanceName: "伺服器名稱"
instanceDescription: "伺服器介紹"
maintainerName: "管理員名稱"
maintainerEmail: "管理員箱"
maintainerEmail: "管理員箱"
tosUrl: "服務條款 URL"
thisYear: "本年"
thisMonth: "本月"
@ -459,13 +459,13 @@ moderationNoteDescription: "您可以編寫僅在審查員之間共用的註解
addModerationNote: "新增管理筆記"
moderationLogs: "管理日誌"
nUsersMentioned: "被 {n} 個人提及"
securityKeyAndPasskey: "安全金鑰、Passkey"
securityKeyAndPasskey: "安全金鑰、通行金鑰"
securityKey: "安全金鑰"
lastUsed: "上次使用"
lastUsedAt: "上次使用:{t}"
unregister: "註銷"
passwordLessLogin: "設置無密碼登入"
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
passwordLessLogin: "無密碼登入"
passwordLessLoginDescription: "不使用密碼,以安全金鑰或通行金鑰登入"
resetPassword: "重設密碼"
newPasswordIs: "新密碼為「{password}」"
reduceUiAnimation: "減少介面的動態視覺"
@ -521,7 +521,7 @@ menuStyle: "選單風格"
style: "風格"
drawer: "側邊欄"
popup: "彈出式視窗"
showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的"
showNoteActionsOnlyHover: "僅於游標懸停時顯示貼文選項"
showReactionsCount: "顯示貼文的反應數目"
noHistory: "沒有歷史紀錄"
signinHistory: "登入歷史"
@ -558,12 +558,12 @@ useObjectStorage: "使用物件儲存"
objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL例如 S3https://<bucket>.s3.amazonaws.com、GCShttps://storage.googleapis.com/<bucket>)。"
objectStorageBucket: "儲存空間Bucket"
objectStorageBucketDesc: "請填寫所用服務的儲存空間Bucket名稱。 "
objectStorageBucketDesc: "請填寫所用服務的儲存Bucket名稱。 "
objectStoragePrefix: "前綴"
objectStoragePrefixDesc: "它儲存在此前綴目錄下。"
objectStorageEndpoint: "端點Endpoint"
objectStorageEndpointDesc: "如使用 AWS S3請留空。如使用其他服務請按照其說明文件以「<host>」或「<host>:<port>」的形式設定端點Endpoint。"
objectStorageRegion: "Region"
objectStorageRegion: "Region"
objectStorageRegionDesc: "請填寫一個分區例如「xx-east-1」。 如果您使用的服務不設分區請留空或填寫「us-east-1」。"
objectStorageUseSSL: "使用 SSL"
objectStorageUseSSLDesc: "請在不使用 https 連接 API 時關閉"
@ -586,7 +586,7 @@ popout: "彈出式視窗"
volume: "音量"
masterVolume: "主音量"
notUseSound: "關閉音效"
useSoundOnlyWhenActive: "瀏覽器在前景運作時Misskey 才會發出音效"
useSoundOnlyWhenActive: "僅在 Misskey 於前景運作時發出音效"
details: "詳細資訊"
renoteDetails: "轉發貼文的細節"
chooseEmoji: "選擇您的表情符號"
@ -681,7 +681,7 @@ smtpHost: "主機"
smtpPort: "埠"
smtpUser: "使用者名稱"
smtpPass: "密碼"
emptyToDisableSmtpAuth: "留空使用者名稱和密碼以關閉SMTP驗證。"
emptyToDisableSmtpAuth: "將使用者名稱和密碼留空以關閉 SMTP 驗證。"
smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS"
smtpSecureInfo: "使用 STARTTLS 時關閉。"
testEmail: "測試郵件發送"
@ -698,6 +698,7 @@ userSaysSomethingAbout: "{name} 說了一些關於「{word}」的話"
makeActive: "啟用"
display: "檢視"
copy: "複製"
copiedToClipboard: "已複製到剪貼簿"
metrics: "指標"
overview: "概覽"
logs: "日誌"
@ -711,7 +712,7 @@ useGlobalSetting: "使用全域設定"
useGlobalSettingDesc: "啟用時,將使用帳戶通知設定。停用時,則可以單獨設定。"
other: "其他"
regenerateLoginToken: "重新產生登入權杖"
regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。一般情況下是不需要這樣做的。重新產生後,所有裝置將會被登出。"
regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。通常不需要使用此功能。重新產生後,所有裝置都將被登出。"
theKeywordWhenSearchingForCustomEmoji: "這是搜尋自訂表情符號時的關鍵字"
setMultipleBySeparatingWithSpace: "您可以使用空格分隔多個項目。"
fileIdOrUrl: "檔案 ID 或 URL"
@ -745,7 +746,7 @@ unclip: "解除摘錄"
confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?"
public: "公開"
private: "私密"
i18nInfo: "Misskey 已被志願者們翻譯成各種語言版本。您可以瀏覽 {link} 幫助翻譯。"
i18nInfo: "Misskey 已被志願者們翻譯成各種語言版本。您可以前往 {link} 以協助翻譯。"
manageAccessTokens: "管理存取權杖"
accountInfo: "帳戶資訊"
notesCount: "貼文數量"
@ -765,7 +766,7 @@ driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引"
noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。"
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
lockedAccountInfo: "即使追隨需要核准,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
alwaysMarkSensitive: "預設標記檔案為敏感內容"
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
disableShowingAnimatedImages: "不播放動態圖檔"
@ -781,7 +782,7 @@ useSystemFont: "使用系統預設的字型"
clips: "摘錄"
experimentalFeatures: "實驗中的功能"
experimental: "實驗性"
thisIsExperimentalFeature: "這是實驗性的功能。可能會有變更規格和不能正常動作的可能性。"
thisIsExperimentalFeature: "這是一項實驗性功能,其行為會隨需要進行調整,也可能無法正常運作。"
developer: "開發者"
makeExplorable: "使自己的帳戶更容易被找到"
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。"
@ -1188,7 +1189,7 @@ forYou: "給您"
currentAnnouncements: "最新公告"
pastAnnouncements: "歷史公告"
youHaveUnreadAnnouncements: "有未讀的公告。"
useSecurityKey: "請按照瀏覽器或裝置上的說明來使用安全金鑰或 Passkey。"
useSecurityKey: "請按照瀏覽器或裝置上的說明來使用安全金鑰或通行金鑰。"
replies: "回覆"
renotes: "轉發"
loadReplies: "閱覽回覆"
@ -1205,9 +1206,9 @@ showRenotes: "顯示其他人的轉發貼文"
edited: "已編輯"
notificationRecieveConfig: "接受通知的設定"
mutualFollow: "互相追隨"
followingOrFollower: "追隨中或追隨者"
followingOrFollower: "追隨中或追隨者"
fileAttachedOnly: "只顯示包含附件的貼文"
showRepliesToOthersInTimeline: "顯示給其他人的回覆"
showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆"
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
showRepliesToOthersInTimelineAll: "在時間軸包含追隨中所有人的回覆"
hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回覆"
@ -1247,7 +1248,7 @@ reloadRequiredToApplySettings: "需要重新載入頁面設定才能生效。"
remainingN: "剩餘:{n}"
overwriteContentConfirm: "確定要覆蓋目前的內容嗎?"
seasonalScreenEffect: "隨季節變換畫面的呈現"
decorate: "設置頭像裝飾"
decorate: "裝飾"
addMfmFunction: "插入 MFM 功能語法"
enableQuickAddMfmFunction: "顯示進階 MFM 選擇器"
bubbleGame: "氣泡遊戲"
@ -1274,7 +1275,7 @@ useBackupCode: "使用備用驗證碼"
launchApp: "啟動 APP"
useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊"
keepOriginalFilename: "保留原始檔名"
keepOriginalFilenameDescription: "如果關閉此設,上傳時檔案名稱會自動替換為隨機字串。"
keepOriginalFilenameDescription: "如果關閉此設,上傳時檔案名稱會自動替換為隨機字串。"
noDescription: "沒有說明文字"
alwaysConfirmFollow: "追隨時總是確認"
inquiry: "聯絡我們"
@ -1291,10 +1292,10 @@ performance: "性能"
modified: "已變更"
discard: "取消"
thereAreNChanges: "有 {n} 處的變更"
signinWithPasskey: "使用密碼金鑰登入"
unknownWebAuthnKey: "未註冊的金鑰。"
passkeyVerificationFailed: "驗證金鑰失敗。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。"
signinWithPasskey: "使用通行金鑰登入"
unknownWebAuthnKey: "未註冊的通行金鑰。"
passkeyVerificationFailed: "驗證通行金鑰失敗。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證通行金鑰成功,但是無密碼登入的方式是停用的。"
messageToFollower: "給追隨者的訊息"
target: "目標 "
testCaptchaWarning: "此功能用於 CAPTCHA 的測試。<strong>請勿在正式環境中使用。</strong>"
@ -1309,6 +1310,69 @@ availableRoles: "可用角色"
acknowledgeNotesAndEnable: "了解注意事項後再開啟。"
federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。"
federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。"
confirmOnReact: "反應時確認"
reactAreYouSure: "用「 {emoji} 」反應嗎?"
markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?"
unmarkAsSensitiveConfirm: "要解除這個媒體的敏感設定嗎?"
preferences: "環境設定"
accessibility: "輔助工具"
preferencesProfile: "設定檔案"
copyPreferenceId: "複製設定 ID"
resetToDefaultValue: "還原成預設值"
overrideByAccount: "覆寫帳號"
untitled: "無標題"
noName: "沒有名稱"
skip: "跳過"
restore: "還原"
syncBetweenDevices: "裝置之間的同步化"
preferenceSyncConflictTitle: "伺服器上存在設定值"
preferenceSyncConflictText: "已啟用同步的設定項目會將設定值儲存至伺服器,並已找到該設定項目在伺服器上儲存的設定值。請選擇要使用哪個設定值進行覆寫。"
preferenceSyncConflictChoiceServer: "伺服器設定值"
preferenceSyncConflictChoiceDevice: "裝置的設定值"
preferenceSyncConflictChoiceCancel: "取消啟用同步"
paste: "貼上"
emojiPalette: "表情符號調色盤"
postForm: "發文視窗"
textCount: "字數"
information: "關於"
_emojiPalette:
palettes: "調色盤"
enableSyncBetweenDevicesForPalettes: "啟用裝置與裝置之間的調色盤同步化"
paletteForMain: "主要使用的調色盤"
paletteForReaction: "反應用的調色盤"
_settings:
driveBanner: "您可以管理和設定雲端硬碟、確認使用量,以及調整上傳檔案時的設定。"
pluginBanner: "可使用外掛擴充用戶端的功能。您可以安裝外掛,實施個別的設定與管理。"
notificationsBanner: "您可以設定從伺服器接收通知的類型和範圍,以及推送通知。"
api: "API"
webhook: "Webhook"
serviceConnection: "服務整合"
serviceConnectionBanner: "您可以管理和設定存取權杖與 Webhooks以便與外部應用程式和服務整合。"
accountData: "帳戶資料"
accountDataBanner: "您可以管理帳戶資料的匯出 / 匯入。"
muteAndBlockBanner: "您可以設定和管理要隱藏的內容,並限制特定使用者的行動。"
accessibilityBanner: "可針對客戶端的視覺和行為進行個人化設定,以達到更佳的使用效果。"
privacyBanner: "您可以調整帳戶的隱私設定,例如內容的可見性、尋找內容的容易程度,以及追隨是否需要核准。"
securityBanner: "您可以設定與帳戶安全性相關的設定,例如密碼、登入方式、驗證應用程式和通行金鑰。"
preferencesBanner: "您可以根據喜好設定用戶端的整體行為。"
appearanceBanner: "您可以根據喜好設定與用戶端外觀和顯示方式相關的設定。"
soundsBanner: "您可以調整用戶端播放的聲音設定。"
timelineAndNote: "時間軸及貼文"
makeEveryTextElementsSelectable: "允許選取所有文字"
makeEveryTextElementsSelectable_description: "啟用此功能後,可能會在某些情境下降低可用性。"
_preferencesProfile:
profileName: "設定檔案名稱"
profileNameDescription: "設定一個名稱來識別此裝置。"
profileNameDescription2: "例如:「主要個人電腦」、「智慧型手機」等"
_preferencesBackup:
autoBackup: "自動備份"
restoreFromBackup: "從備份還原"
noBackupsFoundTitle: "找不到備份檔"
noBackupsFoundDescription: "沒有找到自動建立的備份,但如果您手動儲存了備份檔案,則可以匯入並還原。"
selectBackupToRestore: "選擇要還原的備份"
youNeedToNameYourProfileToEnableAutoBackup: "要啟用自動備份,必須設定檔案名稱。"
autoPreferencesBackupIsNotEnabledForThisDevice: "此裝置未啟用自動備份設定。"
backupFound: "找到設定的備份"
_accountSettings:
requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
@ -1319,6 +1383,7 @@ _accountSettings:
makeNotesHiddenBefore: "隱藏過去的貼文"
makeNotesHiddenBeforeDescription: "啟用此功能後,超過設定的日期和時間或超過設定時間的貼文將僅對自己顯示(私密化)。 如果您再次停用它,貼文的公開狀態也會恢復原狀。"
mayNotEffectForFederatedNotes: "聯邦發送至遠端伺服器的貼文可能會不受影響。"
mayNotEffectSomeSituations: "這些限制已經簡化。它們可能不適用於某些情況,例如在遠端伺服器上檢視或管理時。"
notesHavePassedSpecifiedPeriod: "早於指定時間的貼文"
notesOlderThanSpecifiedDateAndTime: "指定時間和日期之前的貼文"
_abuseUserReport:
@ -1433,7 +1498,7 @@ _initialTutorial:
useCases: "伺服器的服務條款可能會規範特定的貼文需要使用隱藏內容,除此之外也會用在隱藏劇情洩漏與敏感內容的貼文。"
_howToMakeAttachmentsSensitive:
title: "如何標記上傳附件為敏感內容?"
description: "如果伺服器服務條款有規範,又或者不希望上傳附件直接被看見,可以設置為「敏感內容」"
description: "如果伺服器的服務條款有規範,又或者不適合直接展示的附件,請記得加上「敏感」標記。"
tryThisFile: "試試看!把附加在發文表單的圖像檔案標記為敏感內容。"
_exampleNote:
note: "打開納豆的包裝失敗了…"
@ -1467,7 +1532,7 @@ _serverSettings:
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
openRegistration: "允許建立帳戶"
openRegistrationWarning: "開放註冊伴隨著風險。 建議只有在伺服器受到持續監控,並準備好在出現問題時能立即處理的情況下才開放註冊。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam如果一段期間內沒有偵測到審查員活動,此設定將自動關閉。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "如果一段期間內沒有偵測到任何審查員活動,此設定將自動關閉,以防止垃圾內容。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名"
@ -1481,7 +1546,7 @@ _accountMigration:
startMigration: "遷移"
migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外請確認在要遷移到的帳戶已經建立了一個別名。"
movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。"
postMigrationNote: "將在完成遷移的 24 小時取消追隨所有帳號。\n此帳戶的追隨中/追隨者人數將歸零。由於不會解除粉絲對您的追隨,因此他們仍然可以繼續閱覽此帳戶僅對追隨者公開的貼文。"
postMigrationNote: "將在完成遷移的 24 小時取消追隨所有帳號。\n此帳戶的追隨中/追隨者人數將歸零。由於不會解除粉絲對您的追隨,因此他們仍然可以繼續閱覽此帳戶僅對追隨者公開的貼文。"
movedTo: "要遷移到的帳戶:"
_achievements:
earnedAt: "獲得日期"
@ -1798,7 +1863,7 @@ _role:
canHideAds: "不顯示廣告"
canSearchNotes: "可否搜尋貼文"
canUseTranslator: "使用翻譯功能"
avatarDecorationLimit: "頭像裝飾的最大設置量"
avatarDecorationLimit: "頭像可掛上的最大裝飾數量"
canImportAntennas: "允許匯入天線"
canImportBlocking: "允許匯入封鎖名單"
canImportFollowing: "允許匯入追隨名單"
@ -1957,7 +2022,7 @@ _instanceMute:
instanceMuteDescription: "包括對被靜音伺服器上的使用者的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。"
instanceMuteDescription2: "設定時以換行進行分隔"
title: "將隱藏被設定的伺服器貼文。"
heading: "將伺服器靜音"
heading: "要靜音的伺服器"
_theme:
explore: "探索佈景主題"
install: "安裝佈景主題"
@ -1967,6 +2032,7 @@ _theme:
installed: "{name}已安裝"
installedThemes: "已經安裝的佈景主題"
builtinThemes: "標準佈景主題"
instanceTheme: "伺服器的主題"
alreadyInstalled: "已安裝此佈景主題"
invalid: "佈景主題格式錯誤"
make: "製作佈景主題"
@ -2076,11 +2142,11 @@ _2fa:
setupCompleted: "設定完成"
step4: "從現在開始,任何登入操作都將要求您提供權杖。"
securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。"
registerTOTPBeforeKey: "如要註冊安全金鑰或 Passkey,請先設定驗證應用程式。"
securityKeyInfo: "您可以設定使用支援 FIDO2 的硬體安全金鑰以及裝置上的生物辨識、PIN 碼和密碼等來登入。"
registerSecurityKey: "註冊安全金鑰或 Passkey"
registerTOTPBeforeKey: "如要註冊安全金鑰或通行金鑰,請先設定驗證應用程式。"
securityKeyInfo: "註冊 WebAuthn 衍生的金鑰,例如支援 FIDO2 的硬體安全金鑰、裝置生物識別、PIN 鎖和通行金鑰。"
registerSecurityKey: "註冊安全金鑰或通行金鑰"
securityKeyName: "輸入金鑰名稱"
tapSecurityKey: "按照瀏覽器的說明註冊安全金鑰或 Passkey。"
tapSecurityKey: "按照瀏覽器的說明註冊安全金鑰或通行金鑰。"
removeKey: "刪除安全金鑰"
removeKeyConfirm: "要刪除{name}嗎?"
whyTOTPOnlyRenew: "如果註冊了安全金鑰,則無法解除驗證應用程式的設定。"
@ -2302,7 +2368,7 @@ _profile:
avatarDecorationMax: "最多可以設置 {max} 個裝飾。"
followedMessage: "被追隨時的訊息"
followedMessageDescription: "可以設定被追隨時顯示給對方的訊息。"
followedMessageDescriptionForLockedAccount: "如果追隨是需要審核的話,在允許追隨請求之後顯示。"
followedMessageDescriptionForLockedAccount: "如果追隨需要核准的話,將在通過追隨請求之後顯示。"
_exportOrImport:
allNotes: "所有貼文"
favoritedNotes: "「我的最愛」貼文"
@ -2408,7 +2474,7 @@ _pages:
note: "嵌式貼文"
_note:
id: "貼文ID"
idDescription: "您也可以粘貼筆記 URL 並進行設置。 "
idDescription: "您也可以貼上貼文 URL 來進行設定。 "
detailed: "顯示詳細內容"
_relayStatus:
requesting: "等待核准"
@ -2422,7 +2488,7 @@ _notification:
youRenoted: "{name} 轉發了你的貼文"
youWereFollowed: "您有新的追隨者"
youReceivedFollowRequest: "您有新的追隨請求"
yourFollowRequestAccepted: "您的追隨請求已通過"
yourFollowRequestAccepted: "您的追隨請求已被核准"
pollEnded: "問卷調查已產生結果"
newNote: "新的貼文"
unreadAntennaNote: "天線 {name}"
@ -2440,6 +2506,8 @@ _notification:
flushNotification: "重置通知歷史紀錄"
exportOfXCompleted: "{x} 的匯出已完成。"
login: "已登入"
createToken: "已產生存取權杖"
createTokenDescription: "如果您不知道,請透過「{text}」刪除存取權杖。"
_types:
all: "全部 "
note: "使用者的最新貼文"
@ -2456,6 +2524,7 @@ _notification:
achievementEarned: "獲得成就"
exportCompleted: "已完成匯出。"
login: "登入"
createToken: "建立存取權杖"
test: "通知測試"
app: "應用程式通知"
_actions:
@ -2483,6 +2552,7 @@ _deck:
useSimpleUiForNonRootPages: "用簡易介面顯示非根頁面"
usedAsMinWidthWhenFlexible: "如果啟用「自動調整寬度」,此為最小寬度"
flexible: "自動調整寬度"
enableSyncBetweenDevicesForProfiles: "啟用裝置與裝置之間的設定檔資料同步化"
_columns:
main: "主列"
widgets: "小工具"
@ -2590,6 +2660,7 @@ _moderationLogTypes:
deletePage: "刪除頁面"
deleteFlash: "刪除 Play"
deleteGalleryPost: "刪除相簿的貼文"
updateProxyAccountDescription: "更新代理帳戶的說明"
_fileViewer:
title: "檔案詳細資訊"
type: "檔案類型 "
@ -2603,10 +2674,8 @@ _externalResourceInstaller:
checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。"
_plugin:
title: "要安裝此外掛嘛?"
metaTitle: "外掛資訊"
_theme:
title: "要安裝此佈景主題嗎?"
metaTitle: "佈景主題資訊"
_meta:
base: "基本配色方案"
_vendorInfo:
@ -2655,7 +2724,7 @@ _dataSaver:
_hemisphere:
N: "北半球"
S: "南半球"
caption: "在某些客戶端的設定中,用於判斷季節。"
caption: "某些客戶端的設定會用此來判斷季節。"
_reversi:
reversi: "黑白棋"
gameSettings: "對弈設定"
@ -2822,8 +2891,6 @@ _remoteLookupErrors:
_responseInvalid:
title: "回應不正確"
description: "雖然能夠與這個伺服器通訊,但是取得的資料不正確。"
_responseInvalidIdHostNotMatch:
description: "輸入的 URI 的網域與最終取得的 URI 的網域不同。 如果您是透過第三方伺服器查詢遠端內容,請使用可在原始伺服器上取得的 URI 再次查詢。"
_noSuchObject:
title: "查無項目"
description: "無法找到所要求的資源,請再次檢查 URI。"
@ -2840,3 +2907,23 @@ _captcha:
_unknown:
title: "CAPTCHA 錯誤"
text: "發生了意外的錯誤。"
_bootErrors:
title: "載入失敗"
serverError: "如果稍等片刻並重新載入後問題仍然存在,請聯絡您的伺服器管理員並提供以下的錯誤 ID。"
solution: "執行以下操作或許可以解決問題。"
solution1: "將瀏覽器和作業系統更新至最新版本"
solution2: "停用廣告攔截器"
solution3: "清除瀏覽器的快取"
solution4: "Tor 瀏覽器)將 dom.webaudio.enabled 設為 true"
otherOption: "其他選項"
otherOption1: "刪除用戶端設定和快取"
otherOption2: "啟動簡易用戶端"
otherOption3: "啟動修復工具"
_search:
searchScopeAll: "全部"
searchScopeLocal: "本地"
searchScopeServer: "指定伺服器"
searchScopeUser: "指定使用者"
pleaseEnterServerHost: "請輸入伺服器的主機名稱"
pleaseSelectUser: "請選擇使用者"
serverHostPlaceholder: "例misskey.example.com"

View file

@ -1,6 +1,6 @@
{
"name": "sharkey",
"version": "2025.3.0-dev",
"version": "2025.3.2-beta.10",
"codename": "shonk",
"repository": {
"type": "git",
@ -24,6 +24,7 @@
"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 --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",
"build-frontend-search-index": "pnpm --filter frontend build-search-index",
"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",
@ -40,7 +41,7 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm -r test",
@ -50,34 +51,35 @@
"cleanall": "pnpm clean-all"
},
"resolutions": {
"chokidar": "3.5.3",
"chokidar": "4.0.3",
"lodash": "4.17.21"
},
"dependencies": {
"cssnano": "6.1.2",
"execa": "8.0.1",
"fast-glob": "3.3.2",
"ignore-walk": "6.0.5",
"cssnano": "7.0.6",
"execa": "9.5.2",
"fast-glob": "3.3.3",
"ignore-walk": "7.0.0",
"js-yaml": "4.1.0",
"postcss": "8.4.49",
"tar": "6.2.1",
"terser": "5.36.0",
"typescript": "5.6.3",
"esbuild": "0.24.0",
"glob": "11.0.0"
"postcss": "8.5.3",
"tar": "7.4.3",
"terser": "5.39.0",
"typescript": "5.8.2",
"esbuild": "0.25.0",
"glob": "11.0.1"
},
"optionalDependencies": {
"cypress": "13.15.2"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.0.3",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "22.13.10",
"@typescript-eslint/eslint-plugin": "8.26.0",
"@typescript-eslint/parser": "8.26.0",
"cross-env": "7.0.3",
"eslint": "9.14.0",
"globals": "15.12.0",
"eslint": "9.22.0",
"globals": "16.0.0",
"ncp": "2.0.0",
"start-server-and-test": "2.0.8"
"pnpm": "10.6.1",
"start-server-and-test": "2.0.10"
}
}

View file

@ -1,5 +1,5 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class GoogleAnalytics1739006797620 {
name = 'GoogleAnalytics1739006797620'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsMeasurementId" character varying(64)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsMeasurementId"`);
}
}

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts1740121393164 {
name = 'SystemAccounts1740121393164'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "system_account" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(256) NOT NULL, CONSTRAINT "PK_edb56f4aaf9ddd50ee556da97ba" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_41a3c87a37aea616ee459369e1" ON "system_account" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c362033aee0ea51011386a5a7e" ON "system_account" ("type") `);
await queryRunner.query(`ALTER TABLE "system_account" ADD CONSTRAINT "FK_41a3c87a37aea616ee459369e12" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor'`);
if (instanceActor.length > 0) {
await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${instanceActor[0].id}', '${instanceActor[0].id}', 'actor')`);
}
const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor'`);
if (relayActor.length > 0) {
await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${relayActor[0].id}', '${relayActor[0].id}', 'relay')`);
}
const meta = await queryRunner.query(`SELECT "proxyAccountId" FROM "meta" ORDER BY "id" DESC LIMIT 1`);
if (!meta && meta.length >= 1 && meta[0].proxyAccountId) {
await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${meta[0].proxyAccountId}', '${meta[0].proxyAccountId}', 'proxy')`);
}
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "system_account" DROP CONSTRAINT "FK_41a3c87a37aea616ee459369e12"`);
await queryRunner.query(`DROP INDEX "public"."IDX_c362033aee0ea51011386a5a7e"`);
await queryRunner.query(`DROP INDEX "public"."IDX_41a3c87a37aea616ee459369e1"`);
await queryRunner.query(`DROP TABLE "system_account"`);
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts21740129169650 {
name = 'SystemAccounts21740129169650'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`);
const proxyAccountId = await queryRunner.query(`SELECT "userId" FROM "system_account" WHERE "type" = 'proxy' ORDER BY "id" DESC LIMIT 1`);
if (proxyAccountId && proxyAccountId.length >= 1) {
await queryRunner.query(`UPDATE "meta" SET "proxyAccountId" = '${proxyAccountId[0].userId}'`);
}
await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
}
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts31740133121105 {
name = 'SystemAccounts31740133121105'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "rootUserId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc" FOREIGN KEY ("rootUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
const users = await queryRunner.query(`SELECT "id" FROM "user" WHERE "isRoot" = true LIMIT 1`);
if (users.length > 0) {
await queryRunner.query(`UPDATE "meta" SET "rootUserId" = $1`, [users[0].id]);
}
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_c80e4079d632f95eac06a9d28cc"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "rootUserId"`);
}
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts41740993126937 {
name = 'SystemAccounts41740993126937'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isRoot"`);
}
async down(queryRunner) {
// down 実行時は isRoot = true のユーザーが存在しなくなるため手動で対応する必要あり
await queryRunner.query(`ALTER TABLE "user" ADD "isRoot" boolean NOT NULL DEFAULT false`);
}
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SystemAccounts1741279404074 {
name = 'SystemAccounts1741279404074'
async up(queryRunner) {
const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'actor')`);
if (instanceActor.length > 0) {
console.warn('instance.actor was incorrect, updating...');
await queryRunner.query(`UPDATE "system_account" SET "id" = '${instanceActor[0].id}', "userId" = '${instanceActor[0].id}' WHERE "type" = 'actor'`);
}
const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'relay')`);
if (relayActor.length > 0) {
console.warn('relay.actor was incorrect, updating...');
await queryRunner.query(`UPDATE "system_account" SET "id" = '${relayActor[0].id}', "userId" = '${relayActor[0].id}' WHERE "type" = 'relay'`);
}
}
async down(queryRunner) {
// fixup migration, no down migration
}
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UserFeaturedFixup1741424411879 {
name = 'UserFeaturedFixup1741424411879'
async up(queryRunner) {
await queryRunner.query(`CREATE OR REPLACE FUNCTION pg_temp.extract_ap_id(text) RETURNS text AS $$
SELECT
CASE
WHEN $1 ~ '^https?://' THEN $1
WHEN $1 LIKE '{%' THEN COALESCE(jsonb_extract_path_text($1::jsonb, 'id'), null)
ELSE null
END;
$$ LANGUAGE sql IMMUTABLE;`);
// "host" is NOT NULL is not needed but just in case add it to prevent overwriting irreplaceable data
await queryRunner.query(`UPDATE "user" SET "featured" = pg_temp.extract_ap_id("featured") WHERE "host" IS NOT NULL`);
}
async down(queryRunner) {
// fixup migration, no down migration
}
}

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat1742203321812 {
name = 'Chat1742203321812'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_room" ("id" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "ownerId" character varying(32) NOT NULL, CONSTRAINT "PK_8aa3a52cf74c96469f0ef9fbe3e" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_f0d8ad64243fa2ca2800da0dfd" ON "chat_room" ("ownerId") `);
await queryRunner.query(`CREATE TABLE "chat_message" ("id" character varying(32) NOT NULL, "fromUserId" character varying(32) NOT NULL, "toUserId" character varying(32), "toRoomId" character varying(32), "text" character varying(4096), "uri" character varying(512), "reads" character varying(32) array NOT NULL DEFAULT '{}', "fileId" character varying(32), "reactions" character varying(1024) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_3cc0d85193aade457d3077dd06b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_79a26e7a4d9afa5e4fc05f134e" ON "chat_message" ("fromUserId") `);
await queryRunner.query(`CREATE INDEX "IDX_25e097b51d7622c249452c6f75" ON "chat_message" ("toUserId") `);
await queryRunner.query(`CREATE INDEX "IDX_f006b8a76efd1abf9f221c175c" ON "chat_message" ("toRoomId") `);
await queryRunner.query(`CREATE TABLE "chat_room_membership" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_2bd59c741e571b283c048beb69a" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_d99c5279460fb77ef58c596ce5" ON "chat_room_membership" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_c25143ebab714e930aeca1c0e8" ON "chat_room_membership" ("roomId") `);
await queryRunner.query(`ALTER TABLE "chat_room" ADD CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed" FOREIGN KEY ("fromUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_25e097b51d7622c249452c6f757" FOREIGN KEY ("toUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce" FOREIGN KEY ("toRoomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_d99c5279460fb77ef58c596ce51" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d"`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_d99c5279460fb77ef58c596ce51"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_25e097b51d7622c249452c6f757"`);
await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed"`);
await queryRunner.query(`ALTER TABLE "chat_room" DROP CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6"`);
await queryRunner.query(`DROP INDEX "public"."IDX_c25143ebab714e930aeca1c0e8"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d99c5279460fb77ef58c596ce5"`);
await queryRunner.query(`DROP TABLE "chat_room_membership"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f006b8a76efd1abf9f221c175c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_25e097b51d7622c249452c6f75"`);
await queryRunner.query(`DROP INDEX "public"."IDX_79a26e7a4d9afa5e4fc05f134e"`);
await queryRunner.query(`DROP TABLE "chat_message"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f0d8ad64243fa2ca2800da0dfd"`);
await queryRunner.query(`DROP TABLE "chat_room"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat21742608337548 {
name = 'Chat21742608337548'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "chatScope" character varying(128) NOT NULL DEFAULT 'mutual'`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_185b6b5afa707b5d36d1ce3144" ON "chat_room_membership" ("userId", "roomId") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_185b6b5afa707b5d36d1ce3144"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "chatScope"`);
}
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat31742617546147 {
name = 'Chat31742617546147'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `);
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`);
await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`);
await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`);
await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`);
await queryRunner.query(`DROP TABLE "chat_approval"`);
}
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat41742707840715 {
name = 'Chat41742707840715'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "chat_room_invitation" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_9d489521a312dd28225672de2dc" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_8552bb38e7ed038c5bdd398a38" ON "chat_room_invitation" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_5f265075b215fc390a57523b12" ON "chat_room_invitation" ("roomId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044f2a7962b8ee5bbfaa02e8a3" ON "chat_room_invitation" ("userId", "roomId") `);
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_5f265075b215fc390a57523b12a" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_5f265075b215fc390a57523b12a"`);
await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384"`);
await queryRunner.query(`DROP INDEX "public"."IDX_044f2a7962b8ee5bbfaa02e8a3"`);
await queryRunner.query(`DROP INDEX "public"."IDX_5f265075b215fc390a57523b12"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8552bb38e7ed038c5bdd398a38"`);
await queryRunner.query(`DROP TABLE "chat_room_invitation"`);
}
}

View file

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

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Chat61742795111958 {
name = 'Chat61742795111958'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_room" ADD "description" character varying(2048) NOT NULL DEFAULT ''`);
await queryRunner.query(`ALTER TABLE "chat_room" ADD "isArchived" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD "isMuted" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP COLUMN "isMuted"`);
await queryRunner.query(`ALTER TABLE "chat_room" DROP COLUMN "isArchived"`);
await queryRunner.query(`ALTER TABLE "chat_room" DROP COLUMN "description"`);
}
}

View file

@ -37,18 +37,18 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.3.56",
"@swc/core-darwin-x64": "1.3.56",
"@swc/core-darwin-arm64": "1.11.11",
"@swc/core-darwin-x64": "1.11.11",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.3.56",
"@swc/core-linux-arm64-gnu": "1.3.56",
"@swc/core-linux-arm64-musl": "1.3.56",
"@swc/core-linux-x64-gnu": "1.3.56",
"@swc/core-linux-x64-musl": "1.3.56",
"@swc/core-win32-arm64-msvc": "1.3.56",
"@swc/core-win32-ia32-msvc": "1.3.56",
"@swc/core-win32-x64-msvc": "1.3.56",
"bufferutil": "4.0.7",
"@swc/core-linux-arm-gnueabihf": "1.11.11",
"@swc/core-linux-arm64-gnu": "1.11.11",
"@swc/core-linux-arm64-musl": "1.11.11",
"@swc/core-linux-x64-gnu": "1.11.11",
"@swc/core-linux-x64-musl": "1.11.11",
"@swc/core-win32-arm64-msvc": "1.11.11",
"@swc/core-win32-ia32-msvc": "1.11.11",
"@swc/core-win32-x64-msvc": "1.11.11",
"bufferutil": "4.0.9",
"slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10",
"slacc-darwin-arm64": "0.0.10",
@ -62,36 +62,33 @@
"slacc-linux-x64-musl": "0.0.10",
"slacc-win32-arm64-msvc": "0.0.10",
"slacc-win32-x64-msvc": "0.0.10",
"utf-8-validate": "6.0.3"
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "6.5.0",
"@bull-board/fastify": "6.5.0",
"@bull-board/ui": "6.5.0",
"@aws-sdk/client-s3": "3.772.0",
"@aws-sdk/lib-storage": "3.772.0",
"@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.1",
"@fastify/cookie": "11.0.1",
"@fastify/cors": "10.0.1",
"@fastify/express": "4.0.1",
"@fastify/http-proxy": "10.0.1",
"@fastify/multipart": "9.0.1",
"@fastify/static": "8.0.2",
"@fastify/view": "10.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2",
"@fastify/multipart": "9.0.3",
"@fastify/static": "8.1.1",
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7",
"@nestjs/testing": "10.4.7",
"@misskey-dev/summaly": "5.2.0",
"@nestjs/common": "11.0.12",
"@nestjs/core": "11.0.12",
"@nestjs/testing": "11.0.12",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.38.0",
"@sentry/profiling-node": "8.38.0",
"@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@sentry/node": "8.55.0",
"@sentry/profiling-node": "8.55.0",
"@simplewebauthn/server": "12.0.0",
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12",
"@swc/core": "1.9.2",
"@swc/cli": "0.6.0",
"@swc/core": "1.11.11",
"@transfem-org/sfm-js": "0.24.6",
"@twemoji/parser": "15.1.1",
"@types/psl": "^1.1.3",
@ -103,11 +100,11 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.26.1",
"bullmq": "5.44.1",
"cacheable-lookup": "7.0.0",
"canvas": "^3.1.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
"chalk": "5.4.1",
"chalk-template": "1.1.0",
"chokidar": "3.6.0",
"cli-highlight": "2.1.11",
@ -116,48 +113,48 @@
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fast-xml-parser": "4.4.1",
"fastify": "5.0.0",
"fastify": "5.2.1",
"fastify-multer": "^2.0.3",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.1",
"form-data": "4.0.2",
"glob": "11.0.0",
"got": "14.4.4",
"happy-dom": "15.11.4",
"got": "14.4.6",
"happy-dom": "16.8.1",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.4.1",
"ioredis": "5.6.0",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0",
"is-svg": "5.1.0",
"js-yaml": "4.1.0",
"jsdom": "24.1.1",
"jsdom": "26.0.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsonld": "8.3.3",
"jsrsasign": "11.1.0",
"juice": "11.0.0",
"juice": "11.0.1",
"megalodon": "workspace:*",
"meilisearch": "0.45.0",
"meilisearch": "0.49.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"moment": "^2.30.1",
"ms": "3.0.0-canary.1",
"nanoid": "5.0.8",
"nanoid": "5.1.5",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.9.16",
"oauth": "0.10.0",
"nodemailer": "6.10.0",
"oauth": "0.10.2",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.3.4",
"otpauth": "9.3.6",
"parse5": "7.2.1",
"pg": "8.13.1",
"pg": "8.14.1",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -172,31 +169,31 @@
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
"sanitize-html": "2.13.1",
"secure-json-parse": "2.7.0",
"rxjs": "7.8.2",
"sanitize-html": "2.15.0",
"secure-json-parse": "3.0.2",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.23.5",
"systeminformation": "5.25.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",
"tsc-alias": "1.8.10",
"tsc-alias": "1.8.11",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.6.3",
"ulid": "2.3.0",
"typeorm": "0.3.21",
"typescript": "5.8.2",
"ulid": "2.4.0",
"uuid": "^9.0.1",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.18.0",
"ws": "8.18.1",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.7",
"@simplewebauthn/types": "10.0.0",
"@nestjs/platform-express": "10.4.15",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.37",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.3",
@ -211,15 +208,15 @@
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.14",
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.9.0",
"@types/nodemailer": "6.4.16",
"@types/node": "22.13.10",
"@types/nodemailer": "6.4.17",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.10",
"@types/pg": "8.11.11",
"@types/proxy-addr": "^2.0.3",
"@types/pug": "2.0.10",
"@types/qrcode": "1.5.5",
@ -235,18 +232,18 @@
"@types/uuid": "^9.0.4",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.0.1",
"@types/ws": "8.18.0",
"@typescript-eslint/eslint-plugin": "8.27.0",
"@typescript-eslint/parser": "8.27.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"execa": "8.0.1",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.7",
"pid-port": "1.0.0",
"nodemon": "3.1.9",
"pid-port": "1.0.2",
"simple-oauth2": "5.1.0"
}
}

View file

@ -141,7 +141,7 @@ const $meta: Provider = {
for (const key in body.after) {
(meta as any)[key] = (body.after as any)[key];
}
meta.proxyAccount = null; // joinなカラムは通常取ってこないので
meta.rootUser = null; // joinなカラムは通常取ってこないので
break;
}
default:

View file

@ -77,6 +77,7 @@ type Source = {
proxyBypassHosts?: string[];
allowedPrivateNetworks?: string[];
disallowExternalApRedirect?: boolean;
maxFileSize?: number;
maxNoteLength?: number;
@ -126,8 +127,8 @@ type Source = {
logging?: {
sql?: {
disableQueryTruncation? : boolean,
enableQueryParamLogging? : boolean,
disableQueryTruncation?: boolean,
enableQueryParamLogging?: boolean,
}
}
@ -179,6 +180,7 @@ export type Config = {
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
disallowExternalApRedirect: boolean;
maxFileSize: number;
maxNoteLength: number;
maxRemoteNoteLength: number;
@ -206,8 +208,8 @@ export type Config = {
checkActivityPubGetSignature: boolean | undefined;
logging?: {
sql?: {
disableQueryTruncation? : boolean,
enableQueryParamLogging? : boolean,
disableQueryTruncation?: boolean,
enableQueryParamLogging?: boolean,
}
}
@ -360,6 +362,7 @@ export function loadConfig(): Config {
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
disallowExternalApRedirect: config.disallowExternalApRedirect ?? false,
maxFileSize: config.maxFileSize ?? 262144000,
maxNoteLength: config.maxNoteLength ?? 3000,
maxRemoteNoteLength: config.maxRemoteNoteLength ?? 100000,

View file

@ -10,9 +10,9 @@ 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 { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdService } from './IdService.js';
@Injectable()
@ -27,7 +27,7 @@ export class AbuseReportService {
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private systemAccountService: SystemAccountService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) {
@ -136,7 +136,7 @@ export class AbuseReportService {
forwarded: true,
});
const actor = await this.instanceActorService.getInstanceActor();
const actor = await this.systemAccountService.fetch('actor');
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);

View file

@ -20,10 +20,10 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable()
export class AccountMoveService {
@ -58,12 +58,12 @@ export class AccountMoveService {
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private perUserFollowingChart: PerUserFollowingChart,
private federatedInstanceService: FederatedInstanceService,
private instanceChart: InstanceChart,
private relayService: RelayService,
private queueService: QueueService,
private systemAccountService: SystemAccountService,
) {
}
@ -130,11 +130,11 @@ export class AccountMoveService {
}
// follow the new account
const proxy = await this.proxyAccountService.fetch();
const proxy = await this.systemAccountService.fetch('proxy');
const followings = await this.followingsRepository.findBy({
followeeId: src.id,
followerHost: IsNull(), // follower is local
followerId: proxy ? Not(proxy.id) : undefined,
followerId: Not(proxy.id),
});
const followJobs = followings.map(following => ({
from: { id: following.followerId },
@ -269,10 +269,8 @@ export class AccountMoveService {
// Have the proxy account follow the new account in the same way as UserListService.push
if (this.userEntityService.isRemoteUser(dst)) {
const proxy = await this.proxyAccountService.fetch();
if (proxy) {
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
}
const proxy = await this.systemAccountService.fetch('proxy');
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
}
}

View file

@ -47,7 +47,7 @@ export type CaptchaSetting = {
siteKey: string | null;
secretKey: string | null;
}
}
};
export class CaptchaError extends Error {
public readonly code: CaptchaErrorCode;
@ -63,11 +63,11 @@ export class CaptchaError extends Error {
export type CaptchaSaveSuccess = {
success: true;
}
};
export type CaptchaSaveFailure = {
success: false;
error: CaptchaError;
}
};
export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure;
type CaptchaResponse = {

View file

@ -0,0 +1,780 @@
/*
* 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 { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { QueueService } from '@/core/QueueService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js';
import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomInvitationsRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiChatRoomMembership, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { QueryService } from '@/core/QueryService.js';
import { RoleService } from '@/core/RoleService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
import { Packed } from '@/misc/json-schema.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { emojiRegex } from '@/misc/emoji-regex.js';
const MAX_ROOM_MEMBERS = 30;
const MAX_REACTIONS_PER_MESSAGE = 100;
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
@Injectable()
export class ChatService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.chatMessagesRepository)
private chatMessagesRepository: ChatMessagesRepository,
@Inject(DI.chatApprovalsRepository)
private chatApprovalsRepository: ChatApprovalsRepository,
@Inject(DI.chatRoomsRepository)
private chatRoomsRepository: ChatRoomsRepository,
@Inject(DI.chatRoomInvitationsRepository)
private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
@Inject(DI.chatRoomMembershipsRepository)
private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userEntityService: UserEntityService,
private chatEntityService: ChatEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private queueService: QueueService,
private pushNotificationService: PushNotificationService,
private userBlockingService: UserBlockingService,
private queryService: QueryService,
private roleService: RoleService,
private userFollowingService: UserFollowingService,
private customEmojiService: CustomEmojiService,
) {
}
@bindThis
public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
text?: string | null;
file?: MiDriveFile | null;
uri?: string | null;
}): Promise<Packed<'ChatMessageLite'>> {
if (fromUser.id === toUser.id) {
throw new Error('yourself');
}
const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval')
.where(new Brackets(qb => { // 自分が相手を許可しているか
qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id })
.andWhere('approval.otherId = :toUserId', { toUserId: toUser.id });
}))
.orWhere(new Brackets(qb => { // 相手が自分を許可しているか
qb.where('approval.userId = :toUserId', { toUserId: toUser.id })
.andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id });
}))
.take(2)
.getMany();
const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id);
const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id);
if (!otherApprovedMe) {
if (toUser.chatScope === 'none') {
throw new Error('recipient is cannot chat (none)');
} else if (toUser.chatScope === 'followers') {
const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id);
if (!isFollower) {
throw new Error('recipient is cannot chat (followers)');
}
} else if (toUser.chatScope === 'following') {
const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id);
if (!isFollowing) {
throw new Error('recipient is cannot chat (following)');
}
} else if (toUser.chatScope === 'mutual') {
const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id);
if (!isMutual) {
throw new Error('recipient is cannot chat (mutual)');
}
}
}
if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
throw new Error('recipient is cannot chat (policy)');
}
const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id);
if (blocked) {
throw new Error('blocked');
}
const message = {
id: this.idService.gen(),
fromUserId: fromUser.id,
toUserId: toUser.id,
text: params.text ? params.text.trim() : null,
fileId: params.file ? params.file.id : null,
reads: [],
uri: params.uri ?? null,
} satisfies Partial<MiChatMessage>;
const inserted = await this.chatMessagesRepository.insertOne(message);
// 相手を許可しておく
if (!iApprovedOther) {
this.chatApprovalsRepository.insertOne({
id: this.idService.gen(),
userId: fromUser.id,
otherId: toUser.id,
});
}
const packedMessage = await this.chatEntityService.packMessageLiteFor1on1(inserted);
if (this.userEntityService.isLocalUser(toUser)) {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.set(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`, message.id);
redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`);
redisPipeline.exec();
}
if (this.userEntityService.isLocalUser(fromUser)) {
// 自分のストリーム
this.globalEventService.publishChatUserStream(fromUser.id, toUser.id, 'message', packedMessage);
}
if (this.userEntityService.isLocalUser(toUser)) {
// 相手のストリーム
this.globalEventService.publishChatUserStream(toUser.id, fromUser.id, 'message', packedMessage);
}
// 3秒経っても既読にならなかったらイベント発行
if (this.userEntityService.isLocalUser(toUser)) {
setTimeout(async () => {
const marker = await this.redisClient.get(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`);
if (marker == null) return; // 既読
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser);
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
//this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
}, 3000);
}
return packedMessage;
}
@bindThis
public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: {
text?: string | null;
file?: MiDriveFile | null;
uri?: string | null;
}): Promise<Packed<'ChatMessageLite'>> {
const memberships = await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id });
if (toRoom.ownerId !== fromUser.id && !memberships.some(member => member.userId === fromUser.id)) {
throw new Error('you are not a member of the room');
}
const message = {
id: this.idService.gen(),
fromUserId: fromUser.id,
toRoomId: toRoom.id,
text: params.text ? params.text.trim() : null,
fileId: params.file ? params.file.id : null,
reads: [],
uri: params.uri ?? null,
} satisfies Partial<MiChatMessage>;
const inserted = await this.chatMessagesRepository.insertOne(message);
const packedMessage = await this.chatEntityService.packMessageLiteForRoom(inserted);
this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage);
const redisPipeline = this.redisClient.pipeline();
for (const membership of memberships) {
if (membership.isMuted) continue;
redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id);
redisPipeline.sadd(`newChatMessagesExists:${membership.userId}`, `room:${toRoom.id}`);
}
redisPipeline.exec();
// 3秒経っても既読にならなかったらイベント発行
setTimeout(async () => {
const redisPipeline = this.redisClient.pipeline();
for (const membership of memberships) {
redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`);
}
const markers = await redisPipeline.exec();
if (markers == null) throw new Error('redis error');
if (markers.every(marker => marker[1] == null)) return;
const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted);
for (let i = 0; i < memberships.length; i++) {
const marker = markers[i][1];
if (marker == null) continue;
this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo);
//this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo);
}
}, 3000);
return packedMessage;
}
@bindThis
public async readUserChatMessage(
readerId: MiUser['id'],
senderId: MiUser['id'],
): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.del(`newUserChatMessageExists:${readerId}:${senderId}`);
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`);
await redisPipeline.exec();
}
@bindThis
public async readRoomChatMessage(
readerId: MiUser['id'],
roomId: MiChatRoom['id'],
): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.del(`newRoomChatMessageExists:${readerId}:${roomId}`);
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `room:${roomId}`);
await redisPipeline.exec();
}
@bindThis
public findMessageById(messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId });
}
@bindThis
public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId });
}
@bindThis
public async deleteMessage(message: MiChatMessage) {
await this.chatMessagesRepository.delete(message.id);
if (message.toUserId) {
const [fromUser, toUser] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: message.fromUserId }),
this.usersRepository.findOneByOrFail({ id: message.toUserId }),
]);
if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatUserStream(message.toUserId, message.fromUserId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
//const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser));
//this.queueService.deliver(fromUser, activity, toUser.inbox);
}
} else if (message.toRoomId) {
this.globalEventService.publishChatRoomStream(message.toRoomId, 'deleted', message.id);
}
}
@bindThis
public async userTimeline(meId: MiUser['id'], otherId: MiUser['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb => {
qb
.where('message.fromUserId = :meId')
.andWhere('message.toUserId = :otherId');
}))
.orWhere(new Brackets(qb => {
qb
.where('message.fromUserId = :otherId')
.andWhere('message.toUserId = :meId');
}));
}))
.setParameter('meId', meId)
.setParameter('otherId', otherId);
const messages = await query.take(limit).getMany();
return messages;
}
@bindThis
public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
.where('message.toRoomId = :roomId', { roomId })
.leftJoinAndSelect('message.file', 'file')
.leftJoinAndSelect('message.fromUser', 'fromUser');
const messages = await query.take(limit).getMany();
return messages;
}
@bindThis
public async userHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
const history: MiChatMessage[] = [];
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: meId });
for (let i = 0; i < limit; i++) {
const found = history.map(m => (m.fromUserId === meId) ? m.toUserId! : m.fromUserId!);
const query = this.chatMessagesRepository.createQueryBuilder('message')
.orderBy('message.id', 'DESC')
.where(new Brackets(qb => {
qb
.where('message.fromUserId = :meId', { meId: meId })
.orWhere('message.toUserId = :meId', { meId: meId });
}))
.andWhere('message.toRoomId IS NULL')
.andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`);
if (found.length > 0) {
query.andWhere('message.fromUserId NOT IN (:...found)', { found: found });
query.andWhere('message.toUserId NOT IN (:...found)', { found: found });
}
query.setParameters(mutingQuery.getParameters());
const message = await query.getOne();
if (message) {
history.push(message);
} else {
break;
}
}
return history;
}
@bindThis
public async roomHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
// TODO: 一回のクエリにまとめられるかも
const [memberRoomIds, ownedRoomIds] = await Promise.all([
this.chatRoomMembershipsRepository.findBy({
userId: meId,
}).then(xs => xs.map(x => x.roomId)),
this.chatRoomsRepository.findBy({
ownerId: meId,
}).then(xs => xs.map(x => x.id)),
]);
const roomIds = memberRoomIds.concat(ownedRoomIds);
if (memberRoomIds.length === 0 && ownedRoomIds.length === 0) {
return [];
}
const history: MiChatMessage[] = [];
for (let i = 0; i < limit; i++) {
const found = history.map(m => m.toRoomId!);
const query = this.chatMessagesRepository.createQueryBuilder('message')
.orderBy('message.id', 'DESC')
.where('message.toRoomId IN (:...roomIds)', { roomIds });
if (found.length > 0) {
query.andWhere('message.toRoomId NOT IN (:...found)', { found: found });
}
const message = await query.getOne();
if (message) {
history.push(message);
} else {
break;
}
}
return history;
}
@bindThis
public async getUserReadStateMap(userId: MiUser['id'], otherIds: MiUser['id'][]) {
const readStateMap: Record<MiUser['id'], boolean> = {};
const redisPipeline = this.redisClient.pipeline();
for (const otherId of otherIds) {
redisPipeline.get(`newUserChatMessageExists:${userId}:${otherId}`);
}
const markers = await redisPipeline.exec();
if (markers == null) throw new Error('redis error');
for (let i = 0; i < otherIds.length; i++) {
const marker = markers[i][1];
readStateMap[otherIds[i]] = marker == null;
}
return readStateMap;
}
@bindThis
public async getRoomReadStateMap(userId: MiUser['id'], roomIds: MiChatRoom['id'][]) {
const readStateMap: Record<MiChatRoom['id'], boolean> = {};
const redisPipeline = this.redisClient.pipeline();
for (const roomId of roomIds) {
redisPipeline.get(`newRoomChatMessageExists:${userId}:${roomId}`);
}
const markers = await redisPipeline.exec();
if (markers == null) throw new Error('redis error');
for (let i = 0; i < roomIds.length; i++) {
const marker = markers[i][1];
readStateMap[roomIds[i]] = marker == null;
}
return readStateMap;
}
@bindThis
public async hasUnreadMessages(userId: MiUser['id']) {
const card = await this.redisClient.scard(`newChatMessagesExists:${userId}`);
return card > 0;
}
@bindThis
public async createRoom(owner: MiUser, params: Partial<{
name: string;
description: string;
}>) {
const room = {
id: this.idService.gen(),
name: params.name,
description: params.description,
ownerId: owner.id,
} satisfies Partial<MiChatRoom>;
const created = await this.chatRoomsRepository.insertOne(room);
return created;
}
@bindThis
public async deleteRoom(room: MiChatRoom) {
await this.chatRoomsRepository.delete(room.id);
}
@bindThis
public async findMyRoomById(ownerId: MiUser['id'], roomId: MiChatRoom['id']) {
return this.chatRoomsRepository.findOneBy({ id: roomId, ownerId: ownerId });
}
@bindThis
public async findRoomById(roomId: MiChatRoom['id']) {
return this.chatRoomsRepository.findOne({ where: { id: roomId }, relations: ['owner'] });
}
@bindThis
public async isRoomMember(room: MiChatRoom, userId: MiUser['id']) {
if (room.ownerId === userId) return true;
const membership = await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId });
return membership != null;
}
@bindThis
public async createRoomInvitation(inviterId: MiUser['id'], roomId: MiChatRoom['id'], inviteeId: MiUser['id']) {
if (inviterId === inviteeId) {
throw new Error('yourself');
}
const room = await this.chatRoomsRepository.findOneByOrFail({ id: roomId, ownerId: inviterId });
if (await this.isRoomMember(room, inviteeId)) {
throw new Error('already member');
}
const existingInvitation = await this.chatRoomInvitationsRepository.findOneBy({ roomId, userId: inviteeId });
if (existingInvitation) {
throw new Error('already invited');
}
const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
if (membershipsCount >= MAX_ROOM_MEMBERS) {
throw new Error('room is full');
}
// TODO: cehck block
const invitation = {
id: this.idService.gen(),
roomId: room.id,
userId: inviteeId,
} satisfies Partial<MiChatRoomInvitation>;
const created = await this.chatRoomInvitationsRepository.insertOne(invitation);
return created;
}
@bindThis
public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
.where('room.ownerId = :ownerId', { ownerId });
const rooms = await query.take(limit).getMany();
return rooms;
}
@bindThis
public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId)
.where('invitation.userId = :userId', { userId })
.andWhere('invitation.ignored = FALSE');
const invitations = await query.take(limit).getMany();
return invitations;
}
@bindThis
public async joinToRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
if (membershipsCount >= MAX_ROOM_MEMBERS) {
throw new Error('room is full');
}
const membership = {
id: this.idService.gen(),
roomId: roomId,
userId: userId,
} satisfies Partial<MiChatRoomMembership>;
// TODO: transaction
await this.chatRoomMembershipsRepository.insertOne(membership);
await this.chatRoomInvitationsRepository.delete(invitation.id);
}
@bindThis
public async ignoreRoomInvitation(userId: MiUser['id'], roomId: MiChatRoom['id']) {
const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
await this.chatRoomInvitationsRepository.update(invitation.id, { ignored: true });
}
@bindThis
public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
await this.chatRoomMembershipsRepository.delete(membership.id);
}
@bindThis
public async muteRoom(userId: MiUser['id'], roomId: MiChatRoom['id'], mute: boolean) {
const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
await this.chatRoomMembershipsRepository.update(membership.id, { isMuted: mute });
}
@bindThis
public async updateRoom(room: MiChatRoom, params: {
name?: string;
description?: string;
}): Promise<MiChatRoom> {
return this.chatRoomsRepository.createQueryBuilder().update()
.set(params)
.where('id = :id', { id: room.id })
.returning('*')
.execute()
.then((response) => {
return response.raw[0];
});
}
@bindThis
public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
.where('membership.roomId = :roomId', { roomId });
const memberships = await query.take(limit).getMany();
return memberships;
}
@bindThis
public async searchMessages(meId: MiUser['id'], query: string, limit: number, params: {
userId?: MiUser['id'] | null;
roomId?: MiChatRoom['id'] | null;
}) {
const q = this.chatMessagesRepository.createQueryBuilder('message');
if (params.userId) {
q.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb => {
qb
.where('message.fromUserId = :meId')
.andWhere('message.toUserId = :otherId');
}))
.orWhere(new Brackets(qb => {
qb
.where('message.fromUserId = :otherId')
.andWhere('message.toUserId = :meId');
}));
}))
.setParameter('meId', meId)
.setParameter('otherId', params.userId);
} else if (params.roomId) {
q.where('message.toRoomId = :roomId', { roomId: params.roomId });
} else {
const membershipsQuery = this.chatRoomMembershipsRepository.createQueryBuilder('membership')
.select('membership.roomId')
.where('membership.userId = :meId', { meId: meId });
const ownedRoomsQuery = this.chatRoomsRepository.createQueryBuilder('room')
.select('room.id')
.where('room.ownerId = :meId', { meId });
q.andWhere(new Brackets(qb => {
qb
.where('message.fromUserId = :meId')
.orWhere('message.toUserId = :meId')
.orWhere(`message.toRoomId IN (${membershipsQuery.getQuery()})`)
.orWhere(`message.toRoomId IN (${ownedRoomsQuery.getQuery()})`);
}));
q.setParameters(membershipsQuery.getParameters());
q.setParameters(ownedRoomsQuery.getParameters());
}
q.andWhere('LOWER(message.text) LIKE :q', { q: `%${ sqlLikeEscape(query.toLowerCase()) }%` });
q.leftJoinAndSelect('message.file', 'file');
q.leftJoinAndSelect('message.fromUser', 'fromUser');
q.leftJoinAndSelect('message.toUser', 'toUser');
q.leftJoinAndSelect('message.toRoom', 'toRoom');
q.leftJoinAndSelect('toRoom.owner', 'toRoomOwner');
const messages = await q.orderBy('message.id', 'DESC').take(limit).getMany();
return messages;
}
@bindThis
public async react(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) {
let reaction;
// TODO: ReactionServiceのやつと共通化
function normalize(x: string) {
const match = emojiRegex.exec(x);
if (match) {
// 合字を含む1つの絵文字
const unicode = match[0];
// 異体字セレクタ除去
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
} else {
throw new Error('invalid emoji');
}
}
const custom = reaction_.match(isCustomEmojiRegexp);
if (custom == null) {
reaction = normalize(reaction_);
} else {
const name = custom[1];
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
if (emoji == null) {
throw new Error('no such emoji');
} else {
reaction = `:${name}:`;
}
}
const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId });
if (message.fromUserId === userId) {
throw new Error('cannot react to own message');
}
if (message.toRoomId === null && message.toUserId !== userId) {
throw new Error('cannot react to others message');
}
if (message.reactions.length >= MAX_REACTIONS_PER_MESSAGE) {
throw new Error('too many reactions');
}
const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null;
if (room) {
if (!await this.isRoomMember(room, userId)) {
throw new Error('cannot react to others message');
}
}
await this.chatMessagesRepository.createQueryBuilder().update()
.set({
reactions: () => `array_append("reactions", '${userId}/${reaction}')`,
})
.where('id = :id', { id: message.id })
.execute();
if (room) {
this.globalEventService.publishChatRoomStream(room.id, 'react', {
messageId: message.id,
user: await this.userEntityService.pack(userId),
reaction,
});
} else {
this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'react', {
messageId: message.id,
reaction,
});
this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'react', {
messageId: message.id,
reaction,
});
}
}
@bindThis
public async getMyMemberships(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
.where('membership.userId = :userId', { userId });
const memberships = await query.take(limit).getMany();
return memberships;
}
}

View file

@ -28,7 +28,6 @@ import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js';
import { AvatarDecorationService } from './AvatarDecorationService.js';
import { CaptchaService } from './CaptchaService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js';
import { DeleteAccountService } from './DeleteAccountService.js';
import { DownloadService } from './DownloadService.js';
@ -41,7 +40,7 @@ import { HashtagService } from './HashtagService.js';
import { HttpRequestService } from './HttpRequestService.js';
import { IdService } from './IdService.js';
import { ImageProcessingService } from './ImageProcessingService.js';
import { InstanceActorService } from './InstanceActorService.js';
import { SystemAccountService } from './SystemAccountService.js';
import { InternalStorageService } from './InternalStorageService.js';
import { MetaService } from './MetaService.js';
import { MfmService } from './MfmService.js';
@ -51,7 +50,6 @@ import { NoteEditService } from './NoteEditService.js';
import { NoteDeleteService } from './NoteDeleteService.js';
import { LatestNoteService } from './LatestNoteService.js';
import { NotePiningService } from './NotePiningService.js';
import { NoteReadService } from './NoteReadService.js';
import { NotificationService } from './NotificationService.js';
import { PollService } from './PollService.js';
import { PushNotificationService } from './PushNotificationService.js';
@ -75,7 +73,6 @@ import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js';
@ -83,6 +80,7 @@ import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js';
import { FanoutTimelineService } from './FanoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js';
import { ChatService } from './ChatService.js';
import { RegistryApiService } from './RegistryApiService.js';
import { ReversiService } from './ReversiService.js';
@ -108,6 +106,7 @@ import { AppEntityService } from './entities/AppEntityService.js';
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
import { BlockingEntityService } from './entities/BlockingEntityService.js';
import { ChannelEntityService } from './entities/ChannelEntityService.js';
import { ChatEntityService } from './entities/ChatEntityService.js';
import { ClipEntityService } from './entities/ClipEntityService.js';
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
@ -174,7 +173,6 @@ const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppL
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService };
@ -187,7 +185,6 @@ const $HashtagService: Provider = { provide: 'HashtagService', useExisting: Hash
const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService };
const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
@ -197,10 +194,9 @@ const $NoteEditService: Provider = { provide: 'NoteEditService', useExisting: No
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $LatestNoteService: Provider = { provide: 'LatestNoteService', useExisting: LatestNoteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExisting: ProxyAccountService };
const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
@ -235,6 +231,7 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
const $TimeService: Provider = { provide: 'TimeService', useExisting: TimeService };
@ -263,6 +260,7 @@ const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting:
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService };
const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService };
const $ChatEntityService: Provider = { provide: 'ChatEntityService', useExisting: ChatEntityService };
const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService };
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
@ -333,7 +331,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
AchievementService,
AvatarDecorationService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
DeleteAccountService,
DownloadService,
@ -346,7 +343,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
HttpRequestService,
IdService,
ImageProcessingService,
InstanceActorService,
InternalStorageService,
MetaService,
MfmService,
@ -356,10 +352,9 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
NoteDeleteService,
LatestNoteService,
NotePiningService,
NoteReadService,
NotificationService,
PollService,
ProxyAccountService,
SystemAccountService,
PushNotificationService,
QueryService,
ReactionService,
@ -394,6 +389,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
FanoutTimelineService,
FanoutTimelineEndpointService,
ChannelFollowingService,
ChatService,
RegistryApiService,
ReversiService,
TimeService,
@ -422,6 +418,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
AuthSessionEntityService,
BlockingEntityService,
ChannelEntityService,
ChatEntityService,
ClipEntityService,
DriveFileEntityService,
DriveFolderEntityService,
@ -488,7 +485,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$AchievementService,
$AvatarDecorationService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
$DeleteAccountService,
$DownloadService,
@ -501,7 +497,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$HttpRequestService,
$IdService,
$ImageProcessingService,
$InstanceActorService,
$InternalStorageService,
$MetaService,
$MfmService,
@ -511,10 +506,9 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$NoteDeleteService,
$LatestNoteService,
$NotePiningService,
$NoteReadService,
$NotificationService,
$PollService,
$ProxyAccountService,
$SystemAccountService,
$PushNotificationService,
$QueryService,
$ReactionService,
@ -549,6 +543,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$FanoutTimelineService,
$FanoutTimelineEndpointService,
$ChannelFollowingService,
$ChatService,
$RegistryApiService,
$ReversiService,
$TimeService,
@ -577,6 +572,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$AuthSessionEntityService,
$BlockingEntityService,
$ChannelEntityService,
$ChatEntityService,
$ClipEntityService,
$DriveFileEntityService,
$DriveFolderEntityService,
@ -644,7 +640,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
AchievementService,
AvatarDecorationService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
DeleteAccountService,
DownloadService,
@ -657,7 +652,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
HttpRequestService,
IdService,
ImageProcessingService,
InstanceActorService,
InternalStorageService,
MetaService,
MfmService,
@ -667,10 +661,9 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
NoteDeleteService,
LatestNoteService,
NotePiningService,
NoteReadService,
NotificationService,
PollService,
ProxyAccountService,
SystemAccountService,
PushNotificationService,
QueryService,
ReactionService,
@ -705,6 +698,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
FanoutTimelineService,
FanoutTimelineEndpointService,
ChannelFollowingService,
ChatService,
RegistryApiService,
ReversiService,
TimeService,
@ -732,6 +726,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
AuthSessionEntityService,
BlockingEntityService,
ChannelEntityService,
ChatEntityService,
ClipEntityService,
DriveFileEntityService,
DriveFolderEntityService,
@ -798,7 +793,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$AchievementService,
$AvatarDecorationService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
$DeleteAccountService,
$DownloadService,
@ -811,7 +805,6 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$HttpRequestService,
$IdService,
$ImageProcessingService,
$InstanceActorService,
$InternalStorageService,
$MetaService,
$MfmService,
@ -821,10 +814,9 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$NoteDeleteService,
$LatestNoteService,
$NotePiningService,
$NoteReadService,
$NotificationService,
$PollService,
$ProxyAccountService,
$SystemAccountService,
$PushNotificationService,
$QueryService,
$ReactionService,
@ -858,6 +850,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$FanoutTimelineService,
$FanoutTimelineEndpointService,
$ChannelFollowingService,
$ChatService,
$RegistryApiService,
$ReversiService,
$TimeService,
@ -885,6 +878,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$AuthSessionEntityService,
$BlockingEntityService,
$ChannelEntityService,
$ChatEntityService,
$ClipEntityService,
$DriveFileEntityService,
$DriveFolderEntityService,

View file

@ -1,95 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
//import bcrypt from 'bcryptjs';
import { IsNull, DataSource } from 'typeorm';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
import { DI } from '@/di-symbols.js';
import generateNativeUserToken from '@/misc/generate-native-user-token.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class CreateSystemUserService {
constructor(
@Inject(DI.db)
private db: DataSource,
private idService: IdService,
) {
}
@bindThis
public async createSystemUser(username: string): Promise<MiUser> {
const password = randomUUID();
// Generate hash of password
//const salt = await bcrypt.genSalt(8);
const hash = await argon2.hash(password);
// Generate secret
const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair();
let account!: MiUser;
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
usernameLower: username.toLowerCase(),
host: IsNull(),
});
if (exist) throw new Error('the user is already exists');
account = await transactionalEntityManager.insert(MiUser, {
id: this.idService.gen(),
username: username,
usernameLower: username.toLowerCase(),
host: null,
token: secret,
isRoot: false,
isLocked: true,
isExplorable: false,
approved: true,
isBot: true,
/* we always allow requests about our instance actor, because when
a remote instance needs to check our signature on a request we
sent, it will need to fetch information about the user that
signed it (which is our instance actor), and if we try to check
their signature on *that* request, we'll fetch *their* instance
actor... leading to an infinite recursion */
allowUnsignedFetch: 'always',
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
await transactionalEntityManager.insert(MiUserKeypair, {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id,
});
await transactionalEntityManager.insert(MiUserProfile, {
userId: account.id,
autoAcceptFollowed: false,
password: hash,
});
await transactionalEntityManager.insert(MiUsedUsername, {
createdAt: new Date(),
username: username.toLowerCase(),
});
});
return account;
}
}

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
import type { FollowingsRepository, MiMeta, MiUser, UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@ -13,11 +13,15 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
@Injectable()
export class DeleteAccountService {
constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -29,6 +33,7 @@ export class DeleteAccountService {
private queueService: QueueService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
private systemAccountService: SystemAccountService,
) {
}
@ -37,9 +42,13 @@ export class DeleteAccountService {
id: string;
host: string | null;
}, moderator?: MiUser): Promise<void> {
if (this.meta.rootUserId === user.id) throw new Error('cannot delete a root account');
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account');
if (isSystemAccount(_user)) throw new Error('cannot delete a system account');
if (isSystemAccount(_user)) {
throw new Error('cannot delete a system account');
}
if (moderator != null) {
this.moderationLogService.log(moderator, 'deleteAccount', {

View file

@ -60,8 +60,8 @@ export class DownloadService {
request: operationTimeout, // whole operation timeout
},
agent: {
http: this.httpRequestService.httpAgent,
https: this.httpRequestService.httpsAgent,
http: this.httpRequestService.getAgentForHttp(urlObj, true),
https: this.httpRequestService.getAgentForHttps(urlObj, true),
},
http2: false, // default
retry: {

View file

@ -177,7 +177,8 @@ export class DriveService {
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
// for original
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const prefix = this.meta.objectStoragePrefix ? `${this.meta.objectStoragePrefix}/` : '';
const key = `${prefix}${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`;
// for alts
@ -194,7 +195,7 @@ export class DriveService {
];
if (alts.webpublic) {
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@ -202,7 +203,7 @@ export class DriveService {
}
if (alts.thumbnail) {
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);

View file

@ -164,6 +164,13 @@ export class EmailService {
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
}> {
if (!this.utilityService.validateEmailFormat(emailAddress)) {
return {
available: false,
reason: 'format',
};
}
const exist = await this.userProfilesRepository.countBy({
emailVerified: true,
email: emailAddress,

View file

@ -9,7 +9,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
export type FanoutTimelineName =
export type FanoutTimelineName = (
// home timeline
| `homeTimeline:${string}`
| `homeTimelineWithFiles:${string}` // only notes with files are included
@ -37,6 +37,7 @@ export type FanoutTimelineName =
// role timelines
| `roleTimeline:${string}` // any notes are included
);
@Injectable()
export class FanoutTimelineService {

View file

@ -20,7 +20,7 @@ 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 { MiAvatarDecoration, MiChatMessage, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@ -72,12 +72,8 @@ export interface MainEventTypes {
readAllNotifications: undefined;
notificationFlushed: undefined;
unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id'];
readAllUnreadMentions: undefined;
unreadSpecifiedNote: MiNote['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllAntennas: undefined;
unreadAntenna: MiAntenna;
newChatMessage: Packed<'ChatMessage'>;
readAllAnnouncements: undefined;
myTokenRegenerated: undefined;
signin: {
@ -168,6 +164,16 @@ export interface AdminEventTypes {
};
}
export interface ChatEventTypes {
message: Packed<'ChatMessageLite'>;
deleted: Packed<'ChatMessageLite'>['id'];
react: {
reaction: string;
user?: Packed<'UserLite'>;
messageId: MiChatMessage['id'];
};
}
export interface ReversiEventTypes {
matched: {
game: Packed<'ReversiGameDetailed'>;
@ -207,7 +213,7 @@ export interface ReversiGameEventTypes {
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
type EventUnionFromDictionary<
T extends object,
U = Events<T>
U = Events<T>,
> = U[keyof U];
type SerializedAll<T> = {
@ -216,7 +222,7 @@ type SerializedAll<T> = {
type UndefinedAsNullAll<T> = {
[K in keyof T]: T[K] extends undefined ? null : T[K];
}
};
export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
@ -300,6 +306,14 @@ export type GlobalEvents = {
name: 'notesStream';
payload: Serialized<Packed<'Note'>>;
};
chat: {
name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`;
payload: EventTypesToEventPayload<ChatEventTypes>;
};
chatRoom: {
name: `chatRoomStream:${MiChatRoom['id']}`;
payload: EventTypesToEventPayload<ChatEventTypes>;
};
reversi: {
name: `reversiStream:${MiUser['id']}`;
payload: EventTypesToEventPayload<ReversiEventTypes>;
@ -398,6 +412,16 @@ export class GlobalEventService {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishChatUserStream<K extends keyof ChatEventTypes>(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void {
this.publish(`chatUserStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishChatRoomStream<K extends keyof ChatEventTypes>(toRoomId: MiChatRoom['id'], type: K, value?: ChatEventTypes[K]): void {
this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);

View file

@ -16,6 +16,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
import { ApUtilityService } from './activitypub/ApUtilityService.js';
import type { Response } from 'node-fetch';
@ -115,32 +116,32 @@ export class HttpRequestService {
/**
* Get http non-proxy agent (without local address filtering)
*/
private httpNative: http.Agent;
private readonly httpNative: http.Agent;
/**
* Get https non-proxy agent (without local address filtering)
*/
private httpsNative: https.Agent;
private readonly httpsNative: https.Agent;
/**
* Get http non-proxy agent
*/
private http: http.Agent;
private readonly http: http.Agent;
/**
* Get https non-proxy agent
*/
private https: https.Agent;
private readonly https: https.Agent;
/**
* Get http proxy or non-proxy agent
*/
public httpAgent: http.Agent;
public readonly httpAgent: http.Agent;
/**
* Get https proxy or non-proxy agent
*/
public httpsAgent: https.Agent;
public readonly httpsAgent: https.Agent;
constructor(
@Inject(DI.config)
@ -198,7 +199,7 @@ export class HttpRequestService {
/**
* Get agent by URL
* @param url URL
* @param bypassProxy Allways bypass proxy
* @param bypassProxy Always bypass proxy
* @param isLocalAddressAllowed
*/
@bindThis
@ -216,8 +217,40 @@ export class HttpRequestService {
}
}
/**
* Get agent for http by URL
* @param url URL
* @param isLocalAddressAllowed
*/
@bindThis
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObjectWithId> {
public getAgentForHttp(url: URL, isLocalAddressAllowed = false): http.Agent {
if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
return isLocalAddressAllowed
? this.httpNative
: this.http;
} else {
return this.httpAgent;
}
}
/**
* Get agent for https by URL
* @param url URL
* @param isLocalAddressAllowed
*/
@bindThis
public getAgentForHttps(url: URL, isLocalAddressAllowed = false): https.Agent {
if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
return isLocalAddressAllowed
? this.httpsNative
: this.https;
} else {
return this.httpsAgent;
}
}
@bindThis
public async getActivityJson(url: string, isLocalAddressAllowed = false, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise<IObjectWithId> {
const res = await this.send(url, {
method: 'GET',
headers: {
@ -235,7 +268,7 @@ export class HttpRequestService {
// Make sure the object ID matches the final URL (which is where it actually exists).
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url, allowSoftfail);
return activity as IObjectWithId;
}

View file

@ -1,57 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser } from '@/models/User.js';
import type { UsersRepository } from '@/models/_.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
private cache: MemorySingleCache<MiLocalUser>;
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new MemorySingleCache<MiLocalUser>(Infinity);
}
@bindThis
public async realLocalUsersPresent(): Promise<boolean> {
return await this.usersRepository.existsBy({
host: IsNull(),
username: Not(ACTOR_USERNAME),
});
}
@bindThis
public async getInstanceActor(): Promise<MiLocalUser> {
const cached = this.cache.get();
if (cached) return cached;
const user = await this.usersRepository.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
}) as MiLocalUser | undefined;
if (user) {
this.cache.set(user);
return user;
} else {
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as MiLocalUser;
this.cache.set(created);
return created;
}
}
}

View file

@ -53,7 +53,7 @@ export class MetaService implements OnApplicationShutdown {
case 'metaUpdated': {
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...(body.after),
proxyAccount: null, // joinなカラムは通常取ってこないので
rootUser: null, // joinなカラムは通常取ってこないので
};
break;
}
@ -113,17 +113,20 @@ export class MetaService implements OnApplicationShutdown {
if (before) {
await transactionalEntityManager.update(MiMeta, before.id, data);
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
id: 'DESC',
},
});
return metas[0];
} else {
return await transactionalEntityManager.save(MiMeta, data);
await transactionalEntityManager.save(MiMeta, {
...data,
id: 'x',
});
}
const afters = await transactionalEntityManager.find(MiMeta, {
order: {
id: 'DESC',
},
});
return afters[0];
});
if (data.hiddenTags) {

View file

@ -576,7 +576,8 @@ export class MfmService {
appendChildren(nodes, body);
const serialized = new XMLSerializer().serializeToString(body);
// Remove the unnecessary namespace
const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
happyDOM.close().catch(err => {});

View file

@ -42,7 +42,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@ -203,7 +202,6 @@ export class NoteCreateService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private queueService: QueueService,
private fanoutTimelineService: FanoutTimelineService,
private noteReadService: NoteReadService,
private notificationService: NotificationService,
private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService,
@ -651,31 +649,6 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
// 未読通知を作成
if (data.visibility === 'specified') {
if (data.visibleUsers == null) throw new Error('invalid param');
for (const u of data.visibleUsers) {
// ローカルユーザーのみ
if (!this.userEntityService.isLocalUser(u)) continue;
this.noteReadService.insertNoteUnread(u.id, note, {
isSpecified: true,
isMentioned: false,
});
}
} else {
for (const u of mentionedUsers) {
// ローカルユーザーのみ
if (!this.userEntityService.isLocalUser(u)) continue;
this.noteReadService.insertNoteUnread(u.id, note, {
isSpecified: false,
isMentioned: true,
});
}
}
// Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets, In } from 'typeorm';
import { Brackets, In, IsNull, Not } from 'typeorm';
import { Injectable, Inject } from '@nestjs/common';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
@ -231,13 +231,27 @@ export class NoteDeleteService {
}) as MiRemoteUser[];
}
@bindThis
private async getRenotedOrRepliedRemoteUsers(note: MiNote) {
const query = this.notesRepository.createQueryBuilder('note')
.leftJoinAndSelect('note.user', 'user')
.where(new Brackets(qb => {
qb.orWhere('note.renoteId = :renoteId', { renoteId: note.id });
qb.orWhere('note.replyId = :replyId', { replyId: note.id });
}))
.andWhere({ userHost: Not(IsNull()) });
const notes = await query.getMany() as (MiNote & { user: MiRemoteUser })[];
const remoteUsers = notes.map(({ user }) => user);
return remoteUsers;
}
@bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content);
const remoteUsers = await this.getMentionedRemoteUsers(note);
for (const remoteUser of remoteUsers) {
this.apDeliverManagerService.deliverToUser(user, content, remoteUser);
}
this.apDeliverManagerService.deliverToUsers(user, content, [
...await this.getMentionedRemoteUsers(note),
...await this.getRenotedOrRepliedRemoteUsers(note),
]);
}
}

View file

@ -1,147 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiNote } from '@/models/Note.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { trackPromise } from '@/misc/promise-tracker.js';
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
}
@bindThis
public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: {
// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
isSpecified: boolean;
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
// スレッドミュート
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: userId,
threadId: note.threadId ?? note.id,
},
});
if (isThreadMuted) return;
const unread = {
id: this.idService.gen(),
noteId: note.id,
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteUserId: note.userId,
};
/* we may be called from NoteEditService, for a note that's
already present in the `note_unread` table: `upsert` makes sure
we don't throw a "duplicate key" error, while still updating
the other columns if they've changed */
await this.noteUnreadsRepository.upsert(unread, ['userId', 'noteId']);
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
if (!exist) return;
if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
}
if (params.isSpecified) {
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
}
}, () => { /* aborted, ignore it */ });
}
@bindThis
public async read(
userId: MiUser['id'],
notes: (MiNote | Packed<'Note'>)[],
): Promise<void> {
if (notes.length === 0) return;
const noteIds = new Set<MiNote['id']>();
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
noteIds.add(note.id);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
noteIds.add(note.id);
}
}
if (noteIds.size === 0) return;
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In(Array.from(noteIds)),
});
// TODO: ↓まとめてクエリしたい
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isMentioned: true,
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
}));
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isSpecified: true,
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
}));
}
@bindThis
public dispose(): void {
this.#shutdownController.abort();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View file

@ -1,28 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type { MiMeta, UsersRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ProxyAccountService {
constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
) {
}
@bindThis
public async fetch(): Promise<MiLocalUser | null> {
if (this.meta.proxyAccountId == null) return null;
return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
}
}

View file

@ -4,53 +4,34 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { RelaysRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { MiRelay } from '@/models/Relay.js';
import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { DI } from '@/di-symbols.js';
import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
const ACTOR_USERNAME = 'relay.actor' as const;
import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable()
export class RelayService {
private relaysCache: MemorySingleCache<MiRelay[]>;
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.relaysRepository)
private relaysRepository: RelaysRepository,
private idService: IdService,
private queueService: QueueService,
private createSystemUserService: CreateSystemUserService,
private systemAccountService: SystemAccountService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
}
@bindThis
private async getRelayActor(): Promise<MiLocalUser> {
const user = await this.usersRepository.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
});
if (user) return user as MiLocalUser;
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
return created as MiLocalUser;
}
@bindThis
public async addRelay(inbox: string): Promise<MiRelay> {
const relay = await this.relaysRepository.insertOne({
@ -59,8 +40,8 @@ export class RelayService {
status: 'requesting',
});
const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const relayActor = await this.systemAccountService.fetch('relay');
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox, false);
@ -77,7 +58,7 @@ export class RelayService {
throw new Error('relay not found');
}
const relayActor = await this.getRelayActor();
const relayActor = await this.systemAccountService.fetch('relay');
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo);

View file

@ -74,7 +74,7 @@ export class RemoteUserResolveService {
if (user == null) {
const self = await this.resolveSelf(acctLower);
if (self.href.startsWith(this.config.url)) {
if (this.utilityService.isUriLocal(self.href)) {
const local = this.apDbResolverService.parseUri(self.href);
if (local.local && local.type === 'users') {
// the LR points to local

View file

@ -66,6 +66,7 @@ export type RolePolicies = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
canChat: boolean;
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -103,11 +104,11 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
canChat: true,
};
@Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
@ -143,7 +144,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
@ -411,19 +411,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
canChat: calc('canChat', vs => vs.some(v => v === true)),
};
}
@bindThis
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
public async isModerator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
}
@bindThis
public async isAdministrator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
public async isAdministrator(user: { id: MiUser['id'] } | null): Promise<boolean> {
if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
return (this.meta.rootUserId === user.id) || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
}
@bindThis
@ -472,16 +473,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
.map(a => a.userId),
);
if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
if (includeRoot && this.meta.rootUserId) {
resultSet.add(this.meta.rootUserId);
}
return [...resultSet].sort((x, y) => x.localeCompare(y));

View file

@ -46,6 +46,8 @@ export class S3Service {
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),
requestChecksumCalculation: 'WHEN_REQUIRED',
responseChecksumValidation: 'WHEN_REQUIRED',
});
}

View file

@ -15,13 +15,14 @@ import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { generateNativeUserToken } from '@/misc/token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserService } from '@/core/UserService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class SignupService {
@ -42,7 +43,8 @@ export class SignupService {
private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
private instanceActorService: InstanceActorService,
private systemAccountService: SystemAccountService,
private metaService: MetaService,
private usersChart: UsersChart,
) {
}
@ -77,7 +79,7 @@ export class SignupService {
}
// Generate secret
const secret = generateUserToken();
const secret = generateNativeUserToken();
// Check username duplication
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
@ -89,9 +91,7 @@ export class SignupService {
throw new Error('USED_USERNAME');
}
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
if (!opts.ignorePreservedUsernames && this.meta.rootUserId != null) {
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new Error('USED_USERNAME');
@ -132,8 +132,7 @@ export class SignupService {
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
isRoot: isTheFirstUser,
approved: isTheFirstUser || (opts.approved ?? !this.meta.approvalRequiredForSignup),
approved: opts.approved ?? !this.meta.approvalRequiredForSignup,
signupReason: reason,
enableRss: false,
}));
@ -159,6 +158,10 @@ export class SignupService {
this.usersChart.update(account, true);
this.userService.notifySystemWebhook(account, 'userCreated');
if (this.meta.rootUserId == null) {
await this.metaService.update({ rootUserId: account.id });
}
return { account, secret };
}
}

View file

@ -0,0 +1,172 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, IsNull } from 'typeorm';
import bcrypt from 'bcryptjs';
import { MiLocalUser, MiUser } from '@/models/User.js';
import { MiSystemAccount, MiUsedUsername, MiUserKeypair, MiUserProfile, type UsersRepository, type SystemAccountsRepository } from '@/models/_.js';
import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { generateNativeUserToken } from '@/misc/token.js';
import { IdService } from '@/core/IdService.js';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
export const SYSTEM_ACCOUNT_TYPES = ['actor', 'relay', 'proxy'] as const;
@Injectable()
export class SystemAccountService {
private cache: MemoryKVCache<MiLocalUser>;
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.systemAccountsRepository)
private systemAccountsRepository: SystemAccountsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private idService: IdService,
) {
this.cache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 10); // 10m
}
@bindThis
public async list(): Promise<MiSystemAccount[]> {
const accounts = await this.systemAccountsRepository.findBy({});
return accounts;
}
@bindThis
public async fetch(type: typeof SYSTEM_ACCOUNT_TYPES[number]): Promise<MiLocalUser> {
const cached = this.cache.get(type);
if (cached) return cached;
const systemAccount = await this.systemAccountsRepository.findOne({
where: { type: type },
relations: ['user'],
});
if (systemAccount) {
this.cache.set(type, systemAccount.user as MiLocalUser);
return systemAccount.user as MiLocalUser;
} else {
const created = await this.createCorrespondingUser(type, {
username: `system.${type}`, // NOTE: (できれば避けたいが) . が含まれるかどうかでシステムアカウントかどうかを判定している処理もあるので変えないように
name: this.meta.name,
});
this.cache.set(type, created);
return created;
}
}
@bindThis
private async createCorrespondingUser(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: {
username: MiUser['username'];
name?: MiUser['name'];
}): Promise<MiLocalUser> {
const password = randomUUID();
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
// Generate secret
const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair();
let account!: MiUser;
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
usernameLower: extra.username.toLowerCase(),
host: IsNull(),
});
if (exist) {
account = exist;
return;
}
account = await transactionalEntityManager.insert(MiUser, {
id: this.idService.gen(),
username: extra.username,
usernameLower: extra.username.toLowerCase(),
host: null,
token: secret,
isLocked: true,
isExplorable: false,
isBot: true,
name: extra.name,
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
await transactionalEntityManager.insert(MiUserKeypair, {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id,
});
await transactionalEntityManager.insert(MiUserProfile, {
userId: account.id,
autoAcceptFollowed: false,
password: hash,
});
await transactionalEntityManager.insert(MiUsedUsername, {
createdAt: new Date(),
username: extra.username.toLowerCase(),
});
await transactionalEntityManager.insert(MiSystemAccount, {
id: this.idService.gen(),
userId: account.id,
type: type,
});
});
return account as MiLocalUser;
}
@bindThis
public async updateCorrespondingUserProfile(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: {
name?: string;
description?: MiUserProfile['description'];
}): Promise<MiLocalUser> {
const user = await this.fetch(type);
const updates = {} as Partial<MiUser>;
if (extra.name !== undefined) updates.name = extra.name;
if (Object.keys(updates).length > 0) {
await this.usersRepository.update(user.id, updates);
}
const profileUpdates = {} as Partial<MiUserProfile>;
if (extra.description !== undefined) profileUpdates.description = extra.description;
if (Object.keys(profileUpdates).length > 0) {
await this.userProfilesRepository.update(user.id, profileUpdates);
}
const updated = await this.usersRepository.findOneByOrFail({ id: user.id }) as MiLocalUser;
this.cache.set(type, updated);
return updated;
}
}

View file

@ -5,7 +5,7 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { IsNull } from 'typeorm';
import { Brackets, IsNull } from 'typeorm';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js';
@ -736,4 +736,30 @@ export class UserFollowingService implements OnModuleInit {
.where('following.followerId = :followerId', { followerId: userId })
.getMany();
}
@bindThis
public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) {
return this.followingsRepository.exists({
where: {
followerId,
followeeId,
},
});
}
@bindThis
public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) {
const count = await this.followingsRepository.createQueryBuilder('following')
.where(new Brackets(qb => {
qb.where('following.followerId = :aUserId', { aUserId })
.andWhere('following.followeeId = :bUserId', { bUserId });
}))
.orWhere(new Brackets(qb => {
qb.where('following.followerId = :bUserId', { bUserId })
.andWhere('following.followeeId = :aUserId', { aUserId });
}))
.getCount();
return count === 2;
}
}

View file

@ -15,11 +15,11 @@ import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { QueueService } from '@/core/QueueService.js';
import { RedisKVCache } from '@/misc/cache.js';
import { RoleService } from '@/core/RoleService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
@Injectable()
export class UserListService implements OnApplicationShutdown, OnModuleInit {
@ -43,8 +43,8 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private queueService: QueueService,
private systemAccountService: SystemAccountService,
) {
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
lifetime: 1000 * 60 * 30, // 30m
@ -111,10 +111,8 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (this.userEntityService.isRemoteUser(target)) {
const proxy = await this.proxyAccountService.fetch();
if (proxy) {
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
}
const proxy = await this.systemAccountService.fetch('proxy');
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
}
}

View file

@ -15,7 +15,7 @@ import { QueueService } from '@/core/QueueService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' |'mention' | 'edited' ? {
T extends 'note' | 'reply' | 'renote' | 'mention' | 'edited' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {

View file

@ -39,6 +39,14 @@ export class UtilityService {
return this.punyHost(uri) === this.toPuny(this.config.host);
}
// メールアドレスのバリデーションを行う
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
@bindThis
public validateEmailFormat(email: string): boolean {
const regexp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return regexp.test(email);
}
@bindThis
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
if (host == null) return false;

View file

@ -127,11 +127,11 @@ export class WebAuthnService {
const { registrationInfo } = verification;
return {
credentialID: registrationInfo.credentialID,
credentialPublicKey: registrationInfo.credentialPublicKey,
credentialID: registrationInfo.credential.id,
credentialPublicKey: registrationInfo.credential.publicKey,
attestationObject: registrationInfo.attestationObject,
fmt: registrationInfo.fmt,
counter: registrationInfo.counter,
counter: registrationInfo.credential.counter,
userVerified: registrationInfo.userVerified,
credentialDeviceType: registrationInfo.credentialDeviceType,
credentialBackedUp: registrationInfo.credentialBackedUp,
@ -212,9 +212,9 @@ export class WebAuthnService {
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
credential: {
id: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},
@ -292,9 +292,9 @@ export class WebAuthnService {
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
credential: {
id: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},

View file

@ -7,42 +7,16 @@ import { Injectable } from '@nestjs/common';
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js';
import { type AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
import { type Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
const oneDayMillis = 24 * 60 * 60 * 1000;
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
targetUser: null,
reporterId: 'dummy-reporter-user',
reporter: null,
assigneeId: null,
assignee: null,
resolved: false,
forwarded: false,
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override,
};
return {
...result,
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
};
}
function generateDummyUser(override?: Partial<MiUser>): MiUser {
return {
id: 'dummy-user-1',
@ -79,13 +53,13 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isBot: false,
isCat: true,
speakAsCat: true,
isRoot: false,
isExplorable: true,
isHibernated: false,
isDeleted: false,
requireSigninToViewContents: false,
makeNotesFollowersOnlyBefore: null,
makeNotesHiddenBefore: null,
chatScope: 'mutual',
emojis: [],
score: 0,
host: null,
@ -150,135 +124,6 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
};
}
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
return {
id: note.id,
createdAt: new Date().toISOString(),
deletedAt: null,
text: note.text,
cw: note.cw,
userId: note.userId,
user: toPackedUserLite(note.user ?? generateDummyUser()),
replyId: note.replyId,
renoteId: note.renoteId,
isHidden: false,
visibility: note.visibility,
mentions: note.mentions,
visibleUserIds: note.visibleUserIds,
fileIds: note.fileIds,
files: [],
tags: note.tags,
poll: null,
emojis: note.emojis,
channelId: note.channelId,
channel: note.channel,
localOnly: note.localOnly,
reactionAcceptance: note.reactionAcceptance,
reactionEmojis: {},
reactions: {},
reactionCount: 0,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
uri: note.uri ?? undefined,
url: note.url ?? undefined,
reactionAndUserPairCache: note.reactionAndUserPairCache,
...(detail ? {
clippedCount: note.clippedCount,
reply: note.reply ? toPackedNote(note.reply, false) : null,
renote: note.renote ? toPackedNote(note.renote, true) : null,
myReaction: null,
} : {}),
...override,
};
}
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
return {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl,
avatarBlurhash: user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,
angle: it.angle,
flipH: it.flipH,
url: 'https://example.com/dummy-image001.png',
offsetX: it.offsetX,
offsetY: it.offsetY,
})),
isBot: user.isBot,
isCat: user.isCat,
speakAsCat: user.speakAsCat,
emojis: user.emojis,
onlineStatus: 'active',
badgeRoles: [],
noindex: user.noindex,
isModerator: false,
isAdmin: false,
isSystem: false,
isSilenced: user.isSilenced,
enableRss: true,
mandatoryCW: null,
...override,
};
}
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
return {
...toPackedUserLite(user),
url: null,
uri: null,
movedTo: null,
alsoKnownAs: [],
createdAt: new Date().toISOString(),
updatedAt: user.updatedAt?.toISOString() ?? null,
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
backgroundUrl: user.backgroundUrl,
backgroundBlurhash: user.backgroundBlurhash,
isLocked: user.isLocked,
isSilenced: false,
isSuspended: user.isSuspended,
description: null,
location: null,
birthday: null,
lang: null,
fields: [],
verifiedLinks: [],
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: [],
pinnedNotes: [],
pinnedPageId: null,
pinnedPage: null,
publicReactions: true,
followersVisibility: 'public',
followingVisibility: 'public',
twoFactorEnabled: false,
usePasswordLessLogin: false,
securityKeys: false,
roles: [],
memo: null,
moderationNote: undefined,
isFollowing: false,
isFollowed: false,
hasPendingFollowRequestFromYou: false,
hasPendingFollowRequestToYou: false,
isBlocking: false,
isBlocked: false,
isMuted: false,
isRenoteMuted: false,
notify: 'none',
withReplies: true,
listenbrainz: null,
...override,
};
}
const dummyUser1 = generateDummyUser();
const dummyUser2 = generateDummyUser({
id: 'dummy-user-2',
@ -311,6 +156,7 @@ export class WebhookTestService {
};
constructor(
private customEmojiService: CustomEmojiService,
private userWebhookService: UserWebhookService,
private systemWebhookService: SystemWebhookService,
private queueService: QueueService,
@ -381,19 +227,19 @@ export class WebhookTestService {
switch (params.type) {
case 'note': {
send('note', { note: toPackedNote(dummyNote1) });
send('note', { note: await this.toPackedNote(dummyNote1) });
break;
}
case 'reply': {
send('reply', { note: toPackedNote(dummyReply1) });
send('reply', { note: await this.toPackedNote(dummyReply1) });
break;
}
case 'renote': {
send('renote', { note: toPackedNote(dummyRenote1) });
send('renote', { note: await this.toPackedNote(dummyRenote1) });
break;
}
case 'mention': {
send('mention', { note: toPackedNote(dummyMention1) });
send('mention', { note: await this.toPackedNote(dummyMention1) });
break;
}
case 'edited': {
@ -401,15 +247,15 @@ export class WebhookTestService {
break;
}
case 'follow': {
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
send('follow', { user: await this.toPackedUserDetailedNotMe(dummyUser1) });
break;
}
case 'followed': {
send('followed', { user: toPackedUserLite(dummyUser2) });
send('followed', { user: await this.toPackedUserLite(dummyUser2) });
break;
}
case 'unfollow': {
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
send('unfollow', { user: await this.toPackedUserDetailedNotMe(dummyUser3) });
break;
}
// まだ実装されていない (#9485)
@ -458,7 +304,7 @@ export class WebhookTestService {
switch (params.type) {
case 'abuseReport': {
send('abuseReport', generateAbuseReport({
send('abuseReport', await this.generateAbuseReport({
targetUserId: dummyUser1.id,
targetUser: dummyUser1,
reporterId: dummyUser2.id,
@ -467,7 +313,7 @@ export class WebhookTestService {
break;
}
case 'abuseReportResolved': {
send('abuseReportResolved', generateAbuseReport({
send('abuseReportResolved', await this.generateAbuseReport({
targetUserId: dummyUser1.id,
targetUser: dummyUser1,
reporterId: dummyUser2.id,
@ -479,7 +325,7 @@ export class WebhookTestService {
break;
}
case 'userCreated': {
send('userCreated', toPackedUserLite(dummyUser1));
send('userCreated', await this.toPackedUserLite(dummyUser1));
break;
}
case 'inactiveModeratorsWarning': {
@ -505,4 +351,154 @@ export class WebhookTestService {
}
}
}
@bindThis
private async generateAbuseReport(override?: Partial<MiAbuseUserReport>): Promise<AbuseReportPayload> {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
targetUser: null,
reporterId: 'dummy-reporter-user',
reporter: null,
assigneeId: null,
assignee: null,
resolved: false,
forwarded: false,
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override,
};
return {
...result,
targetUser: result.targetUser ? await this.toPackedUserLite(result.targetUser) : null,
reporter: result.reporter ? await this.toPackedUserLite(result.reporter) : null,
assignee: result.assignee ? await this.toPackedUserLite(result.assignee) : null,
};
}
@bindThis
private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise<Packed<'Note'>> {
return {
id: note.id,
createdAt: new Date().toISOString(),
deletedAt: null,
text: note.text,
cw: note.cw,
userId: note.userId,
user: await this.toPackedUserLite(note.user ?? generateDummyUser()),
replyId: note.replyId,
renoteId: note.renoteId,
isHidden: false,
visibility: note.visibility,
mentions: note.mentions,
visibleUserIds: note.visibleUserIds,
fileIds: note.fileIds,
files: [],
tags: note.tags,
poll: null,
emojis: await this.customEmojiService.populateEmojis(note.emojis, note.userHost),
channelId: note.channelId,
channel: note.channel,
localOnly: note.localOnly,
reactionAcceptance: note.reactionAcceptance,
reactionEmojis: {},
reactions: {},
reactionCount: 0,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
uri: note.uri ?? undefined,
url: note.url ?? undefined,
reactionAndUserPairCache: note.reactionAndUserPairCache,
...(detail ? {
clippedCount: note.clippedCount,
reply: note.reply ? await this.toPackedNote(note.reply, false) : null,
renote: note.renote ? await this.toPackedNote(note.renote, true) : null,
myReaction: null,
} : {}),
...override,
};
}
@bindThis
private async toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Promise<Packed<'UserLite'>> {
return {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl,
avatarBlurhash: user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
id: it.id,
angle: it.angle,
flipH: it.flipH,
url: 'https://example.com/dummy-image001.png',
offsetX: it.offsetX,
offsetY: it.offsetY,
})),
isBot: user.isBot,
isCat: user.isCat,
emojis: await this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: 'active',
badgeRoles: [],
...override,
};
}
@bindThis
private async toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Promise<Packed<'UserDetailedNotMe'>> {
return {
...await this.toPackedUserLite(user),
url: null,
uri: null,
movedTo: null,
alsoKnownAs: [],
createdAt: new Date().toISOString(),
updatedAt: user.updatedAt?.toISOString() ?? null,
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: false,
isSuspended: user.isSuspended,
description: null,
location: null,
birthday: null,
lang: null,
fields: [],
verifiedLinks: [],
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: [],
pinnedNotes: [],
pinnedPageId: null,
pinnedPage: null,
publicReactions: true,
followersVisibility: 'public',
followingVisibility: 'public',
chatScope: 'mutual',
twoFactorEnabled: false,
usePasswordLessLogin: false,
securityKeys: false,
roles: [],
memo: null,
moderationNote: undefined,
isFollowing: false,
isFollowed: false,
hasPendingFollowRequestFromYou: false,
hasPendingFollowRequestToYou: false,
isBlocking: false,
isBlocked: false,
isMuted: false,
isRenoteMuted: false,
notify: 'none',
withReplies: true,
...override,
};
}
}

View file

@ -197,6 +197,25 @@ export class ApDeliverManagerService {
await manager.execute();
}
/**
* Deliver activity to users
* @param actor
* @param activity Activity
* @param targets Target users
*/
@bindThis
public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
activity,
);
for (const to of targets) manager.addDirectRecipe(to);
await manager.execute();
}
@bindThis
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager(

View file

@ -598,19 +598,12 @@ export class ApInboxService {
return `skip: delete actor ${actor.uri} !== ${uri}`;
}
const user = await this.usersRepository.findOneBy({ id: actor.id });
if (user == null) {
return 'skip: actor not found';
} else if (user.isDeleted) {
return 'skip: already deleted';
if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
return 'skip: already deleted or actor not found';
}
const job = await this.queueService.createDeleteAccountJob(actor);
await this.usersRepository.update(actor.id, {
isDeleted: true,
});
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
return `ok: queued ${job.name} ${job.id}`;

View file

@ -30,6 +30,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { IdService } from '@/core/IdService.js';
import { appendContentWarning } from '@/misc/append-content-warning.js';
import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
@ -72,6 +73,7 @@ export class ApRendererService {
private mfmService: MfmService,
private idService: IdService,
private readonly queryService: QueryService,
private utilityService: UtilityService,
) {
}
@ -266,6 +268,49 @@ export class ApRendererService {
};
}
@bindThis
public renderIdenticon(user: MiLocalUser): IApImage {
return {
type: 'Image',
url: this.userEntityService.getIdenticonUrl(user),
sensitive: false,
name: null,
};
}
@bindThis
public renderSystemAvatar(user: MiLocalUser): IApImage {
if (this.meta.iconUrl == null) return this.renderIdenticon(user);
return {
type: 'Image',
url: this.meta.iconUrl,
sensitive: false,
name: null,
};
}
@bindThis
public renderSystemBanner(): IApImage | null {
if (this.meta.bannerUrl == null) return null;
return {
type: 'Image',
url: this.meta.bannerUrl,
sensitive: false,
name: null,
};
}
@bindThis
public renderSystemBackground(): IApImage | null {
if (this.meta.backgroundImageUrl == null) return null;
return {
type: 'Image',
url: this.meta.backgroundImageUrl,
sensitive: false,
name: null,
};
}
@bindThis
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
return {
@ -503,11 +548,28 @@ export class ApRendererService {
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
]);
const tryRewriteUrl = (maybeUrl: string) => {
const urlSafeRegex = /^(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/;
try {
const match = maybeUrl.match(urlSafeRegex);
if (!match) {
return maybeUrl;
}
const urlPart = match[0];
const urlPartParsed = new URL(urlPart);
const restPart = maybeUrl.slice(match[0].length);
return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
} catch (e) {
return maybeUrl;
}
};
const attachment = profile.fields.map(field => ({
type: 'PropertyValue',
name: field.name,
value: (field.value.startsWith('http://') || field.value.startsWith('https://'))
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
? tryRewriteUrl(field.value)
: field.value,
}));
@ -542,9 +604,9 @@ export class ApRendererService {
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null,
backgroundUrl: background ? this.renderImage(background) : null,
icon: avatar ? this.renderImage(avatar) : isSystem ? this.renderSystemAvatar(user) : this.renderIdenticon(user),
image: banner ? this.renderImage(banner) : isSystem ? this.renderSystemBanner() : null,
backgroundUrl: background ? this.renderImage(background) : isSystem ? this.renderSystemBackground() : null,
tag,
manuallyApprovesFollowers: user.isLocked,
discoverable: user.isExplorable,
@ -660,7 +722,7 @@ export class ApRendererService {
@bindThis
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
const id = typeof object !== 'string' && typeof object.id === 'string' && this.utilityService.isUriLocal(object.id) ? `${object.id}/undo` : undefined;
return {
type: 'Undo',

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