Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-04-30 17:51:27 +08:00 committed by GitHub
commit f1a9bb97a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
491 changed files with 13387 additions and 7670 deletions

View File

@ -51,6 +51,23 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@ -62,6 +79,22 @@ redis:
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

View File

@ -51,6 +51,23 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@ -62,6 +79,22 @@ redis:
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────
@ -102,10 +135,14 @@ id: 'aid'
# Job concurrency per worker
#deliverJobConcurrency: 128
#inboxJobConcurrency: 16
#relashionshipJobConcurrency: 16
# What's relashionshipJob?:
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
# Job rate limiter
#deliverJobPerSec: 128
#inboxJobPerSec: 16
#relashionshipJobPerSec: 64
# Job attempts
#deliverJobMaxAttempts: 12

View File

@ -4,7 +4,10 @@
"service": "app",
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
"ghcr.io/devcontainers/features/node:1": {
"version": "18.16.0"
}
},
"forwardPorts": [3000],
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",

View File

@ -51,6 +51,23 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@ -62,6 +79,22 @@ redis:
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

25
.github/labeler.yml vendored
View File

@ -1,12 +1,21 @@
'Server':
'packages/backend':
- packages/backend/**/*
'🖥Client':
- packages/frontend/**/*
'🧪Test':
- cypress/**/*
'packages/backend:test':
- packages/backend/test/**/*
'‼️ wrong locales':
- any: ['locales/*.yml', '!locales/ja-JP.yml']
'packages/frontend':
- packages/frontend/**/*
'packages/frontend:test':
- cypress/**/*
'packages/sw':
- packages/sw/**/*
'packages/misskey-js':
- packages/misskey-js/**/*
'packages/misskey-js:test':
- packages/misskey-js/test/**/*
- packages/misskey-js/test-d/**/*

View File

@ -19,5 +19,6 @@ https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
## Checklist
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
- [ ] Test working in a local environment
- [ ] (If needed) Add story of storybook
- [ ] (If needed) Update CHANGELOG.md
- [ ] (If possible) Add tests

View File

@ -16,7 +16,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install dependencies

View File

@ -17,11 +17,11 @@ jobs:
submodules: true
- uses: pnpm/action-setup@v2
with:
version: 7
version: 8
run_install: false
- uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
@ -48,7 +48,7 @@ jobs:
run_install: false
- uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
@ -74,7 +74,7 @@ jobs:
run_install: false
- uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile

View File

@ -2,15 +2,16 @@ name: Storybook
on:
push:
branches:
- master
- develop
pull_request_target:
branches-ignore:
- l10n_develop
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v3.3.0
with:
@ -19,12 +20,12 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
version: 8
run_install: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
@ -34,21 +35,38 @@ jobs:
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
env:
NODE_OPTIONS: "--max_old_space_size=7168"
- name: Publish to Chromatic
id: chromatic
uses: chromaui/action@v1
with:
exitOnceUploaded: true
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
storybookBuildDir: storybook-static
workingDir: packages/frontend
- name: Compare on Chromatic
if: github.event_name == 'pull_request_target'
run: pnpm --filter frontend chromatic -d storybook-static --exit-once-uploaded --patch-build ${{ github.head_ref }}...${{ github.base_ref }}
if: github.ref == 'refs/heads/master'
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.ref != 'refs/heads/master'
id: chromatic
run: |
DIFF="${{ github.event.before }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic will skip testing
uses: actions/github-script@v6.4.0
if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/develop' && steps.chromatic.outputs.skip == 'true'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Chromatic will skip testing but you may still have to [review the changes on Chromatic](https://www.chromatic.com/pullrequests?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:

View File

@ -24,7 +24,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:6
image: redis:7
ports:
- 56312:6379
@ -35,7 +35,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0

View File

@ -22,7 +22,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
@ -63,7 +63,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:6
image: redis:7
ports:
- 56312:6379
@ -106,7 +106,7 @@ jobs:
install: false
start: pnpm start:test
wait-on: 'http://localhost:61812'
headless: false
headed: true
browser: ${{ matrix.browser }}
- uses: actions/upload-artifact@v2
if: failure()

42
.github/workflows/test-production.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Test (production install and build)
on:
push:
branches:
- master
- develop
pull_request:
env:
NODE_ENV: production
jobs:
production:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v3.3.0
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .github/misskey/test.yml .config/default.yml
- name: Build
run: pnpm build

View File

@ -1 +1 @@
v18.13.0
18.16.0

View File

@ -6,5 +6,6 @@
"files.associations": {
"*.test.ts": "typescript"
},
"jest.jestCommandLine": "pnpm run jest",
"jest.autoRun": "off"
}

View File

@ -11,30 +11,154 @@
-
-->
## 13.x.x (unreleased)
### NOTE
- Node.js 18.6.0以上が必要になりました
### General
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
(自分自身に対してもメモを追加できます。)
* ユーザーメニューから追加できます。
デスクトップ表示ではusernameの右側のボタンからも追加可能
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
* 一度引っ越したアカウントは利用に制限がかかります
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
- カスタム絵文字のライセンスを複数でセットできるようになりました。
- 管理者が予約ユーザー名を設定できるようになりました。
### Client
- 通知の表示をカスタマイズできるように
- コントロールパネルのカスタム絵文字ページおよびaboutのカスタム絵文字の検索インプットで、`:emojiname1::emojiname2:`のように検索して絵文字を検索できるように
* 絵文字ピッカーから入力可能になります
- データセーバーモードを追加
* 画像が全て隠れた状態で表示されるようになります
- 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように
- Fix: リアクションをホバーした時のユーザーリストで猫耳が切れてしまっていた問題を修正
- 新しい実績を追加
- Renoteしたユーザーの一覧を見れるように
### Server
- 環境変数MISSKEY_CONFIG_YMLで設定ファイルをdefault.ymlから変更可能に
- Fix: エクスポートデータの拡張子がunknownになる問題を修正
- Fix: Content-Dispositionのパースでエラーが発生した場合にダウンロードが完了しない問題を修正
- Fix: API: i/update avatarIdとbannerIdにnullを渡した時、画像がリセットされない問題を修正
- Fix: 1:1ではない画像のリアクション通知バッジが左や上に寄ってしまっていたのを中央に来るように修正
- Fix: .wav, .flacが再生できない問題を修正新しくアップロードされたファイルのみ修正が適用されます
- Fix: メモリの使用量を`used - buffers - cached`ではなく`total - available`で求めるように(環境によって正常に計測できていなかったため)
## 13.11.3
### General
- 指定したロールを持つユーザーのノートのみが流れるロールタイムラインを追加
- Deckのカラムとしても追加可能
- カスタム絵文字関連の改善
* ートなどに含まれるemojispopulateEmojiの結果プロキシされたURLではなくオリジナルのURLを指すように
* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように
- カスタム絵文字でリアクションできないことがある問題を修正
### Client
- チャンネルのピン留めされたノートの順番が正しくない問題を修正
### Server
- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io
- Misskey Webでのサーバーサイドエラー画面を改善
- Misskey Webでのサーバーサイドエラーのログが残るように
- ノート作成時のアンテナ追加パフォーマンスを改善
- アンテナとロールTLのuntil/sinceプロパティが動くように
## 13.11.2
### Note
- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照)
### General
- チャンネルの検索用ページの追加
### Client
- 常に広告を見られるオプションを追加
- ユーザーページの画像一覧が表示されない問題を修正
- webhook, 連携アプリ一覧でコンテンツが重複して表示される問題を修正
- iPhoneで絵文字ピッカーの表示が崩れる問題を修正
- iPhoneでウィジェットドロワーの「ウィジェットを編集」が押しにくい問題を修正
- 投稿フォームのデザインを調整
- ギャラリーの人気の投稿が無限にページングされる問題を修正
### Server
- channels/search Endpoint APIの追加
- APIパラメータサイズ上限を32kbから1mbに緩和
- プッシュ通知送信時のパフォーマンスを改善
- ローカルのカスタム絵文字のキャッシュが効いていなかった問題を修正
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
### Service Worker
- 「通知が既読になったらプッシュ通知を削除する」を復活
* 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように)
- プッシュ通知が実績を解除 (achievementEarned) に対応
- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正
- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように
## 13.11.1
### General
- チャンネルの投稿を過去までさかのぼれるように
### Client
- PWA時の絵文字ピッカーの位置をホームバーに重ならないように調整
- リスト管理の画面でリストが無限に読み込まれる問題を修正
- 自分のクリップが無限に読み込まれる問題を修正
- チャンネルのお気に入りが無限に読み込まれる問題を修正
- さがすのローカルユーザー(ピンどめ)が無限に生成される問題を修正
- チャンネルを新規作成できない問題を修正
- ユーザープレビューが表示されない問題を修正
### Server
- 通知読み込みでエラーが発生する場合がある問題を修正
- リアクションできないことがある問題を修正
- IDをaid以外に設定している場合の問題を修正
- 連合しているインスタンスについて予期せず配送が全て停止されることがある問題を修正
## 13.11.0
### NOTE
- このバージョンからRedis 7.xが必要です。
- アップデートを行うと全ての通知およびアンテナのノートはリセットされます。
### General
- チャンネルをお気に入りに登録できるように
- タイムラインのアンテナ選択などでは、フォローしているアンテナの代わりにお気に入りしたアンテナが表示されるようになっています。チャンネルをお気に入りに登録するには、当該チャンネルのページ→概要→⭐️のボタンを押します。
- チャンネルにノートをピン留めできるように
### Client
- 投稿フォームのデザインを改善
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
- オブジェクトストレージの設定画面を分かりやすく
- 広告・お知らせが新規登録時に増殖しないように
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
- Add Minimizing ("folding") of windows
- 「データセーバー」モードを追加
- 非NSFWメディアが隠れている際にも「閲覧注意」が出てしまう問題を修正
### Server
- PostgreSQLのレプリケーション対応
- 設定ファイルの `dbReplications` および `dbSlaves` にて設定できます
- イベント用Redisを別サーバーに分離できるように
- ジョブキュー用Redisを別サーバーに分離できるように
- サーバーの全体的なパフォーマンスを向上
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
- 通知に関する全体的なパフォーマンスを向上
- webhookがcontent-type text/plain;charset=UTF-8 で飛んでくる問題を修正
## 13.10.3

View File

@ -165,6 +165,11 @@ pnpm jest -- foo.ts
### e2e tests
TODO
## Environment Variable
- `MISSKEY_CONFIG_YML`: Specify the file path of config.yml instead of default.yml (e.g. `2nd.yml`).
- `MISSKEY_WEBFINGER_USE_HTTP`: If it's set true, WebFinger requests will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION.
## Continuous integration
Misskey uses GitHub Actions for executing automated tests.
Configuration files are located in [`/.github/workflows`](/.github/workflows).
@ -245,7 +250,6 @@ You can override the default story by creating a impl story file (`MyComponent.s
```ts
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-duplicates */
import { StoryObj } from '@storybook/vue3';
import MyComponent from './MyComponent.vue';
export const Default = {

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=18.13.0-bullseye
ARG NODE_VERSION=18.16.0-bullseye
# build assets & compile TypeScript

View File

@ -54,6 +54,25 @@ With Misskey's built in drive, you get cloud storage right in your social media,
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
## Sponsors
<div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div>
## Thanks
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
<a href="https://about.codecov.io/for/open-source/"><img src="https://about.codecov.io/wp-content/themes/codecov/assets/brand/sentry-cobranding/logos/codecov-by-sentry-logo.svg" height="30" alt="Codecov" /></a>
Thanks to [Codecov](https://about.codecov.io/for/open-source/) for providing the code coverage platform that helps us improve our test coverage.
<a href="https://crowdin.com/"><img src="https://user-images.githubusercontent.com/20679825/230709597-1299a011-171a-4294-a91e-355a9b37c672.svg" height="30" alt="Crowdin" /></a>
Thanks to [Crowdin](https://crowdin.com/) for providing the localization platform that helps us translate Misskey into many languages.
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" height="30" alt="Docker" /></a>
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.

View File

@ -72,16 +72,50 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: localhost
port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

View File

@ -52,6 +52,11 @@ describe('After setup instance', () => {
cy.intercept('POST', '/api/signup').as('signup');
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled');
@ -71,6 +76,11 @@ describe('After setup instance', () => {
// ユーザー名が重複している場合の挙動確認
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');

View File

@ -45,7 +45,7 @@ gulp.task('build:backend:script', () => {
});
gulp.task('build:backend:style', () => {
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css'])
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css'])
.pipe(cssnano({
zindex: false
}))

View File

@ -1277,4 +1277,3 @@ _deck:
_webhookSettings:
name: "الإسم"
active: "مفعّل"

View File

@ -1356,4 +1356,3 @@ _deck:
_webhookSettings:
name: "নাম"
active: "চালু"

View File

@ -460,4 +460,3 @@ _deck:
list: "Llistes"
mentions: "Mencions"
direct: "Publicacions directes"

View File

@ -779,4 +779,3 @@ _deck:
_webhookSettings:
name: "Jméno"
active: "Zapnuto"

View File

@ -1,3 +1,2 @@
---
_lang_: "Dansk"

View File

@ -20,6 +20,7 @@ noNotes: "Keine Notizen gefunden"
noNotifications: "Keine Benachrichtigungen gefunden"
instance: "Instanz"
settings: "Einstellungen"
notificationSettings: "Benachrichtigungseinstellungen"
basicSettings: "Allgemeine Einstellungen"
otherSettings: "Weitere Einstellungen"
openInWindow: "In einem Fenster öffnen"
@ -67,7 +68,7 @@ import: "Import"
export: "Export"
files: "Dateien"
download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden."
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Sie wird in allen Inhalten, die sie verwenden, auch verschwinden."
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
@ -196,7 +197,7 @@ instanceInfo: "Instanzinformationen"
statistics: "Statistiken"
clearQueue: "Warteschlange leeren"
clearQueueConfirmTitle: "Möchtest du die Warteschlange wirklich leeren?"
clearQueueConfirmText: "Hierdurch werden jegliche noch nicht gesendete Notizen nicht förderiert. Normalerweise wird dies nicht benötigt."
clearQueueConfirmText: "Hierdurch werden jegliche noch nicht gesendete Notizen nicht föderiert. Normalerweise wird dies nicht benötigt."
clearCachedFiles: "Cache leeren"
clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?"
blockedInstances: "Blockierte Instanzen"
@ -500,12 +501,13 @@ objectStoragePrefixDesc: "Dateien werden in Ordnern unter diesem Prefix gespeich
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "Im Falle von S3 leerlassen, für andere Anbieter den relevanten Endpoint im Format „<host>“ oder „<host>:<port>“ angeben."
objectStorageRegion: "Region"
objectStorageRegionDesc: "Gib eine Region wie z.B. „xx-east-1“ an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, lass dieses Feld leer oder gib „us-east-1“ an."
objectStorageRegionDesc: "Gib eine Region wie z.B. „xx-east-1“ an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, gib „us-east-1“ an. Lasse es leer bei Verwendung von AWS Konfigurationsdateien oder Umgebungsvariablen."
objectStorageUseSSL: "SSL verwenden"
objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein HTTPS verwenden wirst"
objectStorageUseProxy: "Über Proxy verbinden"
objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst"
objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
s3ForcePathStyleDesc: "Ist s3ForcePathStyle aktiviert, so muss der Bucketname nicht im Hostnamen der URL, sondern im Pfad der URL angeben werden. Diese Option muss eventuell aktiviert werden, wenn Dienste wie z.B. eine selbstbetriebene Minio-Instanz verwendet werden."
serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen"
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
@ -919,6 +921,7 @@ pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterst
sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald die relevanten Benachrichtigungen oder Nachrichten gelesen wurden"
sendPushNotificationReadMessageCaption: "Eine Push-Benachrichtigungen mit dem Inhalt \"{emptyPushNotificationMessage}\" wird kurz eingeblendet. Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen."
windowMaximize: "Maximieren"
windowMinimize: "Minimieren"
windowRestore: "Wiederherstellen"
caption: "Beschreibung"
loggedInAsBot: "Momentan als Bot angemeldet"
@ -960,7 +963,9 @@ copyErrorInfo: "Fehlerdetails kopieren"
joinThisServer: "Bei dieser Instanz registrieren"
exploreOtherServers: "Eine andere Instanz finden"
letsLookAtTimeline: "Die Chronik durchstöbern"
disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt."
disableFederationConfirm: "Föderation wirklich deaktivieren?"
disableFederationConfirmWarn: "Auch mit deaktivierter Föderation bleiben Notizen, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird dies nicht benötigt."
disableFederationOk: "Deaktivieren"
invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren."
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
postToTheChannel: "In Kanal senden"
@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverl
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
largeNoteReactions: "Reaktionen vergrößert anzeigen"
noteIdOrUrl: "Notiz-ID oder URL"
accountMigration: "Konto-Umzug"
accountMoved: "Dieser Benutzer ist zu einem neuen Konto umgezogen:"
forceShowAds: "Werbung immer anzeigen"
_accountMigration:
moveTo: "Dieses Konto zu einem neuen umziehen"
moveToLabel: "Umzugsziel:"
moveAccountDescription: "Ein Umzug kann nicht rückgängig gemacht werden. Stelle zuerst sicher, dass du auf dem Umzugsziel einen Alias erstellt hast. Gib dann das Umzugsziel in folgendem Format ein: @person@instance.com"
moveFrom: "Von einem anderen Konto zu diesem umziehen"
moveFromLabel: "Umzugsursprung:"
moveFromDescription: "Stelle sicher, dass du auf dem Umzugsursprungskonto einen Alias zu diesem Konto erstellt hast, falls du die Follower des Ursprungskontos übertragen möchtest. Dies muss vor dem Umzug geschehen! Gib dann das Ursprungskonto in folgendem Format an: @person@instance.com"
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden.\n\nÜberprüfe zusätzlich, dass du auf dem Umzugsziel einen Alias eingerichtet hast."
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@ -1255,7 +1273,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Kann Einladungscodes für diese Instanz erstellen"
canInvite: "Erstellung von Einladungscodes für diese Instanz"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität"
pinMax: "Maximale Anzahl an angehefteten Notizen"
@ -1390,6 +1408,8 @@ _channel:
following: "Gefolgt"
usersCount: "{n} Teilnehmer"
notesCount: "{n} Notizen"
nameAndDescription: "Name und Beschreibung"
nameOnly: "Nur Name"
_menuDisplay:
sideFull: "Seitlich"
sideIcon: "Seitlich (Icons)"
@ -1408,7 +1428,7 @@ _instanceMute:
instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz."
instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben"
title: "Blendet Notizen von stummgeschalteten Instanzen aus."
heading: "Liste der stummzuschaltenden Instanzen"
heading: "Stummzuschaltende Instanzen"
_theme:
explore: "Farbschemata erforschen"
install: "Farbschemata installieren"
@ -1680,7 +1700,7 @@ _visibility:
followersDescription: "Nur für Follower sichtbar"
specified: "Direkt"
specifiedDescription: "Nur für bestimmte Benutzer sichtbar"
disableFederation: "Deförderiert"
disableFederation: "Deföderieren"
disableFederationDescription: "Nicht an andere Instanzen übertragen"
_postForm:
replyPlaceholder: "Dieser Notiz antworten …"
@ -1870,6 +1890,7 @@ _deck:
channel: "Kanal"
mentions: "Erwähnungen"
direct: "Direktnachrichten"
roleTimeline: "Rollenchronik"
_dialog:
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"
@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View File

@ -394,4 +394,3 @@ _deck:
mentions: "Επισημάνσεις"
_webhookSettings:
name: "Όνομα"

View File

@ -20,6 +20,7 @@ noNotes: "No notes"
noNotifications: "No notifications"
instance: "Instance"
settings: "Settings"
notificationSettings: "Notification Settings"
basicSettings: "Basic Settings"
otherSettings: "Other Settings"
openInWindow: "Open in window"
@ -67,7 +68,7 @@ import: "Import"
export: "Export"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? It will also vanish from all contents that use it."
unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while."
@ -500,12 +501,13 @@ objectStoragePrefixDesc: "Files will be stored under directories with this prefi
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "Leave this empty if you are using AWS S3, otherwise specify the endpoint as '<host>' or '<host>:<port>', depending on the service you are using."
objectStorageRegion: "Region"
objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, leave this blank or enter 'us-east-1'."
objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, enter 'us-east-1'. Leave empty if using AWS configuration files or environment variables."
objectStorageUseSSL: "Use SSL"
objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for API connections"
objectStorageUseProxy: "Connect over Proxy"
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
objectStorageSetPublicRead: "Set \"public-read\" on upload"
s3ForcePathStyleDesc: "If s3ForcePathStyle is enabled, the bucket name has to included in the path of the URL as opposed to the hostname of the URL. You may need to enable this setting when using services such as a self-hosted Minio instance."
serverLogs: "Server logs"
deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline"
@ -903,6 +905,7 @@ remoteOnly: "Remote only"
failedToUpload: "Upload failed"
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially NSFW."
cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity."
cannotUploadBecauseExceedsFileSizeLimit: "This file could not be uploaded because it exceeds the maximum allowed size."
beta: "Beta"
enableAutoSensitive: "Automatic NSFW-Marking"
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
@ -917,8 +920,9 @@ unsubscribePushNotification: "Disable push notifications"
pushNotificationAlreadySubscribed: "Push notifications are already enabled"
pushNotificationNotSupported: "Your browser or instance does not support push notifications"
sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read"
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable."
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the power consumption of your device."
windowMaximize: "Maximize"
windowMinimize: "Minimize"
windowRestore: "Restore"
caption: "Caption"
loggedInAsBot: "Currently logged in as bot"
@ -960,7 +964,9 @@ copyErrorInfo: "Copy error details"
joinThisServer: "Sign up at this instance"
exploreOtherServers: "Look for another instance"
letsLookAtTimeline: "Have a look at the timeline"
disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting."
disableFederationConfirm: "Really disable federation?"
disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this."
disableFederationOk: "Disable"
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
emailNotSupported: "This instance does not support sending emails"
postToTheChannel: "Post to channel"
@ -983,6 +989,19 @@ retryAllQueuesConfirmText: "This will temporarily increase the server load."
enableChartsForRemoteUser: "Generate remote user data charts"
enableChartsForFederatedInstances: "Generate remote instance data charts"
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
largeNoteReactions: "Enlargen displayed reactions"
noteIdOrUrl: "Note ID or URL"
accountMigration: "Account Migration"
accountMoved: "This user has moved to a new account:"
forceShowAds: "Always show ads"
_accountMigration:
moveTo: "Migrate this account to a different one"
moveToLabel: "Account to move to:"
moveAccountDescription: "This action is irreversible. First, create an alias for this account on the account you wish to move to. Then, enter the account to move to in the following format: @person@instance.com"
moveFrom: "Migrate another account to this one"
moveFromLabel: "Account to move from:"
moveFromDescription: "Create an alias for the account to move from on this account if you wish to transfer its followers. This has to be done before the transfer! Then, enter the account to move to in the following format: @person@instance.com"
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore.\n\nAlso, confirm you've created an alias at the account to migrate to."
_achievements:
earnedAt: "Unlocked at"
_types:
@ -1390,6 +1409,8 @@ _channel:
following: "Followed"
usersCount: "{n} Participants"
notesCount: "{n} Notes"
nameAndDescription: "Name and description"
nameOnly: "Name only"
_menuDisplay:
sideFull: "Side"
sideIcon: "Side (Icons)"
@ -1680,7 +1701,7 @@ _visibility:
followersDescription: "Make visible to your followers only"
specified: "Direct"
specifiedDescription: "Make visible for specified users only"
disableFederation: "Unfederated"
disableFederation: "Defederate"
disableFederationDescription: "Don't transmit to other instances"
_postForm:
replyPlaceholder: "Reply to this note..."
@ -1852,7 +1873,7 @@ _deck:
swapRight: "Swap with the right column"
swapUp: "Swap with the above column"
swapDown: "Swap with the below column"
stackLeft: "Stack with the left column"
stackLeft: "Stack on left column"
popRight: "Pop column to the right"
profile: "Profile"
newProfile: "New profile"
@ -1870,6 +1891,7 @@ _deck:
channel: "Channel"
mentions: "Mentions"
direct: "Direct notes"
roleTimeline: "Role Timeline"
_dialog:
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."
@ -1893,4 +1915,3 @@ _webhookSettings:
renote: "When renoted"
reaction: "When receiving a reaction"
mention: "When being mentioned"

View File

@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión
objectStorageUseProxy: "Conectarse a través de Proxy"
objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión de Almacenamiento de objetos"
objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
s3ForcePathStyleDesc: "Si s3ForcePathStyle esta habilitado el nombre del bucket debe ser especificado como parte de la URL en lugar del nombre de host en la URL. Puede ser necesario activar esta opción cuando se utilice, por ejemplo, Minio en un servidor propio."
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
@ -919,6 +920,7 @@ pushNotificationNotSupported: "El navegador o la instancia no admiten notificaci
sendPushNotificationReadMessage: "Eliminar las notificaciones push después de leer las notificaciones y los mensajes"
sendPushNotificationReadMessageCaption: "La notificación \"{emptyPushNotificationMessage}\" aparecerá momentáneamente. Esto puede aumentar el consumo de batería del dispositivo."
windowMaximize: "Maximizar"
windowMinimize: "Minimizar"
windowRestore: "Regresar"
caption: "Pie de foto"
loggedInAsBot: "Inicio sesión como cuenta bot."
@ -960,7 +962,9 @@ copyErrorInfo: "Copiar detalles del error"
joinThisServer: "Registrarse en esta instancia"
exploreOtherServers: "Buscar otra instancia"
letsLookAtTimeline: "Mirar la línea de tiempo local"
disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración."
disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?"
disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar."
disableFederationOk: "Desactivar."
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
postToTheChannel: "Publicar en el canal"
@ -980,6 +984,21 @@ drivecleaner: "Limpiador del Drive"
retryAllQueuesNow: "Reintentar inmediatamente todas las colas"
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"
showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas"
largeNoteReactions: "Agrandar las reacciones de las notas"
noteIdOrUrl: "ID o URL de la nota"
accountMigration: "Migración de cuenta"
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
_accountMigration:
moveTo: "Mover esta cuenta a una nueva"
moveToLabel: "Cuenta destino:"
moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com"
moveFrom: "Trasladar de otra cuenta a ésta"
moveFromLabel: "Cuenta desde la que se realiza el traslado:"
moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com"
migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado."
_achievements:
earnedAt: "Desbloqueado el"
_types:
@ -1276,6 +1295,8 @@ _role:
followersMoreThanOrEq: "Tiene X o más seguidores"
followingLessThanOrEq: "Sigue X o menos cuentas"
followingMoreThanOrEq: "Sigue X o más cuentas"
notesLessThanOrEq: "El número de notas es inferior o igual a"
notesMoreThanOrEq: "El número de notas es superior o igual a"
and: "Condicional AND"
or: "Condicional OR"
not: "Condicional NOT"
@ -1875,6 +1896,16 @@ _drivecleaner:
orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos"
_webhookSettings:
createWebhook: "Crear Webhook"
name: "Nombre"
secret: "Secreto"
events: "Eventos de webhook"
active: "Activado"
_events:
follow: "Cuando se sigue a alguien"
followed: "Cuando se es seguido"
note: "Cuando se publica una nota"
reply: "Cuando se recibe una respuesta"
renote: "Cuando reciba un \"re-note\""
reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención"

View File

@ -1470,4 +1470,3 @@ _deck:
_webhookSettings:
name: "Nom"
active: "Activé"

View File

@ -1,2 +1 @@
---

View File

@ -1,2 +1 @@
---

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Hapus tanda konten sensitif"
enterFileName: "Masukkan nama berkas"
mute: "Bisukan"
unmute: "Hapus bisukan"
renoteMute: "Matikan renote"
renoteUnmute: "Batal mematikan renote"
block: "Blokir"
unblock: "Buka blokir"
suspend: "Bekukan"
@ -393,11 +395,15 @@ about: "Informasi"
aboutMisskey: "Tentang Misskey"
administrator: "Admin"
token: "Token"
totp: "Aplikasi autentikator"
totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai"
moderator: "Moderator"
moderation: "Moderasi"
nUsersMentioned: "{n} pengguna disebut"
securityKeyAndPasskey: "Security key dan passkey"
securityKey: "Kunci keamanan"
lastUsed: "Terakhir digunakan"
lastUsedAt: "Penggunaan terakhir: {t}"
unregister: "Batalkan pendaftaran"
passwordLessLogin: "Setel login tanpa kata sandi"
resetPassword: "Atur ulang kata sandi"
@ -844,6 +850,7 @@ tenMinutes: "10 Menit"
oneHour: "1 Jam"
oneDay: "1 Hari"
oneWeek: "1 Bulan"
oneMonth: "satu bulan"
reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan."
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
rateLimitExceeded: "Batas sudah terlampaui"
@ -901,6 +908,7 @@ pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pember
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
windowMaximize: "Maksimalkan"
windowMinimize: "Minimalkan"
windowRestore: "Kembalikan"
caption: "Keterangan"
loggedInAsBot: "Sedang login sebagai bot"
@ -939,6 +947,12 @@ collapseRenotes: "Tutup renote yang sudah kamu lihat"
internalServerError: "Kesalahan internal peladen"
internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga"
copyErrorInfo: "Salin detil galat"
joinThisServer: "Gabung server ini"
exploreOtherServers: "Cari server lain"
letsLookAtTimeline: "LIhat timeline"
disableFederationConfirm: "Matikan federasi?"
disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan."
disableFederationOk: "Matikan federasi"
_achievements:
earnedAt: "Terbuka pada"
_types:
@ -1806,4 +1820,3 @@ _deck:
_webhookSettings:
name: "Nama"
active: "Aktif"

View File

@ -20,6 +20,7 @@ noNotes: "Nessuna nota!"
noNotifications: "Nessuna notifica"
instance: "Istanza"
settings: "Impostazioni"
notificationSettings: "Preferenze di notifica"
basicSettings: "Impostazioni generali"
otherSettings: "Altre impostazioni"
openInWindow: "Apri in una finestra"
@ -170,7 +171,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
host: "Server remoto"
selectUser: "Seleziona profilo"
recipient: "Destinatario"
annotation: "Descrizione"
annotation: "Annotazione preventiva"
federation: "Federazione"
instances: "Istanza"
registeredAt: "Registrato presso"
@ -212,7 +213,7 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a
done: "Fine"
processing: "In elaborazione"
preview: "Anteprima"
default: "Medio"
default: "Predefinito"
defaultValueIs: "Predefinito: {value}"
noCustomEmojis: "Nessun emoji"
noJobs: "Nessun lavoro"
@ -237,14 +238,14 @@ more: "Di più!"
featured: "Tendenze"
usernameOrUserId: "Nome utente o ID utente"
noSuchUser: "Nessun utente trovato"
lookup: "Cerca"
lookup: "Ricerca remota"
announcements: "Annunci"
imageUrl: "URL dell'immagine"
remove: "Elimina"
removed: "Eliminato con successo"
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
deleteAreYouSure: "Eliminare \"{x}\"?"
resetAreYouSure: "Reimposta"
resetAreYouSure: "Ripristinare?"
saved: "Salvato"
messaging: "Messaggi"
upload: "Carica"
@ -409,7 +410,7 @@ lastUsedAt: "Uso più recente: {t}"
unregister: "Annulla l'iscrizione"
passwordLessLogin: "Accedi senza password"
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
resetPassword: "Reimposta password"
resetPassword: "Ripristina la password"
newPasswordIs: "La tua nuova password è「{password}」"
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
share: "Condividi"
@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "Disabilita quest'opzione se non utilizzi HTTPS per le
objectStorageUseProxy: "Usa proxy"
objectStorageUseProxyDesc: "Disabilita quest'opzione se non usi proxy per la connessione API."
objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di caricare"
s3ForcePathStyleDesc: "L'attivazione di s3ForcePathStyle impone di specificare il nome del bucket come parte del percorso nell'URL anziché del nome host. Potrebbe tornare utile quando si utilizzano applicazioni come Minio."
serverLogs: "Log del server"
deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
@ -564,7 +566,7 @@ invisibleNote: "Nota invisibile"
enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità"
poll: "Sondaggio"
useCw: "Nascondere media"
useCw: "Content Warning"
enablePlayer: "Visualizza"
disablePlayer: "Chiudi"
expandTweet: "Espandi tweet"
@ -579,7 +581,7 @@ plugins: "Estensioni"
preferencesBackups: "Backup delle impostazioni"
deck: "Deck"
undeck: "Esci dal deck"
useBlurEffectForModal: "Utilizza effetto sfocatura per i modali"
useBlurEffectForModal: "Utilizza effetto sfocatura per le finestre modali"
useFullReactionPicker: "Usa la totalità del pannello di reazioni"
width: "Larghezza"
height: "Altezza"
@ -596,7 +598,7 @@ notificationType: "Tipo di notifiche"
edit: "Modifica"
emailServer: "Server email"
enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per ripristinare la password"
email: "Email"
emailAddress: "Indirizzo di posta elettronica"
smtpConfig: "Impostazioni del server SMTP"
@ -785,7 +787,7 @@ gallery: "Galleria"
recentPosts: "Le più recenti"
popularPosts: "Le più visualizzate"
shareWithNote: "Condividere in nota"
ads: "Pubblicità"
ads: "Banner"
expiration: "Scadenza"
startingperiod: "Periodo di inizio"
memo: "Promemoria"
@ -814,7 +816,7 @@ translatedFrom: "Tradotto 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"
keepCw: "Mantieni il CW"
keepCw: "Mantieni il Content Warning"
pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
resolved: "Risolto"
@ -919,6 +921,7 @@ pushNotificationNotSupported: "Il client o il server non supporta le notifiche p
sendPushNotificationReadMessage: "Elimina le notifiche push dopo la relativa lettura"
sendPushNotificationReadMessageCaption: "Se possibile, verrà mostrata brevemente una notifica con il testo \"{emptyPushNotificationMessage}\". Potrebbe influire negativamente sulla durata della batteria."
windowMaximize: "Ingrandisci"
windowMinimize: "Contrai finestra"
windowRestore: "Ripristina"
caption: "Didascalia"
loggedInAsBot: "Connessione come Bot"
@ -960,16 +963,18 @@ copyErrorInfo: "Copia le informazioni sull'errore"
joinThisServer: "Registrati su questa istanza"
exploreOtherServers: "Trova altre istanze"
letsLookAtTimeline: "Sbircia la timeline"
disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione."
invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore."
disableFederationConfirm: "Vuoi davvero disattivare la federazione?"
disableFederationConfirmWarn: "Anche se defederate, le Note continueranno ad essere pubbliche, se non diversamente specificato. Di solito, non è necessario far questo."
disableFederationOk: "Disabilita federazione"
invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione."
emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica sul canale"
postToTheChannel: "Pubblica nel canale"
cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Accettazione reazioni"
likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti"
rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi reimpostare la password?"
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
sensitiveWords: "Parole sensibili"
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
@ -980,6 +985,22 @@ drivecleaner: "Drive cleaner"
retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
largeNoteReactions: "Ingrandisci le reazioni"
noteIdOrUrl: "ID della Nota o URL"
accountMigration: "Migrazione del profilo"
accountMoved: "Questo profilo ha migrato altrove:"
forceShowAds: "Mostra sempre i banner"
_accountMigration:
moveTo: "Migrare questo profilo verso un un altro"
moveToLabel: "Profilo verso cui migrare"
moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it"
moveFrom: "Migra un altro profilo dentro a questo"
moveFromLabel: "Profilo da cui migrare:"
moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo."
_achievements:
earnedAt: "Data di conseguimento"
_types:
@ -1276,6 +1297,8 @@ _role:
followersMoreThanOrEq: "Ha più di N follower"
followingLessThanOrEq: "Segue N profili o meno"
followingMoreThanOrEq: "Segue N profili o più"
notesLessThanOrEq: "Conteggio Note inferiore o uguale a"
notesMoreThanOrEq: "Conteggio Note maggiore o uguale a"
and: "E"
or: "O"
not: "NON"
@ -1314,8 +1337,8 @@ _ad:
hide: "Nascondi"
_forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
ifNoEmail: "Se nessun indirizzo e-mail è stato registrato, si prega di contattare l'amministratore·trice dell'istanza."
contactAdmin: "Poiché questa istanza non permette l'utilizzo di una mail, si prega di contattare l'amministratore·trice dell'istanza per poter ripristinare la password."
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
_gallery:
my: "Le mie pubblicazioni"
liked: "Pubblicazioni che mi piacciono"
@ -1385,6 +1408,8 @@ _channel:
following: "Seguiti"
usersCount: "{n} partecipanti"
notesCount: "{n} note"
nameAndDescription: "Nome e descrizione"
nameOnly: "Solo il nome"
_menuDisplay:
sideFull: "Laterale"
sideIcon: "Laterale (solo icone)"
@ -1670,12 +1695,12 @@ _visibility:
public: "Pubblica"
publicDescription: "Visibile per tutti sul Fediverso"
home: "Home"
homeDescription: "Visibile solo sulla timeline \"Home\""
homeDescription: "Visibile solo sulla timeline locale"
followers: "Follower"
followersDescription: "Visibile solo per i tuoi follower"
followersDescription: "Visibile solo ai tuoi follower"
specified: "Nota diretta"
specifiedDescription: "Visibile solo ai profili menzionati"
disableFederation: "Interrompi la federazione"
disableFederation: "Federazione disabilitata"
disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm:
replyPlaceholder: "Rispondi a questa nota..."
@ -1870,11 +1895,21 @@ _dialog:
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
_disabledTimeline:
title: "Timeline disabilitata"
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline"
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"
_webhookSettings:
createWebhook: "Creazione Webhook"
name: "Nome"
secret: "Segreto"
events: "Quando eseguire il Webhook"
active: "Attivo"
_events:
follow: "Quando segui un profilo"
followed: "Quando ti segue un profilo"
note: "Quando pubblichi una Nota"
reply: "Quando rispondono ad una Nota"
renote: "Quando la Nota è Rinotata"
reaction: "Quando ricevo una reazione"
mention: "Quando mi menzionano"

View File

@ -20,6 +20,7 @@ noNotes: "ノートはありません"
noNotifications: "通知はありません"
instance: "サーバー"
settings: "設定"
notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "その他の設定"
openInWindow: "ウィンドウで開く"
@ -262,14 +263,16 @@ noMoreHistory: "これより過去の履歴はありません"
startMessaging: "チャットを開始"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
agree: "同意する"
agreeBelow: "下記に同意する"
basicNotesBeforeCreateAccount: "基本的な注意事項"
tos: "利用規約"
termsOfService: "利用規約"
start: "始める"
home: "ホーム"
remoteUserCaution: "リモートユーザーのため、情報が不完全です。"
activity: "アクティビティ"
images: "画像"
image: "画像"
birthday: "誕生日"
yearsOld: "{age}歳"
registeredDate: "登録日"
@ -473,6 +476,8 @@ createAccount: "アカウントを作成"
existingAccount: "既存のアカウント"
regenerate: "再生成"
fontSize: "フォントサイズ"
mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの高さ"
limitTo: "{x}を上限に"
noFollowRequests: "フォロー申請はありません"
openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード"
@ -698,6 +703,8 @@ contact: "連絡先"
useSystemFont: "システムのデフォルトのフォントを使う"
clips: "クリップ"
experimentalFeatures: "実験的機能"
experimental: "実験的"
ThisIsExperimentalFeature: "これは実験的な機能です。仕様が変更されたり、正常に動作しなかったりする可能性があります。"
developer: "開発者"
makeExplorable: "アカウントを見つけやすくする"
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
@ -904,6 +911,7 @@ remoteOnly: "リモートのみ"
failedToUpload: "アップロード失敗"
cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
beta: "ベータ"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
@ -917,9 +925,10 @@ subscribePushNotification: "プッシュ通知を有効化"
unsubscribePushNotification: "プッシュ通知を停止する"
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
sendPushNotificationReadMessage: "通知が既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "端末の電池消費量が増加する可能性があります。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "元に戻す"
caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中"
@ -936,6 +945,7 @@ didYouLikeMisskey: "Misskeyを気に入っていただけましたか"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
roles: "ロール"
role: "ロール"
noRole: "ロールはありません"
normalUser: "一般ユーザー"
undefined: "未定義"
assign: "アサイン"
@ -945,6 +955,10 @@ manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作成することはできません。"
cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
invalidParamError: "パラメータエラー"
invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。"
permissionDeniedError: "操作が拒否されました"
permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。"
preset: "プリセット"
selectFromPresets: "プリセットから選択"
achievements: "実績"
@ -961,7 +975,9 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録する"
exploreOtherServers: "他のサーバーを探す"
letsLookAtTimeline: "タイムラインを見てみる"
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
disableFederationConfirm: "連合なしにしますか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
disableFederationOk: "連合なしにする"
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿"
@ -986,6 +1002,52 @@ enableChartsForFederatedInstances: "リモートサーバーのチャートを
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
video: "動画"
videos: "動画"
dataSaver: "データセーバー"
accountMigration: "アカウントの移行"
accountMoved: "このユーザーは新しいアカウントに移行しました:"
accountMovedShort: "このアカウントは移行されています"
operationForbidden: "この操作はできません"
forceShowAds: "常に広告を表示する"
addMemo: "メモを追加"
editMemo: "メモを編集"
reactionsList: "リアクション一覧"
renotesList: "Renote一覧"
notificationDisplay: "通知の表示"
leftTop: "左上"
rightTop: "右上"
leftBottom: "左下"
rightBottom: "右下"
stackAxis: "スタック方向"
vertical: "縦"
horizontal: "横"
position: "位置"
serverRules: "サーバールール"
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。"
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
continue: "続ける"
preservedUsernames: "予約ユーザー名"
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
_serverRules:
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"
moveFromSub: "別のアカウントへエイリアスを作成"
moveFromLabel: "移行元のアカウント #{n}"
moveFromDescription: "別のアカウントからこのアカウントに移行したい場合、ここでエイリアスを作成しておく必要があります。\n移行元のアカウントをこのように入力してください: @username@server.example.com\n削除するには、入力欄を空にして保存します非推奨。"
moveTo: "このアカウントを新しいアカウントへ移行"
moveToLabel: "移行先のアカウント:"
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできません。"
moveAccountDescription: "新しいアカウントへ移行します。\n ・フォロワーが新しいアカウントを自動でフォローします\n ・このアカウントからのフォローは全て解除されます\n ・このアカウントではートの作成などができなくなります\n\nフォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。\nリスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。\n\nこの説明はこのサーバーMisskey v13.12.0以降の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。"
moveAccountHowTo: "アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。\nエイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com"
startMigration: "移行する"
migrationConfirm: "本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。"
movedAndCannotBeUndone: "\nアカウントは移行されています。\n移行を取り消すことはできません。"
postMigrationNote: "このアカウントからのフォロー解除は移行操作から24時間後に実行されます。\nこのアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。"
movedTo: "移行先のアカウント:"
_achievements:
earnedAt: "獲得日時"
@ -1158,6 +1220,9 @@ _achievements:
_client30min:
title: "ひとやすみ"
description: "クライアントを起動してから30分以上経過した"
_client60min:
title: "Misskeyの見すぎ"
description: "クライアントを起動してから60分以上経過した"
_noteDeletedWithin1min:
title: "いまのなし"
description: "投稿してから1分以内にその投稿を削除した"
@ -1247,6 +1312,8 @@ _role:
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
isExplorable: "ロールタイムラインを公開"
descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
@ -1412,6 +1479,8 @@ _channel:
following: "フォロー中"
usersCount: "{n}人が参加中"
notesCount: "{n}投稿があります"
nameAndDescription: "名前と説明"
nameOnly: "名前のみ"
_menuDisplay:
sideFull: "横"
@ -1925,6 +1994,7 @@ _deck:
channel: "チャンネル"
mentions: "あなた宛て"
direct: "ダイレクト"
roleTimeline: "ロールタイムライン"
_dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
@ -1952,4 +2022,3 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

View File

@ -16,22 +16,23 @@ cancel: "やめとく"
noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenoteしたで"
noNotes: "ノートなんてあらへんで"
noNotifications: "通知なんてあらへんで"
noNotes: "ノートはあらへん"
noNotifications: "通知はあらへん"
instance: "サーバー"
settings: "設定"
notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "ほかの設定"
openInWindow: "ウィンドウで開くで"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介食ってもた"
noAccountDescription: "自己紹介はあらへん"
login: "ログイン"
loggingIn: "ログインしよるで"
logout: "ログアウト"
signup: "新規登録"
uploading: "アップロードしとるで"
save: "保存"
save: "とっとく"
users: "ユーザー"
addUser: "ユーザーを追加や"
favorite: "お気に入り"
@ -81,9 +82,9 @@ followsYou: "フォローされとるで"
createList: "リスト作る"
manageLists: "リストの管理"
error: "エラー"
somethingHappened: "なんかアカンことが起こったで"
somethingHappened: "なんかあかんわ"
retry: "もっぺんやる?"
pageLoadError: "ページの読み込みに失敗してもうたわ…"
pageLoadError: "ページが読み込めんかったわ。"
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
@ -108,8 +109,8 @@ inChannelQuote: "チャンネル内引用"
pinnedNote: "ピン留めされとるノート"
pinned: "ピン留めしとく"
you: "あんた"
clickToShow: "押したら見えるで"
sensitive: "ちょっとアカンやつやで"
clickToShow: "押したら出ら"
sensitive: "気いつけて見いや"
add: "増やす"
reaction: "リアクション"
reactions: "リアクション"
@ -122,8 +123,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
renoteMute: "Renoteは見いひん"
renoteUnmute: "Renoteもやっぱ見るわ"
block: "ブロック"
unblock: "ブロックやめたる"
suspend: "凍結"
@ -141,14 +142,14 @@ editWidgetsExit: "編集終ったで"
customEmojis: "カスタム絵文字"
emoji: "絵文字"
emojis: "絵文字"
emojiName: "絵文字名"
emojiName: "絵文字はんの"
emojiUrl: "絵文字画像URL"
addEmoji: "絵文字を追加"
settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。"
flagAsBot: "Botにするで"
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsCat: "Catやで"
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
@ -194,10 +195,10 @@ network: "ネットワーク"
disk: "ディスク"
instanceInfo: "サーバー情報"
statistics: "統計"
clearQueue: "キューにさいなら"
clearQueueConfirmTitle: "キューをクリアしまっか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュにさいなら"
clearQueue: "キューをほかす"
clearQueueConfirmTitle: "キューをほかしとこか?"
clearQueueConfirmText: "未配達の投稿は配送されなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュをほかす"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
@ -206,7 +207,7 @@ mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートを削除しまっか?"
noteDeleteConfirm: "このノートをほかしてええか?"
pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了したで管理者アカウントを作ってや。"
done: "でけた"
@ -226,9 +227,9 @@ notResponding: "応答してへんで"
instanceFollowing: "サーバーのフォロー"
instanceFollowers: "サーバーのフォロワー\n"
instanceUsers: "サーバーのユーザー"
changePassword: "パスワード変える"
changePassword: "パスワードをいじる"
security: "セキュリティ"
retypedNotMatch: "入れたやつ同じになってないで。"
retypedNotMatch: "入れたやつ合うてへんわ。"
currentPassword: "今のパスワード"
newPassword: "次のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
@ -258,7 +259,7 @@ uploadFromUrlRequested: "アップロードしたい言うといたで"
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
explore: "みつける"
messageRead: "もう読んだ"
noMoreHistory: "これより過去の履歴はあらへんで"
noMoreHistory: "これより昔のんはあらへんで"
startMessaging: "チャットやるで"
nUsersRead: "{n}人が読んでもうた"
agreeTo: "{0}に同意したで"
@ -294,14 +295,14 @@ createFolder: "フォルダー作る"
renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーをほかす"
addFile: "ファイルを追加"
emptyDrive: "ドライブにはなんも残っとらん"
emptyDrive: "ドライブは空っぽや"
emptyFolder: "このフォルダーは空や"
unableToDelete: "消そうおもってんけどな、あかんかったわ"
unableToDelete: "消んかったわ"
inputNewFileName: "今度のファイル名は何にするん?"
inputNewDescription: "新しいキャプションを入れてや"
inputNewFolderName: "今度のフォルダ名は何にするん?"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
hasChildFilesOrFolders: "このフォルダは空っぽちゃうから消されへん"
copyUrl: "URLをコピー"
rename: "名前を変えるで"
avatar: "アイコン"
@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにする
objectStorageUseProxy: "Proxyを使う"
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん"
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
s3ForcePathStyleDesc: "s3ForcePathStyleを使たらバケット名をURLのホスト名やなくてパスの一部として必ず指定させるようになるで。セルフホストされたMinioとかを使うてるんやったら有効にせなあかん場合があるで。"
serverLogs: "サーバーログ"
deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
@ -643,7 +645,7 @@ reporter: "通報者"
reporteeOrigin: "通報先"
reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
send: "送信"
abuseMarkAsResolved: "対応したで"
openInNewTab: "新しいタブで開く"
@ -739,7 +741,7 @@ capacity: "容量"
inUse: "使用中"
editCode: "コードを編集"
apply: "適用"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
emailNotification: "メール通知"
publish: "公開"
inChannelSearch: "チャンネル内検索"
@ -767,7 +769,7 @@ active: "アクティブ"
offline: "オフライン"
notRecommended: "あんま推奨しやんで"
botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック"
instanceBlocking: "サーバーブロック"
selectAccount: "アカウントを選んでなー"
switchAccount: "アカウントを変えるで"
enabled: "有効"
@ -851,8 +853,8 @@ themeColor: "テーマカラー"
size: "大きさ"
numberOfColumn: "列の数"
searchByGoogle: "探す"
instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ"
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ"
instanceDefaultLightTheme: "サーバーおすすめの明るいテーマ"
instanceDefaultDarkTheme: "サーバーおすすめのの暗いテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
mutePeriod: "ミュートする期間"
period: "期限"
@ -866,7 +868,7 @@ reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
rateLimitExceeded: "レート制限が超えたみたいやで"
cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしたってええか?"
cropImageAsk: "画像をクロップしええか?"
cropYes: "切り抜いたる"
cropNo: "切り抜かへん"
file: "ファイル"
@ -901,11 +903,11 @@ sensitiveMediaDetection: "センシティブなメディアの検出"
localOnly: "ローカルのみ"
remoteOnly: "リモートのみ"
failedToUpload: "アップロードに失敗してもうたわ…"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたでアップロードできまへん。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いでアップロードできまへん。"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへんわ。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへんわ。"
beta: "ベータ"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、インスタンスによっては自動で設定されることがあるで。"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
navbar: "ナビゲーションバー"
shuffle: "シャッフルするで"
@ -915,10 +917,11 @@ pushNotification: "プッシュ通知"
subscribePushNotification: "プッシュ通知をオンにするで"
unsubscribePushNotification: "プッシュ通知を止めるで"
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "元に戻す"
caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中やで"
@ -931,7 +934,7 @@ numberOfLikes: "いいね数"
show: "表示"
neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Misskeyを気に入っとっただけましたん"
didYouLikeMisskey: "Misskey気に入ってくれた"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
roles: "ロール"
role: "ロール"
@ -941,7 +944,7 @@ assign: "アサイン"
unassign: "アサインを解除"
color: "色"
manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作れなさそうや"
youCannotCreateAnymore: "これ以上作れなさそうや"
cannotPerformTemporary: "一時的に利用できへんで"
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
preset: "プリセット"
@ -960,7 +963,9 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録するわ"
exploreOtherServers: "他のサーバー見てみる"
letsLookAtTimeline: "タイムライン見てみーや"
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。"
disableFederationConfirm: "連合なしにしとくか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。"
disableFederationOk: "連合なしにしとく"
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿"
@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きする"
noteIdOrUrl: "ートIDかURL"
accountMigration: "アカウントのお引っ越し"
accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
forceShowAds: "常に広告を表示しとく"
_accountMigration:
moveTo: "このアカウントをさらのアカウントに引っ越すで"
moveToLabel: "引っ越し先のアカウント:"
moveAccountDescription: "この操作は戻されへんで。まず引っ越し先のアカウントでこのアカウントへのエイリアスが作れたか確認してきなはれや。エイリアスができてたら、引っ越し先のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したかったら、ここでエイリアスを作っとく必要があるで。必ずお引っ越しを実行する前に作っとかなあかんで!引っ越し元のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃんと確認しーや"
_achievements:
earnedAt: "貰った日ぃ"
_types:
@ -1043,7 +1061,7 @@ _achievements:
_login7:
title: "ビギナーⅡ"
description: "通算7日ログインした"
flavor: "慣れてきたんちゃう?"
flavor: "慣れてきたんちゃう?"
_login15:
title: "ビギナーⅢ"
description: "通算15日ログインした"
@ -1147,7 +1165,7 @@ _achievements:
_iLoveMisskey:
title: "Misskey好きやねん"
description: "\"I ❤ #Misskey\"を投稿した"
flavor: "Misskeyを使ってくれてありがとう by 開発チーム"
flavor: "Misskeyを使ってくれておおきに by 開発チーム"
_foundTreasure:
title: "なんでも鑑定団"
description: "隠されたお宝を発見した"
@ -1173,7 +1191,7 @@ _achievements:
description: "ホームタイムラインの流速が20npmを超す"
_viewInstanceChart:
title: "アナリスト"
description: "インスタンスのチャートを表示した"
description: "サーバーのチャートを表示した"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "スクラッチパッドで hello worldを出力した"
@ -1210,7 +1228,7 @@ _achievements:
_loggedInOnNewYearsDay:
title: "あけましておめでとうございます!"
description: "元旦にログインした"
flavor: "今年も弊インスタンスをよろしくお願いします"
flavor: "今年も弊サーバーをよろしゅう頼みますわ"
_cookieClicked:
title: "クッキー叩くやつ"
description: "クッキー叩いてもうた"
@ -1225,8 +1243,8 @@ _role:
name: "ロール名"
description: "ロールの説明"
permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はインスタンスの全ての設定を変更できるで。"
assignTarget: "アサインターゲット"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はサーバーの全ての設定を変更できるで。"
assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。"
manual: "マニュアル"
conditional: "コンディショナル"
@ -1255,7 +1273,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canInvite: "インスタンス招待コードの発行"
canInvite: "サーバー招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数"
@ -1267,7 +1285,7 @@ _role:
userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか"
_condition:
@ -1320,7 +1338,7 @@ _ad:
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
contactAdmin: "このインスタンスはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
contactAdmin: "このサーバーはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
_gallery:
my: "あんたの投稿"
liked: "いいねした投稿"
@ -1390,6 +1408,8 @@ _channel:
following: "フォロー中やで"
usersCount: "{n}人が参加中やで"
notesCount: "{n}こ投稿があるで"
nameAndDescription: "名前と説明"
nameOnly: "名前だけ"
_menuDisplay:
sideFull: "横"
sideIcon: "横(アイコン)"
@ -1405,10 +1425,10 @@ _wordMute:
hard: "ハード"
mutedNotes: "ミュートされたノート"
_instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するんやで"
title: "設定したインスタンスのノートを隠すで。"
heading: "ミュートするインスタンス"
title: "設定したサーバーのノートを隠すで。"
heading: "ミュートするサーバー"
_theme:
explore: "テーマを探す"
install: "テーマのインストール"
@ -1630,7 +1650,7 @@ _widgets:
digitalClock: "デジタル時計"
unixClock: "UNIX時計"
federation: "連合"
instanceCloud: "インスタンスクラウド"
instanceCloud: "サーバークラウド"
postForm: "投稿フォーム"
slideshow: "スライドショー"
button: "ボタン"
@ -1681,7 +1701,7 @@ _visibility:
specified: "ダイレクト"
specifiedDescription: "選んだユーザーのみに公開するで"
disableFederation: "連合なし"
disableFederationDescription: "他インスタンスへは送らんとくわ"
disableFederationDescription: "他サーバーへは送らんとくわ"
_postForm:
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
@ -1870,6 +1890,7 @@ _deck:
channel: "チャンネル"
mentions: "あんた宛て"
direct: "ダイレクト"
roleTimeline: "ロールタイムライン"
_dialog:
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}"
@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "Renoteされるとき"
reaction: "リアクションがあるとき~!"
mention: "メンションがあるとき~!"

View File

@ -1,2 +1 @@
---

View File

@ -103,4 +103,3 @@ _deck:
_columns:
notifications: "Ilɣuyen"
list: "Tibdarin"

View File

@ -83,4 +83,3 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ"

View File

@ -958,7 +958,6 @@ copyErrorInfo: "오류 정보 복사"
joinThisServer: "이 서버에 가입"
exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
postToTheChannel: "채널에 게시하기"
@ -1851,4 +1850,3 @@ _dialog:
_webhookSettings:
name: "이름"
active: "활성화"

View File

@ -163,11 +163,15 @@ instanceInfo: "ອີນສະແຕນ"
statistics: "ສະຖິຕິ"
clearQueue: "ລ້າງຄິວ"
clearCachedFiles: "ລຶບລ້າງແຄສ"
noUsers: "ບໍ່ພົບຜູ້ໃຊ້"
editProfile: "ແກ້ໄຂໂປຣໄຟລ໌"
done: "ສຳເລັດ"
processing: "ກຳລັງປະມວນຜົນ"
preview: "ສະແດງເປັນຕົວຢ່າງ"
default: "ຄ່າເລີ່ມຕົ້ນ"
defaultValueIs: "ຄ່າເລີ່ມຕົ້ນ: {value}"
noCustomEmojis: "ບໍ່ມີອີໂມຈິ"
noJobs: "ບໍ່ມີຊິ້ນວຽກ"
federating: "ສະຫະພັນ"
blocked: "ບລັອກແລ້ວ "
suspended: "ໂຈະ"
@ -182,6 +186,9 @@ changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ"
security: "ຄວາມປອດໄພ"
retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
newPassword: "ລະຫັດຜ່ານໃໝ່"
newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ"
attachFile: "ແນບໄຟລ໌"
more: "ເພີ່ມເຕີມ!"
featured: "ໄຮໄລທ໌"
usernameOrUserId: "ຊື່ຜູ້ໃຊ້ ຫຼື id ຜູ້ໃຊ້"
@ -196,25 +203,31 @@ saved: "ບັນທຶກແລ້ວ"
messaging: "ແຊ໋ດ"
upload: "ອັບໂຫຼດ"
keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
fromDrive: "ຈາກ Drive"
fromUrl: "ຈາກ URL"
uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ"
messageRead: "ອ່ານແລ້ວ"
startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
nUsersRead: "ອ່ານໂດຍ {n}"
tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ"
start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ"
home: "ໜ້າຫຼັກ"
activity: "ກິດຈະກຳ"
images: "ຮູບພາບ"
birthday: "ວັນເກີດ"
yearsOld: "{age} ປີ"
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
location: "ທີ່ຕັ້ງ"
theme: "ແທ໋ມ"
themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ"
themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ"
light: "ສະຫວ່າງ"
dark: "ມືດ"
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
darkThemes: "ຮູບແບບສີສັນມືດ"
syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ"
drive: "ຂັບ"
fileName: "ຊື່ໄຟລ໌"
selectFile: "ເລືອກໄຟລ໌"
@ -265,6 +278,9 @@ invite: "ເຊີນ"
driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
turnstileSiteKey: "ກະແຈໄຊທ໌"
turnstileSecretKey: "ກະແຈລັບ"
name: "ຊື່"
userList: "ລາຍການ"
about: "ກ່ຽວກັບ"
aboutMisskey: "ກ່ຽວກັບ Misskey"
@ -326,6 +342,7 @@ _widgets:
instanceInfo: "ອີນສະແຕນ"
notifications: "ການແຈ້ງເຕືອນ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
activity: "ກິດຈະກຳ"
federation: "ສະຫະພັນ"
_userList:
chooseList: "ເລືອກບັນຊີລາຍການ"
@ -335,6 +352,7 @@ _visibility:
home: "ໜ້າຫຼັກ"
followers: "ຜູ້ຕິດຕາມ"
_profile:
name: "ຊື່"
username: "ຊື່ຜູ້ໃຊ້"
_exportOrImport:
followingList: "ກຳລັງຕິດຕາມ"
@ -368,4 +386,5 @@ _deck:
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ"
_webhookSettings:
name: "ຊື່"

View File

@ -485,4 +485,3 @@ _deck:
mentions: "Vermeldingen"
_webhookSettings:
name: "Naam"

View File

@ -1,3 +1,2 @@
---
_lang_: "Norsk Bokmål"

View File

@ -1379,4 +1379,3 @@ _deck:
_webhookSettings:
name: "Nazwa"
active: "Właczono"

View File

@ -557,4 +557,3 @@ _deck:
direct: "Notas diretas"
_webhookSettings:
name: "Nome"

View File

@ -703,4 +703,3 @@ _deck:
mentions: "Mențiuni"
_webhookSettings:
name: "Nume"

View File

@ -950,7 +950,6 @@ copyErrorInfo: "Скопировать код ошибки"
joinThisServer: "Присоединяйтесь к этому серверу"
exploreOtherServers: "Искать другие сервера"
letsLookAtTimeline: "Давайте посмотрим на ленту"
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
_achievements:
earnedAt: "Разблокировано в"
_types:
@ -1839,4 +1838,3 @@ _dialog:
_webhookSettings:
name: "Название"
active: "Вкл."

View File

@ -1,2 +1 @@
---

View File

@ -1477,4 +1477,3 @@ _deck:
_webhookSettings:
name: "Názov"
active: "Zapnuté"

View File

@ -343,9 +343,16 @@ recentlyRegisteredUsers: "Nyligen registrerade användare"
userList: "Listor"
aboutMisskey: "Om Misskey"
administrator: "Administratör"
passwordLessLogin: "Lösenordsfri inloggning"
passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey."
resetPassword: "Återställ Lösenord"
newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela"
enable: "Aktivera"
weakPassword: "Svagt Lösenord"
normalPassword: "Medel Lösenord"
strongPassword: "Starkt Lösenord"
signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt."
serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
enableInfiniteScroll: "Ladda mer automatiskt"
enablePlayer: "Öppna videospelare"
@ -354,11 +361,14 @@ enableEmail: "Aktivera epost-utskick"
smtpHost: "Värd"
smtpUser: "Användarnamn"
smtpPass: "Lösenord"
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
clearCache: "Rensa cache"
onlineUsersCount: "{n} användare är online"
enabled: "Aktiverad"
user: "Användare"
global: "Global"
squareAvatars: "Visa fyrkantiga profilbilder"
incorrectPassword: "Fel lösenord."
searchByGoogle: "Sök"
file: "Filer"
enableAutoSensitive: "Automatisk NSFW markering"
@ -368,6 +378,15 @@ subscribePushNotification: "Aktivera pushnotiser"
unsubscribePushNotification: "Avaktivera pushnotiser"
pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade"
pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser"
windowMaximize: "Maximera"
windowMinimize: "Minimera"
windowRestore: "Återställ"
resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
_achievements:
_types:
_open3windows:
title: "Flera Fönster"
description: "Ha minst 3 fönster öppna samtidigt"
_email:
_follow:
title: "följde dig"
@ -384,6 +403,7 @@ _sfx:
chat: "Chatt"
antenna: "Antenner"
_2fa:
passwordToTOTP: "Skriv in ditt lösenord"
renewTOTPCancel: "Nej tack"
_antennaSources:
all: "Alla noter"
@ -444,4 +464,3 @@ _deck:
mentions: "Omnämningar"
_webhookSettings:
active: "Aktiverad"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "ยกเลิกทำเครื่องหมายเ
enterFileName: "พิมพ์ชื่อไฟล์"
mute: "ปิดเสียง"
unmute: "ยกเลิกการปิดเสียง"
renoteMute: "ปิดเสียงรีโน้ต"
renoteUnmute: "เปิดเสียง รีโน้ต"
block: "บล็อค"
unblock: "เลิกปิดกั้น"
suspend: "ถูกระงับ"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์
flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้"
autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม"
addAccount: "เพิ่มบัญชี"
reloadAccountsList: "รีโหลดรายการบัญชีใหม่"
loginFailed: "การเข้าสู่ระบบไม่สำเร็จ"
showOnRemote: "ดูบนอินสแตนซ์ระยะไกล"
general: "ทั่วไป"
@ -503,6 +506,7 @@ objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้
objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี"
objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API"
objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด"
s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ"
serverLogs: "บันทึกของเซิร์ฟเวอร์"
deleteAll: "ลบทั้งหมด"
showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์"
@ -545,7 +549,9 @@ userSilenced: "ผู้ใช้รายนี้กำลังถูกป
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
tokenRevoked: "โทเค็นไม่ถูกต้อง"
tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ"
accountDeleted: "ลบบัญชีแล้ว"
accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ"
menu: "เมนู"
divider: "ตัวแบ่ง"
addItem: "เพิ่มรายการ"
@ -914,6 +920,7 @@ pushNotificationNotSupported: "เบราว์เซอร์หรืออ
sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
sendPushNotificationReadMessageCaption: "การแจ้งเตือนที่มีข้อความ \"{emptyPushNotificationMessage}\" จะแสดงขึ้นมาในช่วงระยะเวลาสั้นๆ การดำเนินการนี้อาจทำให้เพิ่มการใช้งานแบตเตอรี่ของอุปกรณ์ถ้าหากมีนะ"
windowMaximize: "ขยายใหญ่สุดแล้ว"
windowMinimize: "ย่อเล็กที่สุด"
windowRestore: "เลิกทำ"
caption: "รายละเอียด"
loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้"
@ -955,12 +962,17 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?"
disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ"
disableFederationOk: "ปิดการใช้งาน"
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
reactionAcceptance: "การยอมรับรีแอคชั่น"
likeOnly: "ที่ชอบเท่านั้น"
likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น"
rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
@ -972,6 +984,22 @@ drivecleaner: "ทำความสะอาดไดรฟ์"
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
largeNoteReactions: "ขยายรีแอคชั่นการแสดงผล"
noteIdOrUrl: "โน้ต ID หรือ URL"
accountMigration: "การโยกย้ายบัญชี"
accountMoved: "ผู้ใช้รายนี้ได้ย้ายไปยังบัญชีใหม่แล้ว:"
forceShowAds: "แสดงโฆษณาเสมอ"
_accountMigration:
moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง"
moveToLabel: "บัญชีที่จะย้ายไปที่:"
moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com"
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
moveFromLabel: "บัญชีที่จะย้ายจาก:"
moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ"
_achievements:
earnedAt: "ได้รับเมื่อ"
_types:
@ -1268,6 +1296,8 @@ _role:
followersMoreThanOrEq: "จำนวนผู้ติดตามมากกว่าหรือเท่ากับ\n"
followingLessThanOrEq: "จำนวนบัญชีต่อไปนี้คือ น้อยกว่าหรือเท่ากับ"
followingMoreThanOrEq: "จำนวนบัญชีต่อไปนี้คือ มากกว่าหรือเท่ากับ"
notesLessThanOrEq: "จำนวนโพสต์น้อยกว่าเท่ากับ"
notesMoreThanOrEq: "จำนวนโพสต์มากกว่าเท่ากับ"
and: "และ"
or: "หรือ"
not: "ไม่"
@ -1867,6 +1897,16 @@ _drivecleaner:
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
_webhookSettings:
createWebhook: "สร้าง Webhook"
name: "ชื่อ"
secret: "ความลับ"
events: "อีเว้นท์ Webhook"
active: "เปิดใช้งาน"
_events:
follow: "เมื่อกำลังติดตามผู้ใช้"
followed: "เมื่อกำลังติดตามแล้ว"
note: "เมื่อกำลังโพสต์โน้ต"
reply: "เมื่อได้รับการตอบกลับ"
renote: "รีโน้ตแล้วเมื่อ"
reaction: "เมื่อได้รับรีแอคชั่น"
mention: "เมื่อกำลังถูกกล่าวถึง"

View File

@ -60,4 +60,3 @@ _deck:
_columns:
notifications: "Bildirim"
tl: "Zaman çizelgesi"

View File

@ -2,4 +2,3 @@
_lang_: "ياپونچە"
search: "ئىزدەش"
searchByGoogle: "ئىزدەش"

View File

@ -1641,4 +1641,3 @@ _deck:
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"

View File

@ -1707,4 +1707,3 @@ _dialog:
_webhookSettings:
name: "Tên"
active: "Đã bật"

View File

@ -16,16 +16,17 @@ cancel: "取消"
noThankYou: "不用,谢谢"
enterUsername: "输入用户名"
renotedBy: "由 {user} 转贴"
noNotes: "没有帖"
noNotes: "没有帖"
noNotifications: "无通知"
instance: "服务器"
settings: "设置"
notificationSettings: "通知设置"
basicSettings: "基本设置"
otherSettings: "其他设置"
openInWindow: "在新窗口中打开"
profile: "个人资料"
timeline: "时间线"
noAccountDescription: "这个人很懒,没有写自我介绍"
noAccountDescription: "此用户尚无自我介绍"
login: "登录"
loggingIn: "正在登录..."
logout: "登出"
@ -60,7 +61,7 @@ youGotNewFollower: "你有新的关注者"
receiveFollowRequest: "您收到了关注请求"
followRequestAccepted: "您的关注请求被通过了"
mention: "提及"
mentions: "提"
mentions: "提到我的"
directNotes: "私信"
importAndExport: "导入和导出"
import: "导入"
@ -85,7 +86,7 @@ somethingHappened: "出现了一些问题!"
retry: "重试"
pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
serverIsDead: "没有服务器响应。 请稍后再试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称"
privacy: "隐私"
@ -95,7 +96,7 @@ follow: "关注"
followRequest: "关注申请"
followRequests: "关注申请"
unfollow: "取消关注"
followRequestPending: "发送关注请求"
followRequestPending: "关注请求批准中"
enterEmoji: "输入表情符号"
renote: "转发"
unrenote: "取消转发"
@ -119,7 +120,7 @@ rememberNoteVisibility: "保存上次设置的可见性"
attachCancel: "删除附件"
markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名"
enterFileName: "输入文件名"
mute: "屏蔽"
unmute: "解除屏蔽"
renoteMute: "屏蔽转帖"
@ -142,18 +143,18 @@ customEmojis: "自定义表情符号"
emoji: "表情符号"
emojis: "表情符号"
emojiName: "表情符号名称"
emojiUrl: "表情符号地址"
emojiUrl: "emoji 地址"
addEmoji: "添加表情符号"
settingGuide: "推荐配置"
cacheRemoteFiles: "远程文件缓存"
cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此户识别为机器人。"
flagAsBotDescription: "如果此户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此户识别为机器人。"
flagAsCat: "将这个账户设定为一只猫"
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。"
flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许关注者的关注"
autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败"
@ -203,7 +204,7 @@ blockedInstances: "被阻拦的服务器"
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户"
blockedUsers: "拉黑的用户"
blockedUsers: "拉黑的用户"
noUsers: "无用户"
editProfile: "编辑资料"
noteDeleteConfirm: "要删除该帖子吗?"
@ -336,7 +337,7 @@ enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
registration: "注册"
enableRegistration: "允许新用户注册"
enableRegistration: "允许任何人注册"
invite: "邀请"
driveCapacityPerLocalAccount: "每个用户的网盘空间"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
@ -354,7 +355,7 @@ pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha"
hcaptchaSiteKey: "网站密钥"
hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
hcaptchaSecretKey: "密钥"
recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥"
@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "如果不使用https进行API连接请关闭。"
objectStorageUseProxy: "使用代理"
objectStorageUseProxyDesc: "如果您不使用代理进行API连接请将其关闭。"
objectStorageSetPublicRead: "上传时设置为public-read"
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
serverLogs: "服务器日志"
deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框"
@ -919,6 +921,7 @@ pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息"
sendPushNotificationReadMessage: "删除已读推送通知消息"
sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "还原"
caption: "标题"
loggedInAsBot: "以Bot账户登录"
@ -960,7 +963,9 @@ copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "时间线"
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。"
disableFederationConfirm: "确定要禁用联合?"
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
disableFederationOk: "联合禁用"
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道"
@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
largeNoteReactions: "使用大图标来显示回应"
noteIdOrUrl: "帖子ID或URL"
accountMigration: "账户迁移"
accountMoved: "此用户已迁移账户"
forceShowAds: "总是显示广告"
_accountMigration:
moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户"
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com"
moveFrom: "从别的账号迁移到此账户"
moveFromLabel: "迁移前的账户"
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com"
migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。"
_achievements:
earnedAt: "达成时间"
_types:
@ -1255,7 +1273,7 @@ _role:
gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖"
canInvite: "发放实例邀请码"
canInvite: "发放服务器邀请码"
canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量"
pinMax: "帖子置顶数量限制"
@ -1390,6 +1408,8 @@ _channel:
following: "正在关注"
usersCount: "有{n}人参与"
notesCount: "有{n}个帖子"
nameAndDescription: "名称与描述"
nameOnly: "仅名称"
_menuDisplay:
sideFull: "横向"
sideIcon: "横向(图标)"
@ -1629,7 +1649,7 @@ _widgets:
photos: "照片"
digitalClock: "数字时钟"
unixClock: "UNIX时钟"
federation: "联邦宇宙"
federation: "联"
instanceCloud: "服务器云"
postForm: "投稿窗口"
slideshow: "幻灯片展示"
@ -1870,6 +1890,7 @@ _deck:
channel: "频道"
mentions: "提及"
direct: "指定用户"
roleTimeline: "角色时间线"
_dialog:
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View File

@ -20,6 +20,7 @@ noNotes: "無貼文。"
noNotifications: "沒有通知"
instance: "實例"
settings: "設定"
notificationSettings: "通知選項"
basicSettings: "基本設定"
otherSettings: "其他設定"
openInWindow: "在新視窗開啟"
@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "如果不使用https進行API連接請關閉"
objectStorageUseProxy: "使用網路代理"
objectStorageUseProxyDesc: "如果不使用代理進行API連接請關閉"
objectStorageSetPublicRead: "上傳時設定為\"public-read\""
s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 會強制將儲存槽名稱指定為 URL 中路徑的一部分,而不是主機名。 使用自託管 Minio 之類的可能需要啟用。"
serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
@ -560,7 +562,7 @@ inboxUrl: "收件夾URL"
addedRelays: "已加入的中繼"
serviceworkerInfo: "您需要啟用推送通知"
deletedNote: "已删除的貼文"
invisibleNote: "隱藏的貼文"
invisibleNote: "私密的貼文"
enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "可見性"
poll: "投票"
@ -919,6 +921,7 @@ pushNotificationNotSupported: "瀏覽器或實例不支援推播通知"
sendPushNotificationReadMessage: "通知與訊息如果已讀的話,就將推播通知刪除"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會增加設備的電池消耗。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "復原"
caption: "標題"
loggedInAsBot: "以機器人帳戶登入中"
@ -960,7 +963,9 @@ copyErrorInfo: "複製錯誤資訊"
joinThisServer: "在此伺服器上註冊"
exploreOtherServers: "探索其他伺服器"
letsLookAtTimeline: "看看時間軸"
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
disableFederationConfirm: "要停止聯邦功能嗎?"
disableFederationConfirmWarn: "即使停止了聯邦功能,貼文也不會變成私密的。在大部分的情況下,沒有必要停止聯邦功能。"
disableFederationOk: "停止聯邦功能"
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道"
@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
largeNoteReactions: "將貼文的反應放大顯示"
noteIdOrUrl: "貼文ID或URL"
accountMigration: "遷移帳戶"
accountMoved: "這個使用者已遷移至新的帳戶:"
forceShowAds: "總是顯示廣告"
_accountMigration:
moveTo: "將這個帳戶遷移至新的帳戶"
moveToLabel: "要遷移到的帳戶:"
moveAccountDescription: "這個操作不可撤銷。首先,請確認已在要遷移到的帳戶中為這個帳戶建立了一個別名。建立別名之後,像這樣輸入你要遷移到的帳戶:@person@instance.com"
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromLabel: "要遷移過來的帳戶:"
moveFromDescription: "如果你想把跟隨者從別的帳戶遷移過來,必須先在這裡建立別名。請務必在執行遷移之前建立別名!請像這樣輸入要遷移的帳戶:@person@instance.com"
migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外請確認在要遷移到的帳戶已經建立了一個別名。"
_achievements:
earnedAt: "獲得日期"
_types:
@ -1390,6 +1408,8 @@ _channel:
following: "關注中"
usersCount: "有{n}人參與"
notesCount: "有{n}個貼文"
nameAndDescription: "名稱與說明"
nameOnly: "僅名稱"
_menuDisplay:
sideFull: "側向"
sideIcon: "側向(圖示)"
@ -1870,6 +1890,7 @@ _deck:
channel: "頻道"
mentions: "提及"
direct: "指定使用者"
roleTimeline: "角色時間軸"
_dialog:
charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}"
charactersBelow: "低於最少字數!現在 {current} / 限制 {max}"
@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.10.3",
"version": "13.11.3",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@8.1.0",
"packageManager": "pnpm@8.3.1",
"workspaces": [
"packages/frontend",
"packages/backend",
@ -16,6 +16,7 @@
"scripts": {
"build-pre": "node ./scripts/build-pre.js",
"build": "pnpm build-pre && pnpm -r build && pnpm gulp",
"build-storybook": "pnpm --filter frontend build-storybook",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
"init": "pnpm migrate",
@ -50,19 +51,19 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "5.0.2"
"typescript": "5.0.4"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.59.0",
"cross-env": "7.0.3",
"cypress": "12.9.0",
"eslint": "8.37.0",
"cypress": "12.10.0",
"eslint": "8.38.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.2.0"
"@tensorflow/tfjs-core": "4.4.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,8 +1,15 @@
import Redis from 'ioredis';
import { loadConfig } from './built/config.js';
import { createRedisConnection } from './built/redis.js';
const config = loadConfig();
const redis = createRedisConnection(config);
const redis = new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => {

View File

@ -0,0 +1,18 @@
export class UserMemo1680702787050 {
name = 'UserMemo1680702787050'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "user_memo" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "targetUserId" character varying(32) NOT NULL, "memo" character varying(2048) NOT NULL, CONSTRAINT "PK_e9aaa58f7d3699a84d79078f4d9" PRIMARY KEY ("id")); COMMENT ON COLUMN "user_memo"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "user_memo"."targetUserId" IS 'The ID of target user.'; COMMENT ON COLUMN "user_memo"."memo" IS 'Memo.'`);
await queryRunner.query(`CREATE INDEX "IDX_650b49c5639b5840ee6a2b8f83" ON "user_memo" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_66ac4a82894297fd09ba61f3d3" ON "user_memo" ("targetUserId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_faef300913c738265638ba3ebc" ON "user_memo" ("userId", "targetUserId") `);
await queryRunner.query(`ALTER TABLE "user_memo" ADD CONSTRAINT "FK_650b49c5639b5840ee6a2b8f83e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_memo" ADD CONSTRAINT "FK_66ac4a82894297fd09ba61f3d35" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_memo" DROP CONSTRAINT "FK_66ac4a82894297fd09ba61f3d35"`);
await queryRunner.query(`ALTER TABLE "user_memo" DROP CONSTRAINT "FK_650b49c5639b5840ee6a2b8f83e"`);
await queryRunner.query(`DROP TABLE "user_memo"`);
}
}

View File

@ -0,0 +1,17 @@
export class AvatarUrlAndBannerUrl1680775031481 {
name = 'AvatarUrlAndBannerUrl1680775031481'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`);
}
}

View File

@ -0,0 +1,17 @@
export class AccountMove1680931179228 {
name = 'AccountMove1680931179228'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "movedToUri" character varying(512)`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
await queryRunner.query(`ALTER TABLE "user" ADD "alsoKnownAs" text`);
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAs"`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedToUri"`);
}
}

View File

@ -0,0 +1,11 @@
export class ServerRules1681400427971 {
name = 'ServerRules1681400427971'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "serverRules" character varying(280) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverRules"`);
}
}

View File

@ -0,0 +1,12 @@
export class RoleTLSetting1681870960239 {
name = 'RoleTLSetting1681870960239'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "isExplorable" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "isExplorable"`);
}
}

View File

@ -0,0 +1,13 @@
export class MovedAt1682190963894 {
name = 'MovedAt1682190963894'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "movedAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedAt" IS 'When the user moved to another account'`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."movedAt" IS 'When the user moved to another account'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedAt"`);
}
}

View File

@ -0,0 +1,11 @@
export class PreservedUsernames1682754135458 {
name = 'PreservedUsernames1682754135458'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "preservedUsernames" character varying(1024) array NOT NULL DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "preservedUsernames"`);
}
}

View File

@ -22,42 +22,42 @@
"test-and-coverage": "pnpm jest-and-coverage"
},
"optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.42",
"@swc/core-darwin-x64": "^1.3.42",
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
"@swc/core-linux-arm64-gnu": "^1.3.42",
"@swc/core-linux-arm64-musl": "^1.3.42",
"@swc/core-linux-x64-gnu": "^1.3.42",
"@swc/core-linux-x64-musl": "^1.3.42",
"@swc/core-win32-arm64-msvc": "^1.3.42",
"@swc/core-win32-ia32-msvc": "^1.3.42",
"@swc/core-win32-x64-msvc": "^1.3.42",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.3.51",
"@swc/core-darwin-x64": "1.3.51",
"@swc/core-linux-arm-gnueabihf": "1.3.51",
"@swc/core-linux-arm64-gnu": "1.3.51",
"@swc/core-linux-arm64-musl": "1.3.51",
"@swc/core-linux-x64-gnu": "1.3.51",
"@swc/core-linux-x64-musl": "1.3.51",
"@swc/core-win32-arm64-msvc": "1.3.51",
"@swc/core-win32-ia32-msvc": "1.3.51",
"@swc/core-win32-x64-msvc": "1.3.51",
"@tensorflow/tfjs": "4.4.0",
"@tensorflow/tfjs-node": "4.4.0"
},
"dependencies": {
"@aws-sdk/client-s3": "3.301.0",
"@aws-sdk/lib-storage": "3.301.0",
"@aws-sdk/node-http-handler": "3.296.0",
"@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
"@aws-sdk/client-s3": "3.315.0",
"@aws-sdk/lib-storage": "3.315.0",
"@aws-sdk/node-http-handler": "3.310.0",
"@bull-board/api": "5.0.1",
"@bull-board/fastify": "5.0.1",
"@bull-board/ui": "5.0.1",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.1",
"@fastify/http-proxy": "9.0.0",
"@fastify/multipart": "7.5.0",
"@fastify/static": "6.9.0",
"@fastify/multipart": "7.6.0",
"@fastify/static": "6.10.1",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.12",
"@nestjs/core": "9.3.12",
"@nestjs/testing": "9.3.12",
"@nestjs/common": "9.4.0",
"@nestjs/core": "9.4.0",
"@nestjs/testing": "9.4.0",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.42",
"@swc/core": "1.3.51",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@ -82,16 +82,16 @@
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "12.6.0",
"happy-dom": "8.9.0",
"happy-dom": "9.8.2",
"hpagent": "1.2.0",
"ioredis": "4.28.5",
"ioredis": "5.3.2",
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "21.1.1",
"json5": "2.2.3",
"jsonld": "8.1.1",
"jsrsasign": "10.7.0",
"jsrsasign": "10.8.2",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
@ -119,7 +119,7 @@
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
"rndstr": "1.0.0",
"rss-parser": "3.12.0",
"rss-parser": "3.13.0",
"rxjs": "7.8.0",
"s-age": "1.1.2",
"sanitize-html": "2.10.0",
@ -136,8 +136,8 @@
"tsc-alias": "1.8.5",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typescript": "5.0.2",
"typeorm": "0.3.15",
"typescript": "5.0.4",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "9.0.0",
@ -149,7 +149,7 @@
},
"devDependencies": {
"@jest/globals": "29.5.0",
"@swc/jest": "0.2.24",
"@swc/jest": "0.2.26",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2",
@ -159,7 +159,6 @@
"@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10",
"@types/jest": "29.5.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1",
@ -190,11 +189,11 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.59.0",
"aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3",
"eslint": "8.37.0",
"eslint": "8.38.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",

View File

@ -1,19 +1,16 @@
import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
const config = loadConfig();
const $config: Provider = {
provide: DI.config,
useValue: config,
useValue: loadConfig(),
};
const $db: Provider = {
@ -28,18 +25,47 @@ const $db: Provider = {
const $redis: Provider = {
provide: DI.redis,
useFactory: (config) => {
const redisClient = createRedisConnection(config);
return redisClient;
return new Redis.Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
},
inject: [DI.config],
};
const $redisSubscriber: Provider = {
provide: DI.redisSubscriber,
const $redisForPub: Provider = {
provide: DI.redisForPub,
useFactory: (config) => {
const redisSubscriber = createRedisConnection(config);
redisSubscriber.subscribe(config.host);
return redisSubscriber;
const redis = new Redis.Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
return redis;
},
inject: [DI.config],
};
const $redisForSub: Provider = {
provide: DI.redisForSub,
useFactory: (config) => {
const redis = new Redis.Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
redis.subscribe(config.host);
return redis;
},
inject: [DI.config],
};
@ -47,14 +73,15 @@ const $redisSubscriber: Provider = {
@Global()
@Module({
imports: [RepositoryModule],
providers: [$config, $db, $redis, $redisSubscriber],
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule],
providers: [$config, $db, $redis, $redisForPub, $redisForSub],
exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
@Inject(DI.db) private db: DataSource,
@Inject(DI.redis) private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis,
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
) {}
async onApplicationShutdown(signal: string): Promise<void> {
@ -69,7 +96,8 @@ export class GlobalModule implements OnApplicationShutdown {
await Promise.all([
this.db.destroy(),
this.redisClient.disconnect(),
this.redisSubscriber.disconnect(),
this.redisForPub.disconnect(),
this.redisForSub.disconnect(),
]);
}
}

View File

@ -4,7 +4,7 @@
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
/**
@ -25,6 +25,14 @@ export type Source = {
disableCache?: boolean;
extra?: { [x: string]: string };
};
dbReplications?: boolean;
dbSlaves?: {
host: string;
port: number;
db: string;
user: string;
pass: string;
}[];
redis: {
host: string;
port: number;
@ -33,6 +41,22 @@ export type Source = {
db?: number;
prefix?: string;
};
redisForPubsub?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForJobQueue?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
elasticsearch: {
host: string;
port: number;
@ -60,8 +84,10 @@ export type Source = {
deliverJobConcurrency?: number;
inboxJobConcurrency?: number;
relashionshipJobConcurrency?: number;
deliverJobPerSec?: number;
inboxJobPerSec?: number;
relashionshipJobPerSec?: number;
deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number;
@ -91,6 +117,8 @@ export type Mixin = {
mediaProxy: string;
externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null;
redisForPubsub: NonNullable<Source['redisForPubsub']>;
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
};
export type Config = Source & Mixin;
@ -106,10 +134,11 @@ const dir = `${_dirname}/../../../.config`;
/**
* Path of configuration file
*/
const path = process.env.NODE_ENV === 'test'
? `${dir}/test.yml`
: `${dir}/default.yml`;
const path = process.env.MISSKEY_CONFIG_YML
? resolve(dir, process.env.MISSKEY_CONFIG_YML)
: process.env.NODE_ENV === 'test'
? resolve(dir, 'test.yml')
: resolve(dir, 'default.yml');
export function loadConfig() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
@ -151,6 +180,8 @@ export function loadConfig() {
: null;
if (!config.redis.prefix) config.redis.prefix = mixin.host;
if (config.redisForPubsub == null) config.redisForPubsub = config.redis;
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis;
return Object.assign(config, mixin);
}

View File

@ -56,6 +56,11 @@ export const FILE_TYPE_BROWSERSAFE = [
'audio/webm',
'audio/aac',
// see https://github.com/misskey-dev/misskey/pull/10686
'audio/flac',
'audio/wav',
// backward compatibility
'audio/x-flac',
'audio/vnd.wave',
];

View File

@ -0,0 +1,347 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import type { User } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { QueueService } from '@/core/QueueService.js';
import { RelayService } from '@/core/RelayService.js';
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 { CacheService } from '@/core/CacheService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { MetaService } from '@/core/MetaService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
@Injectable()
export class AccountMoveService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private apPersonService: ApPersonService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private perUserFollowingChart: PerUserFollowingChart,
private federatedInstanceService: FederatedInstanceService,
private instanceChart: InstanceChart,
private metaService: MetaService,
private relayService: RelayService,
private cacheService: CacheService,
private queueService: QueueService,
) {
}
/**
* Move a local account to a new account.
*
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
*/
@bindThis
public async moveFromLocal(src: LocalUser, dst: LocalUser | RemoteUser): Promise<unknown> {
const srcUri = this.userEntityService.getUserUri(src);
const dstUri = this.userEntityService.getUserUri(dst);
// add movedToUri to indicate that the user has moved
const update = {} as Partial<LocalUser>;
update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri];
update.movedToUri = dstUri;
update.movedAt = new Date();
await this.usersRepository.update(src.id, update);
Object.assign(src, update);
// Update cache
this.cacheService.uriPersonCache.set(srcUri, src);
const srcPerson = await this.apRendererService.renderPerson(src);
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
await this.apDeliverManagerService.deliverToFollowers(src, updateAct);
this.relayService.deliverToRelays(src, updateAct);
// Deliver Move activity to the followers of the old account
const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst));
await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
// Unfollow after 24 hours
const followings = await this.followingsRepository.findBy({
followerId: src.id,
});
this.queueService.createDelayedUnfollowJob(followings.map(following => ({
from: { id: src.id },
to: { id: following.followeeId },
})), process.env.NODE_ENV === 'test' ? 10000 : 1000 * 60 * 60 * 24);
await this.postMoveProcess(src, dst);
return iObj;
}
@bindThis
public async postMoveProcess(src: User, dst: User): Promise<void> {
// Copy blockings and mutings, and update lists
try {
await Promise.all([
this.copyBlocking(src, dst),
this.copyMutings(src, dst),
this.updateLists(src, dst),
]);
} catch {
/* skip if any error happens */
}
// follow the new account
const proxy = await this.proxyAccountService.fetch();
const followings = await this.followingsRepository.findBy({
followeeId: src.id,
followerHost: IsNull(), // follower is local
followerId: proxy ? Not(proxy.id) : undefined,
});
const followJobs = followings.map(following => ({
from: { id: following.followerId },
to: { id: dst.id },
})) as RelationshipJobData[];
// Decrease following count instead of unfollowing.
try {
await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src);
} catch {
/* skip if any error happens */
}
// Should be queued because this can cause a number of follow per one move.
this.queueService.createFollowJob(followJobs);
}
@bindThis
public async copyBlocking(src: ThinUser, dst: ThinUser): Promise<void> {
// Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving.
// So block the destination account here.
const srcBlockings = await this.blockingsRepository.findBy({ blockeeId: src.id });
const dstBlockings = await this.blockingsRepository.findBy({ blockeeId: dst.id });
const blockerIds = dstBlockings.map(blocking => blocking.blockerId);
// reblock the destination account
const blockJobs: RelationshipJobData[] = [];
for (const blocking of srcBlockings) {
if (blockerIds.includes(blocking.blockerId)) continue; // skip if already blocked
blockJobs.push({ from: { id: blocking.blockerId }, to: { id: dst.id } });
}
// no need to unblock the old account because it may be still functional
this.queueService.createBlockJob(blockJobs);
}
@bindThis
public async copyMutings(src: ThinUser, dst: ThinUser): Promise<void> {
// Insert new mutings with the same values except mutee
const oldMutings = await this.mutingsRepository.findBy([
{ muteeId: src.id, expiresAt: IsNull() },
{ muteeId: src.id, expiresAt: MoreThan(new Date()) },
]);
if (oldMutings.length === 0) return;
// Check if the destination account is already indefinitely muted by the muter
const existingMutingsMuterUserIds = await this.mutingsRepository.findBy(
{ muteeId: dst.id, expiresAt: IsNull() },
).then(mutings => mutings.map(muting => muting.muterId));
const newMutings: Map<string, { muterId: string; muteeId: string; createdAt: Date; expiresAt: Date | null; }> = new Map();
// 重複しないようにIDを生成
const genId = (): string => {
let id: string;
do {
id = this.idService.genId();
} while (newMutings.has(id));
return id;
};
for (const muting of oldMutings) {
if (existingMutingsMuterUserIds.includes(muting.muterId)) continue; // skip if already muted indefinitely
newMutings.set(genId(), {
...muting,
createdAt: new Date(),
muteeId: dst.id,
});
}
const arrayToInsert = Array.from(newMutings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
await this.mutingsRepository.insert(arrayToInsert);
}
/**
* Update lists while moving accounts.
* - No removal of the old account from the lists
* - Users number limit is not checked
*
* @param src ThinUser (old account)
* @param dst User (new account)
* @returns Promise<void>
*/
@bindThis
public async updateLists(src: ThinUser, dst: User): Promise<void> {
// Return if there is no list to be updated.
const oldJoinings = await this.userListJoiningsRepository.find({
where: {
userId: src.id,
},
});
if (oldJoinings.length === 0) return;
const existingUserListIds = await this.userListJoiningsRepository.find({
where: {
userId: dst.id,
},
}).then(joinings => joinings.map(joining => joining.userListId));
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
// 重複しないようにIDを生成
const genId = (): string => {
let id: string;
do {
id = this.idService.genId();
} while (newJoinings.has(id));
return id;
};
for (const joining of oldJoinings) {
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
newJoinings.set(genId(), {
createdAt: new Date(),
userId: dst.id,
userListId: joining.userListId,
});
}
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
await this.userListJoiningsRepository.insert(arrayToInsert);
// 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 } }]);
}
}
}
@bindThis
private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise<void> {
if (localFollowerIds.length === 0) return;
// Set the old account's following and followers counts to 0.
await this.usersRepository.update({ id: oldAccount.id }, { followersCount: 0, followingCount: 0 });
// Decrease following counts of local followers by 1.
await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1);
// Decrease follower counts of local followees by 1.
const oldFollowings = await this.followingsRepository.findBy({ followerId: oldAccount.id });
if (oldFollowings.length > 0) {
await this.usersRepository.decrement({ id: In(oldFollowings.map(following => following.followeeId)) }, 'followersCount', 1);
}
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
if (this.userEntityService.isRemoteUser(oldAccount)) {
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
});
}
// FIXME: expensive?
for (const followerId of localFollowerIds) {
this.perUserFollowingChart.update({ id: followerId, host: null }, oldAccount, false);
}
}
/**
* dstユーザーのalsoKnownAsをfetchPersonしていきmovedToUrlをdstに指定するユーザーが存在するのかを調べる
*
* @param dst movedToUrlを指定するユーザー
* @param check
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
* @returns Promise<LocalUser | RemoteUser | null>
*/
@bindThis
public async validateAlsoKnownAs(
dst: LocalUser | RemoteUser,
check: (oldUser: LocalUser | RemoteUser | null, newUser: LocalUser | RemoteUser) => boolean | Promise<boolean> = () => true,
instant = false,
): Promise<LocalUser | RemoteUser | null> {
let resultUser: LocalUser | RemoteUser | null = null;
if (this.userEntityService.isRemoteUser(dst)) {
if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
await this.apPersonService.updatePerson(dst.uri);
}
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
}
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) return null;
const dstUri = this.userEntityService.getUserUri(dst);
for (const srcUri of dst.alsoKnownAs) {
try {
let src = await this.apPersonService.fetchPerson(srcUri);
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
if (this.userEntityService.isRemoteUser(dst)) {
if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
await this.apPersonService.updatePerson(srcUri);
}
src = await this.apPersonService.fetchPerson(srcUri) ?? src;
}
if (src.movedToUri === dstUri) {
if (await check(resultUser, src)) {
resultUser = src;
}
if (instant && resultUser) return resultUser;
}
} catch {
/* skip if any error happens */
}
}
return resultUser;
}
}

View File

@ -64,6 +64,7 @@ export const ACHIEVEMENT_TYPES = [
'iLoveMisskey',
'foundTreasure',
'client30min',
'client60min',
'noteDeletedWithin1min',
'postedAtLateNight',
'postedAt0min0sec',

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import type { Antenna } from '@/models/entities/Antenna.js';
import type { Note } from '@/models/entities/Note.js';
import type { User } from '@/models/entities/User.js';
@ -27,8 +27,8 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
this.antennasFetched = false;
this.antennas = [];
this.redisSubscriber.on('message', this.onRedisMessage);
this.redisForSub.on('message', this.onRedisMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onRedisMessage);
this.redisForSub.off('message', this.onRedisMessage);
}
@bindThis
@ -91,16 +91,26 @@ export class AntennaService implements OnApplicationShutdown {
}
@bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
this.redisClient.xadd(
public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise<void> {
const antennas = await this.getAntennas();
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
const redisPipeline = this.redisClient.pipeline();
for (const antenna of matchedAntennas) {
redisPipeline.xadd(
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
`${this.idService.parse(note.id).date.getTime()}-*`,
'*',
'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
}
redisPipeline.exec();
}
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
@bindThis

View File

@ -1,7 +1,7 @@
import { promisify } from 'node:util';
import { Inject, Injectable } from '@nestjs/common';
import redisLock from 'redis-lock';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { UserProfile, UsersRepository } from '@/models/index.js';
import * as Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
@ -16,18 +16,41 @@ export class CacheService implements OnApplicationShutdown {
public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>;
public userMutingsCache: RedisKVCache<string[]>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Set<string>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
@ -36,10 +59,64 @@ export class CacheService implements OnApplicationShutdown {
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', 1000 * 60 * 60 * 24, 1000 * 60);
this.userMutingsCache = new RedisKVCache<string[]>(this.redisClient, 'userMutings', 1000 * 60 * 60 * 24, 1000 * 60);
this.redisSubscriber.on('message', this.onMessage);
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
});
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.renoteMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'renoteMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@ -90,6 +167,6 @@ export class CacheService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
import { AntennaService } from './AntennaService.js';
@ -40,7 +41,7 @@ import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService
import { UserBlockingService } from './UserBlockingService.js';
import { CacheService } from './CacheService.js';
import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
@ -119,6 +120,7 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
@ -161,7 +163,7 @@ const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthentic
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
@ -242,6 +244,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
AccountMoveService,
AccountUpdateService,
AiService,
AntennaService,
@ -284,7 +287,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserBlockingService,
CacheService,
UserFollowingService,
UserKeypairStoreService,
UserKeypairService,
UserListService,
UserMutingService,
UserSuspendService,
@ -359,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
$AntennaService,
@ -401,7 +405,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserBlockingService,
$CacheService,
$UserFollowingService,
$UserKeypairStoreService,
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserSuspendService,
@ -477,6 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
AccountMoveService,
AccountUpdateService,
AiService,
AntennaService,
@ -519,7 +524,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserBlockingService,
CacheService,
UserFollowingService,
UserKeypairStoreService,
UserKeypairService,
UserListService,
UserMutingService,
UserSuspendService,
@ -593,6 +598,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
$AntennaService,
@ -635,7 +641,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserBlockingService,
$CacheService,
$UserFollowingService,
$UserKeypairStoreService,
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserSuspendService,

View File

@ -1,24 +1,29 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js';
import type { EmojisRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js';
@Injectable()
export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config)
private config: Config,
@ -32,9 +37,22 @@ export class CustomEmojiService {
private idService: IdService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) {
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
fromRedisConverter: (value) => {
if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す)
return new Map(JSON.parse(value).map((x: Serialized<Emoji>) => [x.name, {
...x,
updatedAt: x.updatedAt ? new Date(x.updatedAt) : null,
}]));
},
});
}
@bindThis
@ -60,7 +78,7 @@ export class CustomEmojiService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
await this.db.queryResultCache?.remove(['meta_emojis']);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id),
@ -70,6 +88,162 @@ export class CustomEmojiService {
return emoji;
}
@bindThis
public async update(id: Emoji['id'], data: {
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
}): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
});
this.localEmojisCache.refresh();
const updated = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
});
}
}
@bindThis
public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(aliases))],
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
aliases: aliases,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !aliases.includes(x)),
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
category: category,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
license: license,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async delete(id: Emoji['id']) {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
await this.emojisRepository.delete(emoji.id);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
}
@bindThis
public async deleteBulk(ids: Emoji['id'][]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: await this.emojiEntityService.packDetailedMany(emojis),
});
}
@bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト
@ -84,7 +258,7 @@ export class CustomEmojiService {
}
@bindThis
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
@ -116,16 +290,7 @@ export class CustomEmojiService {
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
const isLocal = emoji.host == null;
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
const url = isLocal
? emojiUrl
: this.config.proxyRemoteFiles
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
: emojiUrl;
return url;
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
}
/**
@ -143,30 +308,6 @@ export class CustomEmojiService {
return res;
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
/**
*
*/

View File

@ -36,8 +36,5 @@ export class DeleteAccountService {
await this.usersRepository.update(user.id, {
isDeleted: true,
});
// Terminate streaming
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
}

View File

@ -86,10 +86,14 @@ export class DownloadService {
const contentDisposition = res.headers['content-disposition'];
if (contentDisposition != null) {
try {
const parsed = parse(contentDisposition);
if (parsed.parameters.filename) {
filename = parsed.parameters.filename;
}
} catch (e) {
this.logger.warn(`Failed to parse content-disposition: ${contentDisposition}`, { stack: e });
}
}
}).on('downloadProgress', (progress: Got.Progress) => {
if (progress.transferred > maxSize) {

View File

@ -59,6 +59,8 @@ type AddFileArgs = {
uri?: string | null;
/** Mark file as sensitive */
sensitive?: boolean | null;
/** Extension to force */
ext?: string | null;
requestIp?: string | null;
requestHeaders?: Record<string, string> | null;
@ -125,7 +127,7 @@ export class DriveService {
/***
* Save file
* @param path Path for original
* @param name Name for original
* @param name Name for original (should be extention corrected)
* @param type Content-Type for original
* @param hash Hash for original
* @param size Size for original
@ -151,7 +153,7 @@ export class DriveService {
}
// 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、
// 許可されているファイル形式でしか拡張子をつけない
// 許可されているファイル形式でしかURLに拡張子をつけない
if (!FILE_TYPE_BROWSERSAFE.includes(type)) {
ext = '';
}
@ -173,7 +175,7 @@ export class DriveService {
//#region Uploads
this.registerLogger.info(`uploading original: ${key}`);
const uploads = [
this.upload(key, fs.createReadStream(path), type, ext, name),
this.upload(key, fs.createReadStream(path), type, null, name),
];
if (alts.webpublic) {
@ -189,7 +191,7 @@ export class DriveService {
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext));
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`));
}
await Promise.all(uploads);
@ -396,8 +398,9 @@ export class DriveService {
);
}
// Expire oldest file (without avatar or banner) of remote user
@bindThis
private async deleteOldFile(user: RemoteUser) {
private async expireOldFile(user: RemoteUser, driveCapacity: number) {
const q = this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId', { userId: user.id })
.andWhere('file.isLink = FALSE');
@ -410,12 +413,17 @@ export class DriveService {
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
}
//This selete is hard coded, be careful if change database schema
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
q.orderBy('file.id', 'ASC');
const oldFile = await q.getOne();
const fileList = await q.getRawMany();
const exceedFileIds = fileList.filter((x: any) => x.acc_usage > driveCapacity).map((x: any) => x.file_id);
if (oldFile) {
this.deleteFile(oldFile, true);
for (const fileId of exceedFileIds) {
const file = await this.driveFilesRepository.findOneBy({ id: fileId });
if (file == null) continue;
this.deleteFile(file, true);
}
}
@ -437,6 +445,7 @@ export class DriveService {
sensitive = null,
requestIp = null,
requestHeaders = null,
ext = null,
}: AddFileArgs): Promise<DriveFile> {
let skipNsfwCheck = false;
const instance = await this.metaService.fetch();
@ -468,7 +477,7 @@ export class DriveService {
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
// extを付加してデータベースの文字数制限に当たることはまずない
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
info.type.ext,
ext ?? info.type.ext,
);
if (user && !force) {
@ -489,22 +498,19 @@ export class DriveService {
//#region Check drive usage
if (user && !isLink) {
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
const isLocalUser = this.userEntityService.isLocalUser(user);
const policies = await this.roleService.getUserPolicies(user.id);
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
this.registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
// If usage limit exceeded
if (usage + info.size > driveCapacity) {
if (this.userEntityService.isLocalUser(user)) {
if (driveCapacity < usage + info.size) {
if (isLocalUser) {
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
} else {
// (アバターまたはバナーを含まず)最も古いファイルを削除する
this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser);
}
await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser, driveCapacity - info.size);
}
}
//#endregion

View File

@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
@ -9,23 +10,41 @@ import { bindThis } from '@/decorators.js';
@Injectable()
export class FederatedInstanceService {
private cache: MemoryKVCache<Instance>;
public federatedInstanceCache: RedisKVCache<Instance | null>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private utilityService: UtilityService,
private idService: IdService,
) {
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => {
const parsed = JSON.parse(value);
if (parsed == null) return null;
return {
...parsed,
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
};
},
});
}
@bindThis
public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host);
const cached = this.cache.get(host);
const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached;
const index = await this.instancesRepository.findOneBy({ host });
@ -37,24 +56,27 @@ export class FederatedInstanceService {
firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.cache.set(host, i);
this.federatedInstanceCache.set(host, i);
return i;
} else {
this.cache.set(host, index);
this.federatedInstanceCache.set(host, index);
return index;
}
}
@bindThis
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
host = this.utilityService.toPuny(host);
const cached = this.cache.get(host);
if (cached == null) return;
this.cache.set(host, {
...cached,
...data,
public async update(id: Instance['id'], data: Partial<Instance>): Promise<void> {
const result = await this.instancesRepository.createQueryBuilder().update()
.set(data)
.where('id = :id', { id })
.returning('*')
.execute()
.then((response) => {
return response.raw[0];
});
const updated = result.raw[0];
this.federatedInstanceCache.set(updated.host, updated);
}
}

View File

@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { LoggerService } from '@/core/LoggerService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom';
type NodeInfo = {
@ -42,6 +43,7 @@ export class FetchInstanceMetadataService {
private appLockService: AppLockService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
private federatedInstanceService: FederatedInstanceService,
) {
this.logger = this.loggerService.getLogger('metadata', 'cyan');
}
@ -96,7 +98,7 @@ export class FetchInstanceMetadataService {
if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor;
await this.instancesRepository.update(instance.id, updates);
await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
} catch (e) {

View File

@ -5,7 +5,7 @@ import * as stream from 'node:stream';
import * as util from 'node:util';
import { Injectable } from '@nestjs/common';
import { FSWatcher } from 'chokidar';
import { fileTypeFromFile } from 'file-type';
import * as fileType from 'file-type';
import FFmpeg from 'fluent-ffmpeg';
import isSvg from 'is-svg';
import probeImageSize from 'probe-image-size';
@ -301,6 +301,19 @@ export class FileInfoService {
return fs.promises.access(path).then(() => true, () => false);
}
@bindThis
public fixMime(mime: string | fileType.MimeType): string {
// see https://github.com/misskey-dev/misskey/pull/10686
if (mime === "audio/x-flac") {
return "audio/flac";
}
if (mime === "audio/vnd.wave") {
return "audio/wav";
}
return mime;
}
/**
* Detect MIME Type and extension
*/
@ -315,7 +328,7 @@ export class FileInfoService {
return TYPE_OCTET_STREAM;
}
const type = await fileTypeFromFile(path);
const type = await fileType.fileTypeFromFile(path);
if (type) {
// XMLはSVGかもしれない
@ -324,7 +337,7 @@ export class FileInfoService {
}
return {
mime: type.mime,
mime: this.fixMime(type.mime),
ext: type.ext,
};
}

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
import type { UserList } from '@/models/entities/UserList.js';
@ -14,12 +14,13 @@ import type {
MainStreamTypes,
NoteStreamTypes,
UserListStreamTypes,
UserStreamTypes,
RoleTimelineStreamTypes,
} from '@/server/api/stream/types.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { Role } from '@/models';
@Injectable()
export class GlobalEventService {
@ -27,8 +28,8 @@ export class GlobalEventService {
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForPub)
private redisForPub: Redis.Redis,
) {
}
@ -38,7 +39,7 @@ export class GlobalEventService {
{ type: type, body: null } :
{ type: type, body: value };
this.redisClient.publish(this.config.host, JSON.stringify({
this.redisForPub.publish(this.config.host, JSON.stringify({
channel: channel,
message: message,
}));
@ -49,11 +50,6 @@ export class GlobalEventService {
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishUserEvent<K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void {
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
@ -87,6 +83,11 @@ export class GlobalEventService {
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: Role['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishNotesStream(note: Packed<'Note'>): void {
this.publish('notesStream', null, note);

View File

@ -3,10 +3,11 @@ import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js';
import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js';
import { parseUlid } from '@/misc/id/ulid.js';
@Injectable()
export class IdService {
@ -37,11 +38,10 @@ export class IdService {
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
case 'objectid':
case 'meid': return parseMeid(id);
case 'meidg': return parseMeidg(id);
case 'ulid': return parseUlid(id);
default: throw new Error('unrecognized id generation method');
}
}

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js';
import { MemoryCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
private cache: MemoryCache<LocalUser>;
private cache: MemorySingleCache<LocalUser>;
constructor(
@Inject(DI.usersRepository)
@ -19,7 +19,7 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new MemoryCache<LocalUser>(Infinity);
this.cache = new MemorySingleCache<LocalUser>(Infinity);
}
@bindThis

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { Meta } from '@/models/entities/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.db)
private db: DataSource,
@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
}, 1000 * 60 * 5);
}
this.redisSubscriber.on('message', this.onMessage);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
clearInterval(this.intervalId);
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@ -1,7 +1,7 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { MemoryCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@ -47,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -329,7 +329,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'*',
'note', note.id);
}
@ -493,26 +493,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
});
// Antenna
for (const antenna of (await this.antennaService.getAntennas())) {
this.antennaService.checkHitAntenna(antenna, note, user).then(hit => {
if (hit) {
this.antennaService.addNoteToAntenna(antenna, note, user);
}
});
}
// Channel
if (note.channelId) {
this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => {
for (const following of followings) {
this.noteReadService.insertNoteUnread(following.followerId, note, {
isSpecified: false,
isMentioned: false,
});
}
});
}
this.antennaService.addNoteToAntennas(note, user);
if (data.reply) {
this.saveReply(data.reply, note);
@ -566,6 +547,8 @@ export class NoteCreateService implements OnApplicationShutdown {
this.globalEventService.publishNotesStream(noteObj);
this.roleService.addNoteToRoleTimeline(noteObj);
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {

View File

@ -1,28 +1,20 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Note } from '@/models/entities/Note.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@ -32,18 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
) {
}
@ -54,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
@ -74,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId,
};
@ -92,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
if (params.isSpecified) {
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
}
if (note.channelId) {
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
}, () => { /* aborted, ignore it */ });
}
@ -102,22 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
public async read(
userId: User['id'],
notes: (Note | Packed<'Note'>)[],
info?: {
following: Set<User['id']>;
followingChannels: Set<Channel['id']>;
},
): Promise<void> {
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
@ -125,17 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note);
}
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
}
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
// TODO: ↓まとめてクエリしたい
@ -159,16 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
});
this.noteUnreadsRepository.countBy({
userId: userId,
noteChannelId: Not(IsNull()),
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
}
}

View File

@ -1,5 +1,5 @@
import { setTimeout } from 'node:timers/promises';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
@ -43,6 +43,7 @@ export class NotificationService implements OnApplicationShutdown {
@bindThis
public async readAllNotification(
userId: User['id'],
force = false,
) {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
@ -57,7 +58,7 @@ export class NotificationService implements OnApplicationShutdown {
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
return this.postReadAllNotifications(userId);
}
}
@ -65,6 +66,7 @@ export class NotificationService implements OnApplicationShutdown {
@bindThis
private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
}
@bindThis
@ -73,7 +75,7 @@ export class NotificationService implements OnApplicationShutdown {
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
const profile = await this.cacheService.userProfileCache.fetch(notifieeId, () => this.userProfilesRepository.findOneByOrFail({ userId: notifieeId }));
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
const isMuted = profile.mutingNotificationTypes.includes(type);
if (isMuted) return null;
@ -82,8 +84,8 @@ export class NotificationService implements OnApplicationShutdown {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId, () => this.mutingsRepository.findBy({ muterId: notifieeId }).then(xs => xs.map(x => x.muteeId)));
if (mutings.includes(data.notifierId)) {
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
if (mutings.has(data.notifierId)) {
return null;
}
}
@ -95,10 +97,10 @@ export class NotificationService implements OnApplicationShutdown {
...data,
} as Notification;
this.redisClient.xadd(
const redisIdPromise = this.redisClient.xadd(
`notificationTimeline:${notifieeId}`,
'MAXLEN', '~', '300',
`${this.idService.parse(notification.id).date.getTime()}-*`,
'*',
'data', JSON.stringify(notification));
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
@ -109,7 +111,7 @@ export class NotificationService implements OnApplicationShutdown {
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
if (latestReadNotificationId && (latestReadNotificationId >= notification.id)) return;
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);

View File

@ -1,12 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscriptionsRepository } from '@/models/index.js';
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { RedisKVCache } from '@/misc/cache.js';
// Defined also packages/sw/types.ts#L13
type PushNotificationsTypes = {
@ -15,6 +17,7 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string };
note: Packed<'Note'>;
};
'readAllNotifications': undefined;
};
// Reduce length because push message servers have character limits
@ -40,15 +43,27 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
@Injectable()
export class PushNotificationService {
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository,
private metaService: MetaService,
) {
this.subscriptionsCache = new RedisKVCache<SwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
lifetime: 1000 * 60 * 60 * 1, // 1h
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: (key) => this.swSubscriptionsRepository.findBy({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
}
@bindThis
@ -62,12 +77,13 @@ export class PushNotificationService {
meta.swPublicKey,
meta.swPrivateKey);
// Fetch
const subscriptions = await this.swSubscriptionsRepository.findBy({
userId: userId,
});
const subscriptions = await this.subscriptionsCache.fetch(userId);
for (const subscription of subscriptions) {
if ([
'readAllNotifications',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {

View File

@ -3,18 +3,18 @@ import Bull from 'bull';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from '../queue/types.js';
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
function q<T>(config: Config, name: string, limitPerSec = -1) {
return new Bull<T>(name, {
redis: {
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
db: config.redis.db ?? 0,
port: config.redisForJobQueue.port,
host: config.redisForJobQueue.host,
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
password: config.redisForJobQueue.pass,
db: config.redisForJobQueue.db ?? 0,
},
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
limiter: limitPerSec > 0 ? {
max: limitPerSec,
duration: 1000,
@ -41,7 +41,8 @@ export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue<DbJobData>;
export type DbQueue = Bull.Queue<DbJobData<keyof DbJobMap>>;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
@ -75,6 +76,12 @@ const $db: Provider = {
inject: [DI.config],
};
const $relationship: Provider = {
provide: 'queue:relationship',
useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
inject: [DI.config],
};
const $objectStorage: Provider = {
provide: 'queue:objectStorage',
useFactory: (config: Config) => q(config, 'objectStorage'),
@ -96,6 +103,7 @@ const $webhookDeliver: Provider = {
$deliver,
$inbox,
$db,
$relationship,
$objectStorage,
$webhookDeliver,
],
@ -105,6 +113,7 @@ const $webhookDeliver: Provider = {
$deliver,
$inbox,
$db,
$relationship,
$objectStorage,
$webhookDeliver,
],

View File

@ -6,9 +6,10 @@ import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { ThinUser } from '../queue/types.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
import Bull from 'bull';
@Injectable()
export class QueueService {
@ -21,6 +22,7 @@ export class QueueService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {}
@ -71,7 +73,7 @@ export class QueueService {
@bindThis
public createDeleteDriveFilesJob(user: ThinUser) {
return this.dbQueue.add('deleteDriveFiles', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -81,7 +83,7 @@ export class QueueService {
@bindThis
public createExportCustomEmojisJob(user: ThinUser) {
return this.dbQueue.add('exportCustomEmojis', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -91,7 +93,7 @@ export class QueueService {
@bindThis
public createExportNotesJob(user: ThinUser) {
return this.dbQueue.add('exportNotes', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -101,7 +103,7 @@ export class QueueService {
@bindThis
public createExportFavoritesJob(user: ThinUser) {
return this.dbQueue.add('exportFavorites', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -111,7 +113,7 @@ export class QueueService {
@bindThis
public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) {
return this.dbQueue.add('exportFollowing', {
user: user,
user: { id: user.id },
excludeMuting,
excludeInactive,
}, {
@ -123,7 +125,7 @@ export class QueueService {
@bindThis
public createExportMuteJob(user: ThinUser) {
return this.dbQueue.add('exportMuting', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -133,7 +135,7 @@ export class QueueService {
@bindThis
public createExportBlockingJob(user: ThinUser) {
return this.dbQueue.add('exportBlocking', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -143,7 +145,7 @@ export class QueueService {
@bindThis
public createExportUserListsJob(user: ThinUser) {
return this.dbQueue.add('exportUserLists', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@ -153,7 +155,7 @@ export class QueueService {
@bindThis
public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importFollowing', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@ -161,10 +163,16 @@ export class QueueService {
});
}
@bindThis
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) {
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel }));
return this.dbQueue.addBulk(jobs);
}
@bindThis
public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importMuting', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@ -175,7 +183,7 @@ export class QueueService {
@bindThis
public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importBlocking', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@ -183,10 +191,32 @@ export class QueueService {
});
}
@bindThis
public createImportBlockingToDbJob(user: ThinUser, targets: string[]) {
const jobs = targets.map(rel => this.generateToDbJobData('importBlockingToDb', { user, target: rel }));
return this.dbQueue.addBulk(jobs);
}
@bindThis
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
name: string,
data: D,
opts: Bull.JobOptions,
} {
return {
name,
data,
opts: {
removeOnComplete: true,
removeOnFail: true,
},
};
}
@bindThis
public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importUserLists', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@ -197,7 +227,7 @@ export class QueueService {
@bindThis
public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importCustomEmojis', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@ -208,7 +238,7 @@ export class QueueService {
@bindThis
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
return this.dbQueue.add('deleteAccount', {
user: user,
user: { id: user.id },
soft: opts.soft,
}, {
removeOnComplete: true,
@ -216,6 +246,58 @@ export class QueueService {
});
}
@bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createDelayedUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[], delay: number) {
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel, { delay }));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createUnblockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
const jobs = blockings.map(rel => this.generateRelationshipJobData('unblock', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
name: string,
data: RelationshipJobData,
opts: Bull.JobOptions,
} {
return {
name,
data: {
from: { id: data.from.id },
to: { id: data.to.id },
silent: data.silent,
requestId: data.requestId,
},
opts: {
removeOnComplete: true,
removeOnFail: true,
...opts,
},
};
}
@bindThis
public createDeleteObjectStorageFileJob(key: string) {
return this.objectStorageQueue.add('deleteFile', {

View File

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { RemoteUser, User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
@ -20,6 +19,7 @@ import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
const FALLBACK = '❤';
@ -60,9 +60,6 @@ export class ReactionService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -74,6 +71,7 @@ export class ReactionService {
private utilityService: UtilityService,
private metaService: MetaService,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
@ -104,7 +102,6 @@ export class ReactionService {
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
reaction = '❤️';
} else {
// TODO: cache
reaction = await this.toDbReaction(reaction, user.host);
}
@ -158,20 +155,22 @@ export class ReactionService {
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction);
const emoji = await this.emojisRepository.findOne({
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
: await this.emojisRepository.findOne(
{
where: {
name: decodedReaction.name,
host: decodedReaction.host ?? IsNull(),
host: decodedReaction.host,
},
select: ['name', 'host', 'originalUrl', 'publicUrl'],
});
this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction,
emoji: emoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
emoji: customEmoji != null ? {
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
url: customEmoji.publicUrl || customEmoji.originalUrl,
} : null,
userId: user.id,
});
@ -310,8 +309,10 @@ export class ReactionService {
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) {
const name = custom[1];
const emoji = await this.emojisRepository.findOneBy({
host: reacterHost ?? IsNull(),
const emoji = reacterHost == null
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
: await this.emojisRepository.findOneBy({
host: reacterHost,
name,
});

View File

@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { MemoryCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable()
export class RelayService {
private relaysCache: MemoryCache<Relay[]>;
private relaysCache: MemorySingleCache<Relay[]>;
constructor(
@Inject(DI.usersRepository)
@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
}
@bindThis

View File

@ -4,7 +4,7 @@ import chalk from 'chalk';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { UsersRepository } from '@/models/index.js';
import type { RemoteUser, User } from '@/models/entities/User.js';
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { UtilityService } from '@/core/UtilityService.js';
@ -33,7 +33,7 @@ export class RemoteUserResolveService {
}
@bindThis
public async resolveUser(username: string, host: string | null): Promise<User> {
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
const usernameLower = username.toLowerCase();
if (host == null) {
@ -44,7 +44,7 @@ export class RemoteUserResolveService {
} else {
return u;
}
});
}) as LocalUser;
}
host = this.utilityService.toPuny(host);
@ -57,7 +57,7 @@ export class RemoteUserResolveService {
} else {
return u;
}
});
}) as LocalUser;
}
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
@ -109,7 +109,7 @@ export class RemoteUserResolveService {
if (u == null) {
throw new Error('user not found');
} else {
return u;
return u as LocalUser | RemoteUser;
}
});
}

View File

@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import * as Redis from 'ioredis';
import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, MemoryCache } from '@/misc/cache.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@ -13,6 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Packed } from '@/misc/json-schema';
import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = {
@ -57,15 +58,18 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown {
private rolesCache: MemoryCache<Role[]>;
private rolesCache: MemorySingleCache<Role[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -84,10 +88,10 @@ export class RoleService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new MemoryCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
this.rolesCache = new MemorySingleCache<Role[]>(1000 * 60 * 60 * 1);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(1000 * 60 * 60 * 1);
this.redisSubscriber.on('message', this.onMessage);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@ -398,8 +402,27 @@ export class RoleService implements OnApplicationShutdown {
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
}
@bindThis
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
const roles = await this.getUserRoles(note.userId);
const redisPipeline = this.redisClient.pipeline();
for (const role of roles) {
redisPipeline.xadd(
`roleTimeline:${role.id}`,
'MAXLEN', '~', '1000',
'*',
'note', note.id);
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
}
redisPipeline.exec();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@ -13,8 +13,9 @@ import { UsedUsername } from '@/models/entities/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import UsersChart from './chart/charts/users.js';
import { UtilityService } from './UtilityService.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class SignupService {
@ -34,6 +35,7 @@ export class SignupService {
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
private usersChart: UsersChart,
) {
}
@ -44,6 +46,7 @@ export class SignupService {
password?: string | null;
passwordHash?: UserProfile['password'] | null;
host?: string | null;
ignorePreservedUsernames?: boolean;
}) {
const { username, password, passwordHash, host } = opts;
let hash = passwordHash;
@ -77,6 +80,14 @@ export class SignupService {
throw new Error('USED_USERNAME');
}
if (!opts.ignorePreservedUsernames) {
const instance = await this.metaService.fetch(true);
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new Error('USED_USERNAME');
}
}
const keyPair = await new Promise<string[]>((res, rej) =>
generateKeyPair('rsa', {
modulusLength: 4096,

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