Compare commits

..

9 Commits

Author SHA1 Message Date
syuilo
1e67e9c661 Merge pull request #10342 from misskey-dev/develop
Release: 13.10.0
2023-03-22 09:55:38 +09:00
syuilo
ae517a99a7 Merge pull request #10218 from misskey-dev/develop
Release: 13.9.2
2023-03-06 11:54:12 +09:00
syuilo
b23a9b1a88 Merge pull request #10181 from misskey-dev/develop
Release: 13.9.1
2023-03-03 20:56:50 +09:00
syuilo
5bd68aa3e0 Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
2023-03-03 15:35:40 +09:00
syuilo
647ce174b3 Merge pull request #10112 from misskey-dev/develop
Release: 13.8.1
2023-02-26 20:57:13 +09:00
syuilo
02c8fd9de5 Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718)

* update PULL_REQUEST_TEMPLATE

* 起動時にRedisの疎通確認を行う (#9832)

* 起動時にRedisの疎通確認を行う

* check:connectをstart内に移動

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* Pass `--detectOpenHandles` to Jest (#9895)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* test(backend): restore ap-request tests (#9997)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* fix/refaftor(client): MkTime.vueの変更 (#10061)

* fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示

* fix(client): MkTimeを改良

* numberを許容

* falsyな値もとる

* 不明

* ありません

* fix

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911)

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする

* NO_SUCH_FILE

* Update codecov.yml

* Update apple-touch-icon.png

* デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062)

* デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする

* CIがない場合の処理追加

* enhance(client): improve clip menu ux

* 未知のユーザーが deleteActor されたら処理をスキップする (#10067)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正

* 順番関係ある?

* Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074)

* Update @swc/core to v1.3.36

* Update CHANGELOG.md

* Update CHANGELOG.md

* バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053)

* 🎨

* feat: 2つの検索画面の統合 (#9949) (#10038)

* feat: 検索画面の UI を統一

* fix: エラーの修正

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): ノートメニューからユーザーメニューを開けるように

Resolve #10019

* enhance(client): renoteした際の表示を改善

Resolve #10078

* Update CHANGELOG.md

* enhance(client): tweak contextmenu position calculation

* 🎨

* 🎨

* feat: in-channel featured note

Resolve #9938

* refactor(frontend): fix eslint error (#10084)

* Simplify search.vue (remove dead code) (#10088)

* Simplify search.vue

This is already handled by the code above it, no need to handle it twice

* Remove unused imports

* Update about-misskey.vue

* test(server): add validation test of api:notes/create (#10090)

* fix(server): notes/createのバリデーションが効いていない
Fix #10079

Co-Authored-By: mei23 <m@m544.net>

* anyOf内にバリデーションを書いても最初の一つしかチェックされない

* ✌️

* wip

* wip

* ✌️

* RequiredProp

* Revert "RequiredProp"

This reverts commit 7469390011.

* add api:notes/create

* fix lint

* text

* ✌️

* improve readability

---------

Co-authored-by: mei23 <m@m544.net>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* New Crowdin updates (#10059)

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

* New translations ja-JP.yml (Romanian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Thai)

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Ukrainian)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* enhance(client): improve user menu ux

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098)

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): メニューの「もっと」からインスタンス情報を見れるように

* [Fix] fixed an typo in error message (#10102)

* Update codecov.yml

* Update CHANGELOG.md

* fix(server): エラーのスタックトレースは返さないように

Fix #10064

* [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081)

* Added yaml file in addition to yml file, in editorconfig

* Applied editorconfig for pnpm-workspace.yaml

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* update deps

* ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106)

* refactor

* New translations ja-JP.yml (French) (#10103)

* Update CHANGELOG.md

* 13.8.0

---------

Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
2023-02-26 20:21:54 +09:00
syuilo
1ba49b614d Merge pull request #10058 from misskey-dev/develop
Release: 13.7.5
2023-02-24 13:06:55 +09:00
tamaina
40de14415c Release: 13.7.4
Merge pull request #10050 from misskey-dev/develop
2023-02-23 23:11:25 +09:00
tamaina
7c9330a02f Release: 13.7.3
Merge pull request #10048 from misskey-dev/develop
2023-02-23 22:15:56 +09:00
429 changed files with 6931 additions and 22865 deletions

View File

@@ -62,22 +62,6 @@ 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

@@ -62,22 +62,6 @@ 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 └─────────────────────────────

View File

@@ -62,22 +62,6 @@ 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

@@ -1,36 +0,0 @@
name: API report (misskey.js)
on: [push, pull_request]
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm --filter misskey-js build
- name: Check files
run: ls packages/misskey-js/built
- name: API report
run: pnpm --filter misskey-js api-prod
- name: Show report
if: always()
run: cat packages/misskey-js/temp/misskey-js.api.md

View File

@@ -36,7 +36,6 @@ jobs:
- backend
- frontend
- sw
- misskey-js
steps:
- uses: actions/checkout@v3.3.0
with:
@@ -62,7 +61,6 @@ jobs:
matrix:
workspace:
- backend
- misskey-js
steps:
- uses: actions/checkout@v3.3.0
with:

View File

@@ -1,58 +0,0 @@
name: Storybook
on:
push:
branches:
- master
- develop
pull_request:
branches-ignore:
- l10n_develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
with:
fetch-depth: 0
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
run_install: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js
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 }}
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: storybook
path: packages/frontend/storybook-static

View File

@@ -24,7 +24,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
image: redis:6
ports:
- 56312:6379

View File

@@ -63,7 +63,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
image: redis:6
ports:
- 56312:6379

View File

@@ -1,52 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Test (misskey.js)
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build
run: pnpm --filter misskey-js build
- name: Test
run: pnpm --filter misskey-js test
env:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

2
.gitignore vendored
View File

@@ -55,8 +55,6 @@ api-docs.json
.DS_Store
/files
ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
# blender backups
*.blend1

View File

@@ -5,80 +5,13 @@
-
### Client
-
-
### Server
-
-->
## 13.x.x (unreleased)
### NOTE
- Redis 7.xが必要です
### General
- チャンネルをお気に入りに登録できるように
- チャンネルにノートをピン留めできるように
### Client
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
- オブジェクトストレージの設定画面を分かりやすく
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
### Server
- イベント用Redisを別サーバーに分離できるように
- ジョブキュー用Redisを別サーバーに分離できるように
- サーバーの全体的なパフォーマンスを向上
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
- 通知に関する全体的なパフォーマンスを向上
- webhookがcontent-type text/plain;charset=UTF-8 で飛んでくる問題を修正
## 13.10.3
### Changes
- オブジェクトストレージのリージョン指定が必須になりました
- リージョンの指定の無いサービスは us-east-1 を設定してください
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
- e.g. ~/aws/config, AWS_REGION
### General
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
### Client
- クリップボタンをノートアクションに追加できるように
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
### Server
- リモートユーザーのチャート生成を無効にするオプションを追加
- リモートサーバーのチャート生成を無効にするオプションを追加
- ドライブのチャートはローカルユーザーのみ生成するように
- 空のアンテナが作成できるのを修正
## 13.10.2
### Server
- 絵文字を編集すると保存できないことがある問題を修正
### Client
- ドライブファイルのメニューが正常に動作しない問題を修正
## 13.10.1
### Client
- Misskey PlayのPlayボタンを押した時にエラーが発生する問題を修正
## 13.10.0
### General

View File

@@ -203,116 +203,6 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Storybook
Misskey uses [Storybook](https://storybook.js.org/) for UI development.
### Setup & Run
#### Universal
##### Setup
```bash
pnpm --filter misskey-js build
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
```
##### Run
```bash
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
```
#### macOS & Linux
##### Setup
```bash
pnpm --filter misskey-js build
```
##### Run
```bash
pnpm --filter frontend storybook-dev
```
### Usage
When you create a new component (in this example, `MyComponent.vue`), the story file (`MyComponent.stories.ts`) will be automatically generated by the `.storybook/generate.js` script.
You can override the default story by creating a impl story file (`MyComponent.stories.impl.ts`).
```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 = {
render(args) {
return {
components: {
MyComponent,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MyComponent v-bind="props" />',
};
},
args: {
foo: 'bar',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAvatar>;
```
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
```ts
import MyComponent from './MyComponent.vue';
void MyComponent;
```
You can override the component meta by creating a meta story file (`MyComponent.stories.meta.ts`).
```ts
export const argTypes = {
scale: {
control: {
type: 'range',
min: 1,
max: 4,
},
};
```
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
```ts
import { rest } from 'msw';
export const handlers = [
rest.post('/api/notes/timeline', (req, res, ctx) => {
return res(
ctx.json([]),
);
}),
];
```
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Notes
### How to resolve conflictions occurred at pnpm-lock.yaml?

View File

@@ -23,7 +23,6 @@ COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output

View File

@@ -54,17 +54,6 @@ 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" width="153" 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://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" width="117" 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

@@ -78,27 +78,10 @@ db:
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

@@ -545,6 +545,7 @@ tokenRequested: "منح حق الوصول إلى الحساب"
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
notificationType: "أنواع الإشعارات"
edit: "التعديل"
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
emailServer: "خادم البريد الإلكتروني"
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
email: "البريد الإلكتروني "
@@ -1274,7 +1275,3 @@ _deck:
channel: "القنوات"
mentions: "الإشارات"
direct: "مباشرة"
_webhookSettings:
name: "الإسم"
active: "مفعّل"

View File

@@ -562,6 +562,7 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
notificationType: "বিজ্ঞপ্তির ধরন"
edit: "সম্পাদনা"
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
emailServer: "ইমেইল সার্ভার"
enableEmail: "ইমেইল বিতরণ চালু করুন"
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
@@ -1353,7 +1354,3 @@ _deck:
channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি"
_webhookSettings:
name: "নাম"
active: "চালু"

View File

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

View File

@@ -776,7 +776,3 @@ _deck:
list: "Seznamy"
channel: "Kanály"
mentions: "Zmínění"
_webhookSettings:
name: "Jméno"
active: "Zapnuto"

View File

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

View File

@@ -460,7 +460,7 @@ aboutX: "Über {x}"
emojiStyle: "Emoji-Stil"
native: "Nativ"
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen"
noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren"
@@ -506,7 +506,6 @@ objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein
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"
@@ -595,6 +594,7 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
notificationType: "Art der Benachrichtigung"
edit: "Bearbeiten"
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
emailServer: "Email-Server"
enableEmail: "Email-Versand aktivieren"
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"
@@ -961,9 +961,7 @@ copyErrorInfo: "Fehlerdetails kopieren"
joinThisServer: "Bei dieser Instanz registrieren"
exploreOtherServers: "Eine andere Instanz finden"
letsLookAtTimeline: "Die Chronik durchstöbern"
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"
disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt."
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"
@@ -980,14 +978,6 @@ license: "Lizenz"
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
myClips: "Meine Clips"
drivecleaner: "Drive-Reiniger"
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
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"
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@@ -1284,8 +1274,6 @@ _role:
followersMoreThanOrEq: "Hat X oder mehr Follower"
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
and: "UND-Bedingung"
or: "ODER-Bedingung"
not: "NICHT-Bedingung"
@@ -1884,18 +1872,3 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name"
secret: "Secret"
events: "Webhook-Ereignisse"
active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"
followed: "Wenn dir jemand folgt"
note: "Wenn du eine Notiz schickst"
reply: "Wenn du eine Antwort erhältst"
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View File

@@ -392,6 +392,3 @@ _deck:
antenna: "Αντένες"
list: "Λίστα"
mentions: "Επισημάνσεις"
_webhookSettings:
name: "Όνομα"

View File

@@ -506,7 +506,6 @@ objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for AP
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"
@@ -595,6 +594,7 @@ tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
notificationType: "Notification type"
edit: "Edit"
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
emailServer: "Email server"
enableEmail: "Enable email distribution"
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
@@ -961,9 +961,7 @@ copyErrorInfo: "Copy error details"
joinThisServer: "Sign up at this instance"
exploreOtherServers: "Look for another instance"
letsLookAtTimeline: "Have a look at the timeline"
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"
disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting."
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"
@@ -980,14 +978,6 @@ license: "License"
unfavoriteConfirm: "Really remove from favorites?"
myClips: "My clips"
drivecleaner: "Drive Cleaner"
retryAllQueuesNow: "Retry running all queues"
retryAllQueuesConfirmTitle: "Really retry all?"
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"
_achievements:
earnedAt: "Unlocked at"
_types:
@@ -1284,8 +1274,6 @@ _role:
followersMoreThanOrEq: "Has X or more followers"
followingLessThanOrEq: "Follows X or fewer accounts"
followingMoreThanOrEq: "Follows X or more accounts"
notesLessThanOrEq: "Post count is less than/equal to"
notesMoreThanOrEq: "Post count is greater than/equal to"
and: "AND-Condition"
or: "OR-Condition"
not: "NOT-Condition"
@@ -1884,18 +1872,3 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "Descending Filesizes"
orderByCreatedAtAsc: "Ascending Dates"
_webhookSettings:
createWebhook: "Create Webhook"
name: "Name"
secret: "Secret"
events: "Webhook Events"
active: "Enabled"
_events:
follow: "When following a user"
followed: "When being followed"
note: "When posting a note"
reply: "When receiving a reply"
renote: "When renoted"
reaction: "When receiving a reaction"
mention: "When being mentioned"

View File

@@ -594,6 +594,7 @@ tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
notificationType: "Tipo de notificación"
edit: "Editar"
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
emailServer: "Servidor de correo"
enableEmail: "Activar el envío de correos electrónicos"
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
@@ -960,6 +961,7 @@ 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."
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"
@@ -971,14 +973,6 @@ rolesAssignedToMe: "Roles asignados a mí"
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles"
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
notesSearchNotAvailable: "No se puede buscar una nota"
license: "Licencia"
unfavoriteConfirm: "¿Desea quitar de favoritos?"
myClips: "Mis clips"
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 "
_achievements:
earnedAt: "Desbloqueado el"
_types:
@@ -1870,10 +1864,3 @@ _dialog:
_disabledTimeline:
title: "Línea de tiempo deshabilitada"
description: "No puedes usar esta línea de tiempo con tus roles actuales."
_drivecleaner:
orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos"
_webhookSettings:
name: "Nombre"
active: "Activado"

View File

@@ -575,6 +575,7 @@ tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications"
edit: "Editer"
useStarForReactionFallback: "Utiliser ★ comme alternative si lémoji de réaction est inconnu"
emailServer: "Serveur mail"
enableEmail: "Activer la distribution de courriel"
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."
@@ -1467,7 +1468,3 @@ _deck:
channel: "Canaux"
mentions: "Mentions"
direct: "Direct"
_webhookSettings:
name: "Nom"
active: "Activé"

View File

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

View File

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

View File

@@ -579,6 +579,7 @@ tokenRequested: "Berikan ijin akses ke akun"
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
notificationType: "Jenis pemberitahuan"
edit: "Sunting"
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
emailServer: "Peladen surel"
enableEmail: "Nyalakan distribusi surel"
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
@@ -1803,7 +1804,3 @@ _deck:
channel: "Kanal"
mentions: "Sebutan"
direct: "Langsung"
_webhookSettings:
name: "Nama"
active: "Aktif"

View File

@@ -170,7 +170,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
host: "Server remoto"
selectUser: "Seleziona profilo"
recipient: "Destinatario"
annotation: "Annotazione"
annotation: "Descrizione"
federation: "Federazione"
instances: "Istanza"
registeredAt: "Registrato presso"
@@ -212,7 +212,7 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a
done: "Fine"
processing: "In elaborazione"
preview: "Anteprima"
default: "Predefinito"
default: "Medio"
defaultValueIs: "Predefinito: {value}"
noCustomEmojis: "Nessun emoji"
noJobs: "Nessun lavoro"
@@ -237,14 +237,14 @@ more: "Di più!"
featured: "Tendenze"
usernameOrUserId: "Nome utente o ID utente"
noSuchUser: "Nessun utente trovato"
lookup: "Ricerca remota"
lookup: "Cerca"
announcements: "Annunci"
imageUrl: "URL dell'immagine"
remove: "Elimina"
removed: "Eliminato con successo"
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
deleteAreYouSure: "Eliminare \"{x}\"?"
resetAreYouSure: "Ripristinare?"
resetAreYouSure: "Reimposta"
saved: "Salvato"
messaging: "Messaggi"
upload: "Carica"
@@ -409,7 +409,7 @@ lastUsedAt: "Uso più recente: {t}"
unregister: "Annulla l'iscrizione"
passwordLessLogin: "Accedi senza password"
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
resetPassword: "Ripristina la password"
resetPassword: "Reimposta password"
newPasswordIs: "La tua nuova password è「{password}」"
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
share: "Condividi"
@@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità"
poll: "Sondaggio"
useCw: "Nascondere media"
enablePlayer: "Visualizza"
disablePlayer: "Chiudi"
enablePlayer: "Apri in lettore video"
disablePlayer: "Chiudi il lettore"
expandTweet: "Espandi tweet"
themeEditor: "Editor di temi"
description: "Descrizione"
@@ -594,9 +594,10 @@ tokenRequested: "Autorizza accesso al profilo"
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
notificationType: "Tipo di notifiche"
edit: "Modifica"
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
emailServer: "Server email"
enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per ripristinare la password"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
email: "Email"
emailAddress: "Indirizzo di posta elettronica"
smtpConfig: "Impostazioni del server SMTP"
@@ -960,30 +961,22 @@ copyErrorInfo: "Copia le informazioni sull'errore"
joinThisServer: "Registrati su questa istanza"
exploreOtherServers: "Trova altre istanze"
letsLookAtTimeline: "Sbircia la timeline"
invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione."
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."
emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica nel canale"
postToTheChannel: "Pubblica sul canale"
cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Accettazione reazioni"
likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti"
rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
resetPasswordConfirm: "Vuoi reimpostare 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."
license: "Licenza"
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
myClips: "Le mie Clip"
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"
_achievements:
earnedAt: "Data di conseguimento"
_types:
@@ -1280,8 +1273,6 @@ _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"
@@ -1320,8 +1311,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 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"
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."
_gallery:
my: "Le mie pubblicazioni"
liked: "Pubblicazioni che mi piacciono"
@@ -1876,22 +1867,4 @@ _dialog:
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
_disabledTimeline:
title: "Timeline disabilitata"
description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
_drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente"
_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"
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline"

View File

@@ -460,7 +460,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル"
native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
noHistory: "履歴はありません"
signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする"
@@ -500,13 +500,12 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
objectStorageUseProxy: "Proxyを利用する"
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@@ -595,6 +594,7 @@ tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
notificationType: "通知の種類"
edit: "編集"
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
emailServer: "メールサーバー"
enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
@@ -961,9 +961,7 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録する"
exploreOtherServers: "他のサーバーを探す"
letsLookAtTimeline: "タイムラインを見てみる"
disableFederationConfirm: "連合なしにしますか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
disableFederationOk: "連合なしにする"
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿"
@@ -983,11 +981,6 @@ drivecleaner: "ドライブクリーナー"
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
_achievements:
earnedAt: "獲得日時"
@@ -1286,8 +1279,6 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下"
notesMoreThanOrEq: "投稿数が~以上"
and: "~かつ~"
or: "~または~"
not: "~ではない"
@@ -1938,20 +1929,4 @@ _disabledTimeline:
_drivecleaner:
orderBySizeDesc: "サイズが大きい順"
orderByCreatedAtAsc: "追加日が古い順"
_webhookSettings:
createWebhook: "Webhookを作成"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
active: "有効"
_events:
follow: "フォローしたとき"
followed: "フォローされたとき"
note: "ノートを投稿したとき"
reply: "返信されたとき"
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
orderByCreatedAtAsc: "追加日が古い順"

View File

@@ -16,8 +16,8 @@ cancel: "やめとく"
noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenoteしたで"
noNotes: "ノートあらへん"
noNotifications: "通知あらへん"
noNotes: "ノートなんてあらへん"
noNotifications: "通知なんてあらへん"
instance: "サーバー"
settings: "設定"
basicSettings: "基本設定"
@@ -25,13 +25,13 @@ otherSettings: "ほかの設定"
openInWindow: "ウィンドウで開くで"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はあらへん"
noAccountDescription: "自己紹介食ってもた"
login: "ログイン"
loggingIn: "ログインしよるで"
logout: "ログアウト"
signup: "新規登録"
uploading: "アップロードしとるで"
save: "とっとく"
save: "保存"
users: "ユーザー"
addUser: "ユーザーを追加や"
favorite: "お気に入り"
@@ -81,9 +81,9 @@ followsYou: "フォローされとるで"
createList: "リスト作る"
manageLists: "リストの管理"
error: "エラー"
somethingHappened: "なんかあかんわ"
somethingHappened: "なんかアカンことが起こったで"
retry: "もっぺんやる?"
pageLoadError: "ページ読み込めんかったわ"
pageLoadError: "ページ読み込みに失敗してもうたわ"
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
@@ -108,8 +108,8 @@ inChannelQuote: "チャンネル内引用"
pinnedNote: "ピン留めされとるノート"
pinned: "ピン留めしとく"
you: "あんた"
clickToShow: "押したら出ら"
sensitive: "気いつけて見いや"
clickToShow: "押したら見えるで"
sensitive: "ちょっとアカンやつやで"
add: "増やす"
reaction: "リアクション"
reactions: "リアクション"
@@ -122,8 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
renoteMute: "Renoteは見いひん"
renoteUnmute: "Renoteもやっぱ見るわ"
renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
block: "ブロック"
unblock: "ブロックやめたる"
suspend: "凍結"
@@ -141,14 +141,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 +194,10 @@ network: "ネットワーク"
disk: "ディスク"
instanceInfo: "サーバー情報"
statistics: "統計"
clearQueue: "キューをほかす"
clearQueueConfirmTitle: "キューをほかしとこか?"
clearQueueConfirmText: "未配達の投稿は配送されなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュをほかす"
clearQueue: "キューにさいなら"
clearQueueConfirmTitle: "キューをクリアしまっか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュにさいなら"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
@@ -206,7 +206,7 @@ mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートをほかしてええか?"
noteDeleteConfirm: "このノートを削除しまっか?"
pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了したで管理者アカウントを作ってや。"
done: "でけた"
@@ -226,9 +226,9 @@ notResponding: "応答してへんで"
instanceFollowing: "サーバーのフォロー"
instanceFollowers: "サーバーのフォロワー\n"
instanceUsers: "サーバーのユーザー"
changePassword: "パスワードをいじる"
changePassword: "パスワード変える"
security: "セキュリティ"
retypedNotMatch: "入れたやつ合うてへんわ。"
retypedNotMatch: "入れたやつ同じになってないで。"
currentPassword: "今のパスワード"
newPassword: "次のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
@@ -258,7 +258,7 @@ uploadFromUrlRequested: "アップロードしたい言うといたで"
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
explore: "みつける"
messageRead: "もう読んだ"
noMoreHistory: "これより昔のんはあらへんで"
noMoreHistory: "これより過去の履歴はあらへんで"
startMessaging: "チャットやるで"
nUsersRead: "{n}人が読んでもうた"
agreeTo: "{0}に同意したで"
@@ -294,14 +294,14 @@ createFolder: "フォルダー作る"
renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーをほかす"
addFile: "ファイルを追加"
emptyDrive: "ドライブは空っぽや"
emptyDrive: "ドライブにはなんも残っとらん"
emptyFolder: "このフォルダーは空や"
unableToDelete: "消んかったわ"
unableToDelete: "消そうおもってんけどな、あかんかったわ"
inputNewFileName: "今度のファイル名は何にするん?"
inputNewDescription: "新しいキャプションを入れてや"
inputNewFolderName: "今度のフォルダ名は何にするん?"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
hasChildFilesOrFolders: "このフォルダは空っぽちゃうから消されへん"
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
copyUrl: "URLをコピー"
rename: "名前を変えるで"
avatar: "アイコン"
@@ -506,7 +506,6 @@ objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにする
objectStorageUseProxy: "Proxyを使う"
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん"
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
s3ForcePathStyleDesc: "s3ForcePathStyleを使たらバケット名をURLのホスト名やなくてパスの一部として必ず指定させるようになるで。セルフホストされたMinioとかを使うてるんやったら有効にせなあかん場合があるで。"
serverLogs: "サーバーログ"
deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
@@ -595,6 +594,7 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類"
edit: "編集"
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
emailServer: "メールサーバー"
enableEmail: "メール配信を受け取る"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
@@ -644,7 +644,7 @@ reporter: "通報者"
reporteeOrigin: "通報先"
reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
send: "送信"
abuseMarkAsResolved: "対応したで"
openInNewTab: "新しいタブで開く"
@@ -740,7 +740,7 @@ capacity: "容量"
inUse: "使用中"
editCode: "コードを編集"
apply: "適用"
receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
emailNotification: "メール通知"
publish: "公開"
inChannelSearch: "チャンネル内検索"
@@ -768,7 +768,7 @@ active: "アクティブ"
offline: "オフライン"
notRecommended: "あんま推奨しやんで"
botProtection: "Botプロテクション"
instanceBlocking: "サーバーブロック"
instanceBlocking: "インスタンスブロック"
selectAccount: "アカウントを選んでなー"
switchAccount: "アカウントを変えるで"
enabled: "有効"
@@ -852,8 +852,8 @@ themeColor: "テーマカラー"
size: "大きさ"
numberOfColumn: "列の数"
searchByGoogle: "探す"
instanceDefaultLightTheme: "サーバーおすすめの明るいテーマ"
instanceDefaultDarkTheme: "サーバーおすすめのの暗いテーマ"
instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ"
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
mutePeriod: "ミュートする期間"
period: "期限"
@@ -867,7 +867,7 @@ reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
rateLimitExceeded: "レート制限が超えたみたいやで"
cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしてええか?"
cropImageAsk: "画像をクロップしたってええか?"
cropYes: "切り抜いたる"
cropNo: "切り抜かへん"
file: "ファイル"
@@ -902,11 +902,11 @@ sensitiveMediaDetection: "センシティブなメディアの検出"
localOnly: "ローカルのみ"
remoteOnly: "リモートのみ"
failedToUpload: "アップロードに失敗してもうたわ…"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへん。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへん。"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたアップロードできへん。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いアップロードできへん。"
beta: "ベータ"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、インスタンスによっては自動で設定されることがあるで。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
navbar: "ナビゲーションバー"
shuffle: "シャッフルするで"
@@ -916,7 +916,7 @@ pushNotification: "プッシュ通知"
subscribePushNotification: "プッシュ通知をオンにするで"
unsubscribePushNotification: "プッシュ通知を止めるで"
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。"
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
windowMaximize: "最大化"
@@ -932,7 +932,7 @@ numberOfLikes: "いいね数"
show: "表示"
neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Misskey気に入ってくれた"
didYouLikeMisskey: "Misskey気に入っとっただけましたん"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
roles: "ロール"
role: "ロール"
@@ -942,7 +942,7 @@ assign: "アサイン"
unassign: "アサインを解除"
color: "色"
manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作れなさそうや"
youCannotCreateAnymore: "これ以上作れなさそうや"
cannotPerformTemporary: "一時的に利用できへんで"
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
preset: "プリセット"
@@ -961,9 +961,7 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録するわ"
exploreOtherServers: "他のサーバー見てみる"
letsLookAtTimeline: "タイムライン見てみーや"
disableFederationConfirm: "連合なしにしとくか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。"
disableFederationOk: "連合なしにしとく"
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。"
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿"
@@ -979,15 +977,6 @@ notesSearchNotAvailable: "ノート検索は使われへんで。"
license: "ライセンス"
unfavoriteConfirm: "ほんまに気に入らんの?"
myClips: "自分のクリップ"
drivecleaner: "ドライブキレイキレイ"
retryAllQueuesNow: "キューを全部もっかいやり直す"
retryAllQueuesConfirmTitle: "もっかいやってみるか?"
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きする"
noteIdOrUrl: "ートIDかURL"
_achievements:
earnedAt: "貰った日ぃ"
_types:
@@ -1048,7 +1037,7 @@ _achievements:
_login7:
title: "ビギナーⅡ"
description: "通算7日ログインした"
flavor: "慣れてきたんちゃう?"
flavor: "慣れてきたんちゃう?"
_login15:
title: "ビギナーⅢ"
description: "通算15日ログインした"
@@ -1152,7 +1141,7 @@ _achievements:
_iLoveMisskey:
title: "Misskey好きやねん"
description: "\"I ❤ #Misskey\"を投稿した"
flavor: "Misskeyを使ってくれておおきに by 開発チーム"
flavor: "Misskeyを使ってくれてありがとう by 開発チーム"
_foundTreasure:
title: "なんでも鑑定団"
description: "隠されたお宝を発見した"
@@ -1178,7 +1167,7 @@ _achievements:
description: "ホームタイムラインの流速が20npmを超す"
_viewInstanceChart:
title: "アナリスト"
description: "サーバーのチャートを表示した"
description: "インスタンスのチャートを表示した"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "スクラッチパッドで hello worldを出力した"
@@ -1215,7 +1204,7 @@ _achievements:
_loggedInOnNewYearsDay:
title: "あけましておめでとうございます!"
description: "元旦にログインした"
flavor: "今年も弊サーバーをよろしゅう頼みます"
flavor: "今年も弊インスタンスをよろしくお願いします"
_cookieClicked:
title: "クッキー叩くやつ"
description: "クッキー叩いてもうた"
@@ -1230,8 +1219,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: "コンディショナル"
@@ -1260,7 +1249,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canInvite: "サーバー招待コードの発行"
canInvite: "インスタンス招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数"
@@ -1272,7 +1261,7 @@ _role:
userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか"
_condition:
@@ -1284,8 +1273,6 @@ _role:
followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上"
notesLessThanOrEq: "投稿数が~以下しかない"
notesMoreThanOrEq: "投稿を~以上しとる"
and: "~かつ~"
or: "~または~"
not: "~ではない"
@@ -1325,7 +1312,7 @@ _ad:
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
contactAdmin: "このサーバーはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
contactAdmin: "このインスタンスはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
_gallery:
my: "あんたの投稿"
liked: "いいねした投稿"
@@ -1410,10 +1397,10 @@ _wordMute:
hard: "ハード"
mutedNotes: "ミュートされたノート"
_instanceMute:
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するんやで"
title: "設定したサーバーのノートを隠すで。"
heading: "ミュートするサーバー"
title: "設定したインスタンスのノートを隠すで。"
heading: "ミュートするインスタンス"
_theme:
explore: "テーマを探す"
install: "テーマのインストール"
@@ -1635,7 +1622,7 @@ _widgets:
digitalClock: "デジタル時計"
unixClock: "UNIX時計"
federation: "連合"
instanceCloud: "サーバークラウド"
instanceCloud: "インスタンスクラウド"
postForm: "投稿フォーム"
slideshow: "スライドショー"
button: "ボタン"
@@ -1686,7 +1673,7 @@ _visibility:
specified: "ダイレクト"
specifiedDescription: "選んだユーザーのみに公開するで"
disableFederation: "連合なし"
disableFederationDescription: "他サーバーへは送らんとくわ"
disableFederationDescription: "他インスタンスへは送らんとくわ"
_postForm:
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
@@ -1881,21 +1868,3 @@ _dialog:
_disabledTimeline:
title: "使われへんタイムライン"
description: "あんたの今のロールやったら、このタイムラインは使われへんで。"
_drivecleaner:
orderBySizeDesc: "サイズのでかい順"
orderByCreatedAtAsc: "追加日の古い順"
_webhookSettings:
createWebhook: "Webhookをつくる"
name: "名前"
secret: "シークレット"
events: "Webhookを投げるタイミング"
active: "有効"
_events:
follow: "フォローしたとき~!"
followed: "フォローもらったとき~!"
note: "ノートを投稿したとき~!"
reply: "返信があるとき~!"
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

@@ -592,6 +592,7 @@ tokenRequested: "계정 접근 허용"
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
notificationType: "알림 유형"
edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailServer: "메일 서버"
enableEmail: "이메일 송신 기능 활성화"
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
@@ -958,6 +959,7 @@ copyErrorInfo: "오류 정보 복사"
joinThisServer: "이 서버에 가입"
exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
postToTheChannel: "채널에 게시하기"
@@ -1847,7 +1849,3 @@ _deck:
_dialog:
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
_webhookSettings:
name: "이름"
active: "활성화"

View File

@@ -368,4 +368,3 @@ _deck:
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ"

View File

@@ -483,6 +483,3 @@ _deck:
antenna: "Antennes"
list: "Lijsten"
mentions: "Vermeldingen"
_webhookSettings:
name: "Naam"

View File

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

View File

@@ -129,7 +129,6 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
selectList: "Wybierz listę"
selectChannel: "Wybierz kanał"
selectAntenna: "Wybierz Antennę"
selectWidget: "Wybierz widżet"
editWidgets: "Edytuj widżety"
@@ -150,7 +149,6 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
addAccount: "Dodaj konto"
reloadAccountsList: "Odśwież listę kont"
loginFailed: "Nie udało się zalogować"
showOnRemote: "Zobacz na zdalnej instancji"
general: "Ogólne"
@@ -161,7 +159,6 @@ searchWith: "Szukaj: {q}"
youHaveNoLists: "Nie masz żadnej listy"
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
proxyAccount: "Konto proxy"
proxyAccountDescription: "Opis konta pełnomocniczego"
host: "Host"
selectUser: "Wybierz użytkownika"
recipient: "Odbiorca"
@@ -256,7 +253,6 @@ noMoreHistory: "Nie ma dalszej historii"
startMessaging: "Rozpocznij czat"
nUsersRead: "przeczytano przez {n}"
agreeTo: "Wyrażam zgodę na {0}"
agreeBelow: "Zaakceptuj poniżej"
tos: "Regulamin"
start: "Rozpocznij"
home: "Strona główna"
@@ -389,19 +385,13 @@ about: "Informacje"
aboutMisskey: "O Misskey"
administrator: "Admin"
token: "Token"
2fa: "Klucz 2FA "
totp: "Klucz aplikacji uwierzytelniającej (totp)"
totpDescription: "Opis klucza czasowego"
moderator: "Moderator"
moderation: "Moderacja"
nUsersMentioned: "{n} wspomnianych użytkowników"
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
securityKey: "Klucz bezpieczeństwa"
lastUsed: "Ostatnio używane"
lastUsedAt: "Ostatnio używane w"
unregister: "Cofnij rejestrację"
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
resetPassword: "Zresetuj hasło"
newPasswordIs: "Nowe hasło to „{password}”"
reduceUiAnimation: "Ogranicz animacje w UI"
@@ -528,16 +518,11 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
deleteAllFiles: "Usuń wszystkie pliki"
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
removeAllFollowing: "Przestań obserwować"
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
userSuspended: "To konto zostało zawieszone."
userSilenced: "Ten użytkownik został wyciszony."
yourAccountSuspendedTitle: "To konto jest zawieszone"
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
tokenRevoked: "Token odrzucony"
tokenRevokedDescription: "Opis odrzuconego tokena"
accountDeleted: "Konto usunięte"
accountDeletedDescription: "Opis konta usuniętego"
menu: "Menu"
divider: "Rozdzielacz"
addItem: "Dodaj element"
@@ -563,9 +548,7 @@ author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie"
plugins: "Wtyczki"
preferencesBackups: "Kopia zapasowa ustawień"
deck: "Tablica"
undeck: "oddkouj"
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
width: "Szerokość"
@@ -581,6 +564,7 @@ tokenRequested: "Przydziel dostęp do konta"
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
notificationType: "Rodzaj powiadomień"
edit: "Edytuj"
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
emailServer: "Serwer poczty e-mail"
enableEmail: "Włącz dostarczanie wiadomości e-mail"
emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
@@ -832,8 +816,6 @@ tenMinutes: "10 minut"
oneHour: "1 godzina"
oneDay: "1 dzień"
oneWeek: "1 tydzień"
oneMonth: "jeden miesiąc"
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
file: "Pliki"
recommended: "Zalecane"
check: "Zweryfikuj"
@@ -1376,7 +1358,3 @@ _deck:
channel: "Kanały"
mentions: "Wspomnienia"
direct: "Bezpośredni"
_webhookSettings:
name: "Nazwa"
active: "Właczono"

View File

@@ -555,6 +555,3 @@ _deck:
list: "Listas"
mentions: "Menções"
direct: "Notas diretas"
_webhookSettings:
name: "Nome"

View File

@@ -561,6 +561,7 @@ tokenRequested: "Acordă acces la cont"
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
notificationType: "Tipul notificării"
edit: "Editează"
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
emailServer: "Server email"
enableEmail: "Activează distribuția de emailuri"
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
@@ -701,6 +702,3 @@ _deck:
list: "Liste"
channel: "Canale"
mentions: "Mențiuni"
_webhookSettings:
name: "Nume"

View File

@@ -585,6 +585,7 @@ tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления"
edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
emailServer: "Сервер электронной почты"
enableEmail: "Включить обмен электронной почтой"
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
@@ -950,6 +951,7 @@ copyErrorInfo: "Скопировать код ошибки"
joinThisServer: "Присоединяйтесь к этому серверу"
exploreOtherServers: "Искать другие сервера"
letsLookAtTimeline: "Давайте посмотрим на ленту"
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
_achievements:
earnedAt: "Разблокировано в"
_types:
@@ -1835,7 +1837,3 @@ _deck:
_dialog:
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
_webhookSettings:
name: "Название"
active: "Вкл."

View File

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

View File

@@ -586,6 +586,7 @@ tokenRequested: "Povoliť prístup k účtu"
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
notificationType: "Typ oznámenia"
edit: "Upraviť"
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
emailServer: "Email server"
enableEmail: "Zapnúť email"
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
@@ -1474,7 +1475,3 @@ _deck:
channel: "Kanály"
mentions: "Zmienky"
direct: "Priame poznámky"
_webhookSettings:
name: "Názov"
active: "Zapnuté"

View File

@@ -442,6 +442,3 @@ _deck:
antenna: "Antenner"
list: "Listor"
mentions: "Omnämningar"
_webhookSettings:
active: "Aktiverad"

View File

@@ -544,8 +544,6 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
tokenRevoked: "โทเค็นไม่ถูกต้อง"
accountDeleted: "ลบบัญชีแล้ว"
menu: "เมนู"
divider: "ตัวแบ่ง"
addItem: "เพิ่มรายการ"
@@ -589,6 +587,7 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
notificationType: "ประเภทการแจ้งเตือน"
edit: "แก้ไข"
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
emailServer: "อีเมล์เซิร์ฟเวอร์"
enableEmail: "เปิดใช้งานการกระจายอีเมล"
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
@@ -955,22 +954,11 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
likeOnly: "ที่ชอบเท่านั้น"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
license: "ใบอนุญาต"
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
myClips: "คลิปของฉัน"
drivecleaner: "ทำความสะอาดไดรฟ์"
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
_achievements:
earnedAt: "ได้รับเมื่อ"
_types:
@@ -1230,8 +1218,6 @@ _role:
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
displayOrder: "ตำแหน่ง"
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ"
@@ -1257,7 +1243,6 @@ _role:
rateLimitFactor: "ขีดจำกัดอัตรา"
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
canHideAds: "ซ่อนโฆษณา"
canSearchNotes: "การใช้การค้นหาโน้ต"
_condition:
isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล"
@@ -1859,13 +1844,3 @@ _deck:
_dialog:
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
_disabledTimeline:
title: "ปิดใช้งานไทม์ไลน์"
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
_drivecleaner:
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
_webhookSettings:
name: "ชื่อ"
active: "เปิดใช้งาน"

View File

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

View File

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

View File

@@ -576,6 +576,7 @@ tokenRequested: "Надати доступ до акаунту"
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
notificationType: "Тип сповіщення"
edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Email сервер"
enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
@@ -1638,7 +1639,3 @@ _deck:
channel: "Канали"
mentions: "Згадки"
direct: "Особисте"
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"

View File

@@ -585,6 +585,7 @@ tokenRequested: "Cấp quyền truy cập vào tài khoản"
pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
notificationType: "Loại thông báo"
edit: "Sửa"
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
emailServer: "Email máy chủ"
enableEmail: "Bật phân phối email"
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
@@ -1704,7 +1705,3 @@ _deck:
_dialog:
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"
_webhookSettings:
name: "Tên"
active: "Đã bật"

View File

@@ -16,7 +16,7 @@ cancel: "取消"
noThankYou: "不用,谢谢"
enterUsername: "输入用户名"
renotedBy: "由 {user} 转贴"
noNotes: "没有帖"
noNotes: "没有帖"
noNotifications: "无通知"
instance: "服务器"
settings: "设置"
@@ -25,7 +25,7 @@ otherSettings: "其他设置"
openInWindow: "在新窗口中打开"
profile: "个人资料"
timeline: "时间线"
noAccountDescription: "此用户尚无自我介绍"
noAccountDescription: "这个人很懒,没有写自我介绍"
login: "登录"
loggingIn: "正在登录..."
logout: "登出"
@@ -85,7 +85,7 @@ somethingHappened: "出现了一些问题!"
retry: "重试"
pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
serverIsDead: "没有服务器响应。 请稍后再试。"
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称"
privacy: "隐私"
@@ -95,7 +95,7 @@ follow: "关注"
followRequest: "关注申请"
followRequests: "关注申请"
unfollow: "取消关注"
followRequestPending: "关注请求批准中"
followRequestPending: "发送关注请求"
enterEmoji: "输入表情符号"
renote: "转发"
unrenote: "取消转发"
@@ -119,7 +119,7 @@ rememberNoteVisibility: "保存上次设置的可见性"
attachCancel: "删除附件"
markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名"
enterFileName: "输入文件名"
mute: "屏蔽"
unmute: "解除屏蔽"
renoteMute: "屏蔽转帖"
@@ -145,7 +145,7 @@ emojiName: "表情符号名称"
emojiUrl: "表情符号地址"
addEmoji: "添加表情符号"
settingGuide: "推荐配置"
cacheRemoteFiles: "缓存远程文件"
cacheRemoteFiles: "远程文件缓存"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此帐户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此帐户识别为机器人。"
@@ -153,7 +153,7 @@ flagAsCat: "将这个账户设定为一只猫"
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。"
flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
autoAcceptFollowed: "自动允许关注者的关注"
addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败"
@@ -203,7 +203,7 @@ blockedInstances: "被阻拦的服务器"
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户"
blockedUsers: "拉黑的用户"
blockedUsers: "拉黑的用户"
noUsers: "无用户"
editProfile: "编辑资料"
noteDeleteConfirm: "要删除该帖子吗?"
@@ -336,7 +336,7 @@ enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
registration: "注册"
enableRegistration: "允许任何人注册"
enableRegistration: "允许新用户注册"
invite: "邀请"
driveCapacityPerLocalAccount: "每个用户的网盘空间"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
@@ -354,7 +354,7 @@ pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha"
hcaptchaSiteKey: "网站密钥"
hcaptchaSecretKey: "密钥"
hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥"
@@ -506,7 +506,6 @@ objectStorageUseSSLDesc: "如果不使用https进行API连接请关闭。"
objectStorageUseProxy: "使用代理"
objectStorageUseProxyDesc: "如果您不使用代理进行API连接请将其关闭。"
objectStorageSetPublicRead: "上传时设置为public-read"
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
serverLogs: "服务器日志"
deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框"
@@ -595,6 +594,7 @@ tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型"
edit: "编辑"
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
emailServer: "邮件服务器"
enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置"
@@ -961,9 +961,7 @@ copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "时间线"
disableFederationConfirm: "确定要禁用联合?"
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
disableFederationOk: "联合禁用"
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。"
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道"
@@ -979,15 +977,6 @@ notesSearchNotAvailable: "帖子检索不可用"
license: "许可信息"
unfavoriteConfirm: "确定要取消收藏吗?"
myClips: "我的便签"
drivecleaner: "网盘整理"
retryAllQueuesNow: "立刻重试所有队列"
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
largeNoteReactions: "使用大图标来显示回应"
noteIdOrUrl: "帖子ID或URL"
_achievements:
earnedAt: "达成时间"
_types:
@@ -1260,7 +1249,7 @@ _role:
gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖"
canInvite: "发放服务器邀请码"
canInvite: "发放实例邀请码"
canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量"
pinMax: "帖子置顶数量限制"
@@ -1284,8 +1273,6 @@ _role:
followersMoreThanOrEq: "关注者不少于"
followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于"
notesLessThanOrEq: "帖子数在~以下"
notesMoreThanOrEq: "帖子数在~以上"
and: "符合以下全部条件"
or: "符合以下任一条件"
not: "不符合以下任何条件"
@@ -1634,7 +1621,7 @@ _widgets:
photos: "照片"
digitalClock: "数字时钟"
unixClock: "UNIX时钟"
federation: "联"
federation: "联邦宇宙"
instanceCloud: "服务器云"
postForm: "投稿窗口"
slideshow: "幻灯片展示"
@@ -1881,21 +1868,3 @@ _dialog:
_disabledTimeline:
title: "时间线已禁用"
description: "您不能在当前角色使用时间线。"
_drivecleaner:
orderBySizeDesc: "按大小降序排列"
orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings:
createWebhook: "创建 Webhook"
name: "名称"
secret: "密钥"
events: "何时运行Webhook"
active: "已启用"
_events:
follow: "关注时"
followed: "被关注时"
note: "发布贴文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View File

@@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消"
noThankYou: "現在不要"
enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了"
renotedBy: "{user} 轉了"
noNotes: "無貼文。"
noNotifications: "沒有通知"
instance: "實例"
@@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉發"
unrenote: "取消轉發"
renoted: "轉成功"
renoted: "轉成功"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉之前已經轉過的內容。"
cantReRenote: "無法轉之前已經轉過的內容。"
quote: "引用"
inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用"
@@ -531,8 +531,8 @@ installedDate: "安裝時間"
lastUsedDate: "最後上線日期"
state: "狀態"
sort: "排序"
ascendingOrder: "昇冪"
descendingOrder: "降冪"
ascendingOrder: "遞增"
descendingOrder: "遞減"
scratchpad: "暫存記憶體"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
output: "輸出"
@@ -594,6 +594,7 @@ tokenRequested: "允許存取帳戶"
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
notificationType: "通知形式"
edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailServer: "電郵伺服器"
enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置"
@@ -677,8 +678,8 @@ sentReactionsCount: "反應發送次數"
receivedReactionsCount: "收到反應次數"
pollVotesCount: "已統計的投票數"
pollVotedCount: "已投票數"
yes: ""
no: ""
yes: "確定"
no: "取消"
driveFilesCount: "雲端硬碟檔案數量"
driveUsage: "雲端硬碟使用量"
noCrawle: "拒絕搜尋引擎索引"
@@ -960,6 +961,7 @@ copyErrorInfo: "複製錯誤資訊"
joinThisServer: "在此伺服器上註冊"
exploreOtherServers: "探索其他伺服器"
letsLookAtTimeline: "看看時間軸"
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道"
@@ -971,19 +973,6 @@ rolesAssignedToMe: "指派給自己的角色"
resetPasswordConfirm: "重設密碼?"
sensitiveWords: "敏感詞"
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
notesSearchNotAvailable: "無法使用搜尋貼文功能。"
license: "授權"
unfavoriteConfirm: "要取消收錄我的最愛嗎?"
myClips: "我的摘錄"
drivecleaner: "雲端硬碟清掃器"
retryAllQueuesNow: "立刻重試所有佇列"
retryAllQueuesConfirmTitle: "要現在重試嗎?"
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
largeNoteReactions: "將貼文的反應放大顯示"
noteIdOrUrl: "貼文ID或URL"
_achievements:
earnedAt: "獲得日期"
_types:
@@ -1100,7 +1089,7 @@ _achievements:
title: "有備而來"
description: "設定了個人檔案"
_markedAsCat:
title: "吾輩乃貓是也"
title: "我是貓"
description: "已將帳戶設定為貓"
flavor: "還沒有名字。"
_following1:
@@ -1280,8 +1269,6 @@ _role:
followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上"
notesLessThanOrEq: "發布數在~以下"
notesMoreThanOrEq: "發布數在~以上"
and: "~和~"
or: "~或~"
not: "~否"
@@ -1511,7 +1498,7 @@ _time:
_tutorial:
title: "Misskey使用方法"
step1_1: "歡迎!"
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
@@ -1877,21 +1864,3 @@ _dialog:
_disabledTimeline:
title: "停用的時間軸"
description: "目前的角色無法使用這個時間軸。"
_drivecleaner:
orderBySizeDesc: "檔案由大到小"
orderByCreatedAtAsc: "依照加入的日期順序"
_webhookSettings:
createWebhook: "建立 Webhook"
name: "名稱"
secret: "秘密"
events: "什麼時候運行Webhook"
active: "已啟用"
_events:
follow: "當你追隨時"
followed: "當被追隨時"
note: "當發布貼文時"
reply: "當收到回覆時"
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.11.0-beta.5",
"version": "13.10.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@8.1.1",
"packageManager": "pnpm@7.29.3",
"workspaces": [
"packages/frontend",
"packages/backend",
@@ -50,16 +50,16 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "5.0.3"
"typescript": "4.9.5"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"cross-env": "7.0.3",
"cypress": "12.9.0",
"eslint": "8.37.0",
"cypress": "12.7.0",
"eslint": "8.35.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View File

@@ -1,15 +1,8 @@
import Redis from 'ioredis';
import { loadConfig } from './built/config.js';
import { createRedisConnection } from './built/redis.js';
const config = loadConfig();
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,
});
const redis = createRedisConnection(config);
redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => {

View File

@@ -1,11 +0,0 @@
export class enableChartsForRemoteUser1679639483253 {
name = 'enableChartsForRemoteUser1679639483253'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForRemoteUser" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForRemoteUser"`);
}
}

View File

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

View File

@@ -1,11 +0,0 @@
export class enableChartsForFederatedInstances1679652081809 {
name = 'enableChartsForFederatedInstances1679652081809'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForFederatedInstances" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForFederatedInstances"`);
}
}

View File

@@ -1,21 +0,0 @@
export class channelFavorite1680228513388 {
name = 'channelFavorite1680228513388'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
await queryRunner.query(`DROP TABLE "channel_favorite"`);
}
}

View File

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

View File

@@ -1,10 +0,0 @@
export class cleanup1680491187535 {
name = 'cleanup1680491187535'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "antenna_note" `);
}
async down(queryRunner) {
}
}

View File

@@ -1,11 +0,0 @@
export class cleanup1680582195041 {
name = 'cleanup1680582195041'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "notification" `);
}
async down(queryRunner) {
}
}

View File

@@ -1,17 +0,0 @@
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

@@ -22,46 +22,44 @@
"test-and-coverage": "pnpm jest-and-coverage"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.3.46",
"@swc/core-darwin-x64": "1.3.46",
"@swc/core-linux-arm-gnueabihf": "1.3.46",
"@swc/core-linux-arm64-gnu": "1.3.46",
"@swc/core-linux-arm64-musl": "1.3.46",
"@swc/core-linux-x64-gnu": "1.3.46",
"@swc/core-linux-x64-musl": "1.3.46",
"@swc/core-win32-arm64-msvc": "1.3.46",
"@swc/core-win32-ia32-msvc": "1.3.46",
"@swc/core-win32-x64-msvc": "1.3.46",
"@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.38",
"@swc/core-darwin-x64": "^1.3.38",
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
"@swc/core-linux-arm64-gnu": "^1.3.38",
"@swc/core-linux-arm64-musl": "^1.3.38",
"@swc/core-linux-x64-gnu": "^1.3.38",
"@swc/core-linux-x64-musl": "^1.3.38",
"@swc/core-win32-arm64-msvc": "^1.3.38",
"@swc/core-win32-ia32-msvc": "^1.3.38",
"@swc/core-win32-x64-msvc": "^1.3.38",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@aws-sdk/client-s3": "3.306.0",
"@aws-sdk/lib-storage": "3.306.0",
"@aws-sdk/node-http-handler": "3.306.0",
"@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.1.2",
"@discordapp/twemoji": "14.0.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.10.0",
"@fastify/cors": "8.2.0",
"@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.2",
"@fastify/static": "6.9.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.4.0",
"@nestjs/core": "9.4.0",
"@nestjs/testing": "9.4.0",
"@nestjs/common": "9.3.9",
"@nestjs/core": "9.3.9",
"@nestjs/testing": "9.3.9",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.46",
"@swc/core": "1.3.38",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"autwh": "0.1.0",
"aws-sdk": "2.1318.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"bull": "4.10.4",
@@ -76,7 +74,7 @@
"date-fns": "2.29.3",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"fastify": "4.15.0",
"fastify": "4.14.1",
"feed": "4.2.2",
"file-type": "18.2.1",
"fluent-ffmpeg": "2.1.2",
@@ -88,21 +86,21 @@
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "21.1.1",
"jsdom": "21.1.0",
"json5": "2.2.3",
"jsonld": "8.1.1",
"jsrsasign": "10.7.0",
"jsrsasign": "10.6.1",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-js": "0.0.15",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.1",
"node-fetch": "3.3.0",
"nodemailer": "6.9.1",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "9.1.1",
"otpauth": "^9.0.2",
"parse5": "7.1.2",
"pg": "8.10.0",
"private-ip": "3.0.0",
@@ -125,7 +123,7 @@
"sanitize-html": "2.10.0",
"seedrandom": "3.0.5",
"semver": "7.3.8",
"sharp": "0.32.0",
"sharp": "0.31.3",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
@@ -133,25 +131,25 @@
"systeminformation": "5.17.12",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.5",
"tsconfig-paths": "4.2.0",
"tsc-alias": "1.8.3",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.13",
"typescript": "5.0.3",
"typeorm": "0.3.11",
"typescript": "4.9.5",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.5.0",
"websocket": "1.0.34",
"ws": "8.13.0",
"ws": "8.12.1",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.5.0",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
"@types/archiver": "5.3.1",
"@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0",
"@types/cbor": "6.0.0",
@@ -160,13 +158,13 @@
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10",
"@types/jest": "29.5.0",
"@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1",
"@types/jsdom": "21.1.0",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.8",
"@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1",
"@types/node": "18.15.11",
"@types/node": "18.15.0",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@@ -178,7 +176,7 @@
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.9.0",
"@types/sanitize-html": "2.8.1",
"@types/semver": "7.3.13",
"@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2",
@@ -190,11 +188,10 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1",
"aws-sdk-client-mock": "^2.1.1",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"cross-env": "7.0.3",
"eslint": "8.37.0",
"eslint": "8.35.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",

View File

@@ -2,15 +2,18 @@ import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common';
import 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: loadConfig(),
useValue: config,
};
const $db: Provider = {
@@ -25,31 +28,18 @@ const $db: Provider = {
const $redis: Provider = {
provide: DI.redis,
useFactory: (config) => {
return 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,
});
const redisClient = createRedisConnection(config);
return redisClient;
},
inject: [DI.config],
};
const $redisForPubsub: Provider = {
provide: DI.redisForPubsub,
const $redisSubscriber: Provider = {
provide: DI.redisSubscriber,
useFactory: (config) => {
const redis = new 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;
const redisSubscriber = createRedisConnection(config);
redisSubscriber.subscribe(config.host);
return redisSubscriber;
},
inject: [DI.config],
};
@@ -57,14 +47,14 @@ const $redisForPubsub: Provider = {
@Global()
@Module({
imports: [RepositoryModule],
providers: [$config, $db, $redis, $redisForPubsub],
exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule],
providers: [$config, $db, $redis, $redisSubscriber],
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
@Inject(DI.db) private db: DataSource,
@Inject(DI.redis) private redisClient: Redis.Redis,
@Inject(DI.redisForPubsub) private redisForPubsub: Redis.Redis,
@Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis,
) {}
async onApplicationShutdown(signal: string): Promise<void> {
@@ -79,7 +69,7 @@ export class GlobalModule implements OnApplicationShutdown {
await Promise.all([
this.db.destroy(),
this.redisClient.disconnect(),
this.redisForPubsub.disconnect(),
this.redisSubscriber.disconnect(),
]);
}
}

View File

@@ -33,22 +33,6 @@ 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;
@@ -107,8 +91,6 @@ export type Mixin = {
mediaProxy: string;
externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null;
redisForPubsub: NonNullable<Source['redisForPubsub']>;
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
};
export type Config = Source & Mixin;
@@ -169,8 +151,6 @@ 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

@@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@@ -24,11 +24,8 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: Antenna[];
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForPubsub)
private redisForPubsub: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@@ -36,6 +33,9 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
this.antennasFetched = false;
this.antennas = [];
this.redisForPubsub.on('message', this.onRedisMessage);
this.redisSubscriber.on('message', this.onRedisMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisForPubsub.off('message', this.onRedisMessage);
this.redisSubscriber.off('message', this.onRedisMessage);
}
@bindThis
@@ -92,13 +92,54 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
this.redisClient.xadd(
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
// 通知しない設定になっているか、自分自身の投稿なら既読にする
const read = !antenna.notify || (antenna.userId === noteUser.id);
this.antennaNotesRepository.insert({
id: this.idService.genId(),
antennaId: antenna.id,
noteId: note.id,
read: read,
});
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
where: {
muterId: antenna.userId,
},
select: ['muteeId'],
});
// Copy
const _note: Note = {
...note,
};
if (note.replyId != null) {
_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
}
if (note.renoteId != null) {
_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
}
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}
// 2秒経っても既読にならなかったら通知
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
});
}
}, 2000);
}
}
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View File

@@ -1,172 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import 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';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class CacheService implements OnApplicationShutdown {
public userByIdCache: MemoryKVCache<User>;
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>;
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.redisForPubsub)
private redisForPubsub: 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);
this.userByIdCache = new MemoryKVCache<User>(Infinity);
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', {
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.redisForPubsub.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
break;
}
case 'follow': {
const follower = this.userByIdCache.get(body.followerId);
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
break;
}
default:
break;
}
}
}
@bindThis
public findUserById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisForPubsub.off('message', this.onMessage);
}
}

View File

@@ -38,9 +38,9 @@ import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js';
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
import { UserBlockingService } from './UserBlockingService.js';
import { CacheService } from './CacheService.js';
import { UserCacheService } from './UserCacheService.js';
import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairService } from './UserKeypairService.js';
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
@@ -159,9 +159,9 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
@@ -282,9 +282,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService,
TwoFactorAuthenticationService,
UserBlockingService,
CacheService,
UserCacheService,
UserFollowingService,
UserKeypairService,
UserKeypairStoreService,
UserListService,
UserMutingService,
UserSuspendService,
@@ -399,9 +399,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService,
$TwoFactorAuthenticationService,
$UserBlockingService,
$CacheService,
$UserCacheService,
$UserFollowingService,
$UserKeypairService,
$UserKeypairStoreService,
$UserListService,
$UserMutingService,
$UserSuspendService,
@@ -517,9 +517,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService,
TwoFactorAuthenticationService,
UserBlockingService,
CacheService,
UserCacheService,
UserFollowingService,
UserKeypairService,
UserKeypairStoreService,
UserListService,
UserMutingService,
UserSuspendService,
@@ -633,9 +633,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService,
$TwoFactorAuthenticationService,
$UserBlockingService,
$CacheService,
$UserCacheService,
$UserFollowingService,
$UserKeypairService,
$UserKeypairStoreService,
$UserListService,
$UserMutingService,
$UserSuspendService,

View File

@@ -1,28 +1,24 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import 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 } from '@/models/index.js';
import type { EmojisRepository, Note } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { Cache } 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';
@Injectable()
export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
private cache: Cache<Emoji | null>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config)
private config: Config,
@@ -36,16 +32,9 @@ 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(value.values()),
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
});
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
}
@bindThis
@@ -71,7 +60,7 @@ export class CustomEmojiService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
this.localEmojisCache.refresh();
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id),
@@ -81,146 +70,6 @@ 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 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 {
// クエリに使うホスト
@@ -235,7 +84,7 @@ export class CustomEmojiService {
}
@bindThis
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
@@ -294,6 +143,30 @@ 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,5 +36,8 @@ export class DeleteAccountService {
await this.usersRepository.update(user.id, {
isDeleted: true,
});
// Terminate streaming
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
}

View File

@@ -4,7 +4,6 @@ import { v4 as uuid } from 'uuid';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -37,6 +36,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import type S3 from 'aws-sdk/clients/s3.js';
type AddFileArgs = {
/** User who wish to add file */
@@ -81,7 +81,6 @@ type UploadFromUrlArgs = {
export class DriveService {
private registerLogger: Logger;
private downloaderLogger: Logger;
private deleteLogger: Logger;
constructor(
@Inject(DI.config)
@@ -119,7 +118,6 @@ export class DriveService {
const logger = new Logger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow');
this.downloaderLogger = logger.createSubLogger('downloader');
this.deleteLogger = logger.createSubLogger('delete');
}
/***
@@ -370,7 +368,7 @@ export class DriveService {
Body: stream,
ContentType: type,
CacheControl: 'max-age=31536000, immutable',
} as PutObjectCommandInput;
} as S3.PutObjectRequest;
if (filename) params.ContentDisposition = contentDisposition(
'inline',
@@ -380,16 +378,21 @@ export class DriveService {
);
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
await this.s3Service.upload(meta, params)
const s3 = this.s3Service.getS3(meta);
const upload = s3.upload(params, {
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
});
await upload.promise()
.then(
result => {
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
if (result) {
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
} else { // AbortMultipartUploadCommandOutput
this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
} else {
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`);
}
})
.catch(
},
err => {
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
},
@@ -525,10 +528,10 @@ export class DriveService {
};
const properties: {
width?: number;
height?: number;
orientation?: number;
} = {};
width?: number;
height?: number;
orientation?: number;
} = {};
if (info.width) {
properties['width'] = info.width;
@@ -613,20 +616,17 @@ export class DriveService {
if (user) {
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
// Publish driveFileCreated event
// Publish driveFileCreated event
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
});
}
// 統計を更新
this.driveChart.update(file, true);
if (file.userHost == null) {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, true);
} else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, true);
}
this.perUserDriveChart.update(file, true);
if (file.userHost !== null) {
this.instanceChart.updateDrive(file, true);
}
return file;
@@ -692,7 +692,7 @@ export class DriveService {
@bindThis
private async deletePostProcess(file: DriveFile, isExpired = false) {
// リモートファイル期限切れ削除後は直リンクにする
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, {
isLink: true,
@@ -709,36 +709,33 @@ export class DriveService {
this.driveFilesRepository.delete(file.id);
}
// 統計を更新
this.driveChart.update(file, false);
if (file.userHost == null) {
// ローカルユーザーのみ
this.perUserDriveChart.update(file, false);
} else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, false);
}
this.perUserDriveChart.update(file, false);
if (file.userHost !== null) {
this.instanceChart.updateDrive(file, false);
}
}
@bindThis
public async deleteObjectStorageFile(key: string) {
const meta = await this.metaService.fetch();
try {
const param = {
Bucket: meta.objectStorageBucket,
Key: key,
} as DeleteObjectCommandInput;
await this.s3Service.delete(meta, param);
const s3 = this.s3Service.getS3(meta);
try {
await s3.deleteObject({
Bucket: meta.objectStorageBucket!,
Key: key,
}).promise();
} catch (err: any) {
if (err.name === 'NoSuchKey') {
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
if (err.code === 'NoSuchKey') {
console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err);
return;
} else {
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err,
});
}
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err,
});
}
}

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { Cache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
@Injectable()
export class FederatedInstanceService {
private cache: MemoryKVCache<Instance>;
private cache: Cache<Instance>;
constructor(
@Inject(DI.instancesRepository)
@@ -18,7 +18,7 @@ export class FederatedInstanceService {
private utilityService: UtilityService,
private idService: IdService,
) {
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
this.cache = new Cache<Instance>(1000 * 60 * 60);
}
@bindThis

View File

@@ -14,6 +14,7 @@ import type {
MainStreamTypes,
NoteStreamTypes,
UserListStreamTypes,
UserStreamTypes,
} from '@/server/api/stream/types.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
@@ -48,6 +49,11 @@ 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);

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
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 { genAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js';
@@ -32,17 +32,4 @@ export class IdService {
default: throw new Error('unrecognized id generation method');
}
}
@bindThis
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
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 { MemorySingleCache } from '@/misc/cache.js';
import { Cache } 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: MemorySingleCache<LocalUser>;
private cache: Cache<LocalUser>;
constructor(
@Inject(DI.usersRepository)
@@ -19,12 +19,12 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new MemorySingleCache<LocalUser>(Infinity);
this.cache = new Cache<LocalUser>(Infinity);
}
@bindThis
public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get();
const cached = this.cache.get(null);
if (cached) return cached;
const user = await this.usersRepository.findOneBy({
@@ -33,11 +33,11 @@ export class InstanceActorService {
}) as LocalUser | undefined;
if (user) {
this.cache.set(user);
this.cache.set(null, user);
return user;
} else {
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
this.cache.set(created);
this.cache.set(null, created);
return created;
}
}

View File

@@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer;
constructor(
@Inject(DI.redisForPubsub)
private redisForPubsub: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.db)
private db: DataSource,
@@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
}, 1000 * 60 * 5);
}
this.redisForPubsub.on('message', this.onMessage);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
@@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
clearInterval(this.intervalId);
this.redisForPubsub.off('message', this.onMessage);
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -1,7 +1,6 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
import 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 +19,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 { MemorySingleCache } from '@/misc/cache.js';
import { Cache } 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 +46,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 MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -151,9 +150,6 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -325,14 +321,6 @@ export class NoteCreateService implements OnApplicationShutdown {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
if (data.channel) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
}
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ },
@@ -447,20 +435,15 @@ export class NoteCreateService implements OnApplicationShutdown {
createdAt: User['createdAt'];
isBot: User['isBot'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
const meta = await this.metaService.fetch();
// 統計を更新
this.notesChart.update(note, true);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, true);
}
this.perUserNotesChart.update(user, note, true);
// Register host
if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => {
this.federatedInstanceService.fetch(user.host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true);
}
this.instanceChart.updateNote(i.host, note, true);
});
}
@@ -473,7 +456,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.incNotesCountOfUser(user);
// Word mute
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({
where: {
enableWordMute: true,
},
@@ -502,6 +485,18 @@ export class NoteCreateService implements OnApplicationShutdown {
});
}
// 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,
});
}
});
}
if (data.reply) {
this.saveReply(data.reply, note);
}

View File

@@ -16,7 +16,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class NoteDeleteService {
@@ -40,7 +39,6 @@ export class NoteDeleteService {
private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart,
@@ -97,19 +95,14 @@ export class NoteDeleteService {
}
//#endregion
const meta = await this.metaService.fetch();
// 統計を更新
this.notesChart.update(note, false);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, false);
}
this.perUserNotesChart.update(user, note, false);
if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => {
this.federatedInstanceService.fetch(user.host).then(i => {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false);
}
this.instanceChart.updateNote(i.host, note, false);
});
}
}

View File

@@ -1,20 +1,28 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { In, IsNull, Not } 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 { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.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,
@@ -24,8 +32,21 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
) {
}
@@ -36,6 +57,7 @@ export class NoteReadService implements OnApplicationShutdown {
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
@@ -55,6 +77,7 @@ export class NoteReadService implements OnApplicationShutdown {
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId,
};
@@ -72,6 +95,9 @@ 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 */ });
}
@@ -79,9 +105,23 @@ 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'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
@@ -89,13 +129,25 @@ 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 (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note);
}
}
}
}
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
});
// TODO: ↓まとめてクエリしたい
@@ -119,6 +171,49 @@ 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');
}
});
this.notificationService.readNotificationByQuery(userId, {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
read: true,
});
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});
}
}

View File

@@ -1,9 +1,8 @@
import { setTimeout } from 'node:timers/promises';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -12,22 +11,21 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class NotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@@ -36,36 +34,54 @@ export class NotificationService implements OnApplicationShutdown {
private idService: IdService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private cacheService: CacheService,
) {
}
@bindThis
public async readAllNotification(
public async readNotification(
userId: User['id'],
force = false,
notificationIds: Notification['id'][],
) {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`,
'+',
'-',
'COUNT', 1);
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
if (notificationIds.length === 0) return;
if (latestNotificationId == null) return;
// Update documents
const result = await this.notificationsRepository.update({
notifieeId: userId,
id: In(notificationIds),
isRead: false,
}, {
isRead: true,
});
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
if (result.affected === 0) return;
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
return this.postReadAllNotifications(userId);
}
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
else return this.postReadNotifications(userId, notificationIds);
}
@bindThis
public async readNotificationByQuery(
userId: User['id'],
query: Record<string, any>,
) {
const notificationIds = await this.notificationsRepository.findBy({
...query,
notifieeId: userId,
isRead: false,
}).then(notifications => notifications.map(notification => notification.id));
return this.readNotification(userId, notificationIds);
}
@bindThis
private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
}
@bindThis
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
}
@bindThis
@@ -74,43 +90,45 @@ export class NotificationService implements OnApplicationShutdown {
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
const isMuted = profile.mutingNotificationTypes.includes(type);
if (isMuted) return null;
if (data.notifierId) {
if (notifieeId === data.notifierId) {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
if (mutings.has(data.notifierId)) {
return null;
}
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
}
const notification = {
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification
const notification = await this.notificationsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
notifieeId: notifieeId,
type: type,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data,
} as Notification;
} as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
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, {});
const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) return; // 既に削除されているかもしれない
if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視
const mutings = await this.mutingsRepository.findBy({
muterId: notifieeId,
});
if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
return;
}
//#endregion
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);

View File

@@ -15,6 +15,10 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string };
note: Packed<'Note'>;
};
'readNotifications': { notificationIds: string[] };
'readAllNotifications': undefined;
'readAntenna': { antennaId: string };
'readAllAntennas': undefined;
};
// Reduce length because push message servers have character limits
@@ -68,6 +72,14 @@ export class PushNotificationService {
});
for (const subscription of subscriptions) {
// Continue if sendReadMessage is false
if ([
'readNotifications',
'readAllNotifications',
'readAntenna',
'readAllAntennas',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {

View File

@@ -8,13 +8,13 @@ import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, End
function q<T>(config: Config, name: string, limitPerSec = -1) {
return new Bull<T>(name, {
redis: {
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,
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,
},
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
limiter: limitPerSec > 0 ? {
max: limitPerSec,
duration: 1000,

View File

@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import type { EmojisRepository, BlockingsRepository, 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';
@@ -19,9 +20,6 @@ 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 = '❤';
const legacies: Record<string, string> = {
'like': '👍',
@@ -60,6 +58,9 @@ export class ReactionService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -71,7 +72,6 @@ export class ReactionService {
private utilityService: UtilityService,
private metaService: MetaService,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
@@ -102,6 +102,7 @@ export class ReactionService {
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
reaction = '❤️';
} else {
// TODO: cache
reaction = await this.toDbReaction(reaction, user.host);
}
@@ -146,31 +147,25 @@ export class ReactionService {
.where('id = :id', { id: note.id })
.execute();
const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note);
}
this.perUserReactionsChart.update(user, note);
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction);
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,
},
});
const emoji = await this.emojisRepository.findOne({
where: {
name: decodedReaction.name,
host: decodedReaction.host ?? IsNull(),
},
select: ['name', 'host', 'originalUrl', 'publicUrl'],
});
this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction,
emoji: customEmoji != null ? {
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
emoji: emoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: customEmoji.publicUrl || customEmoji.originalUrl,
url: emoji.publicUrl || emoji.originalUrl,
} : null,
userId: user.id,
});
@@ -256,6 +251,12 @@ export class ReactionService {
//#endregion
}
@bindThis
public async getFallbackReaction(): Promise<string> {
const meta = await this.metaService.fetch();
return meta.useStarForReactionFallback ? '⭐' : '👍';
}
@bindThis
public convertLegacyReactions(reactions: Record<string, number>) {
const _reactions = {} as Record<string, number>;
@@ -289,7 +290,7 @@ export class ReactionService {
@bindThis
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
if (reaction == null) return FALLBACK;
if (reaction == null) return await this.getFallbackReaction();
reacterHost = this.utilityService.toPunyNullable(reacterHost);
@@ -309,17 +310,15 @@ export class ReactionService {
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) {
const name = custom[1];
const emoji = reacterHost == null
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
: await this.emojisRepository.findOneBy({
host: reacterHost,
name,
});
const emoji = await this.emojisRepository.findOneBy({
host: reacterHost ?? IsNull(),
name,
});
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
}
return FALLBACK;
return await this.getFallbackReaction();
}
@bindThis

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 { MemorySingleCache } from '@/misc/cache.js';
import { Cache } 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: MemorySingleCache<Relay[]>;
private relaysCache: Cache<Relay[]>;
constructor(
@Inject(DI.usersRepository)
@@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
}
@bindThis
@@ -109,7 +109,7 @@ export class RelayService {
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return;
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({
status: 'accepted',
}));
if (relays.length === 0) return;

View File

@@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import { Cache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@@ -57,15 +57,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown {
private rolesCache: MemorySingleCache<Role[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
private rolesCache: Cache<Role[]>;
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
constructor(
@Inject(DI.redisForPubsub)
private redisForPubsub: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -77,17 +77,17 @@ export class RoleService implements OnApplicationShutdown {
private roleAssignmentsRepository: RoleAssignmentsRepository,
private metaService: MetaService,
private cacheService: CacheService,
private userCacheService: UserCacheService,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new MemorySingleCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
this.rolesCache = new Cache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
this.redisForPubsub.on('message', this.onMessage);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
@@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'roleCreated': {
const cached = this.rolesCache.get();
const cached = this.rolesCache.get(null);
if (cached) {
cached.push({
...body,
@@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
break;
}
case 'roleUpdated': {
const cached = this.rolesCache.get();
const cached = this.rolesCache.get(null);
if (cached) {
const i = cached.findIndex(x => x.id === body.id);
if (i > -1) {
@@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
break;
}
case 'roleDeleted': {
const cached = this.rolesCache.get();
const cached = this.rolesCache.get(null);
if (cached) {
this.rolesCache.set(cached.filter(x => x.id !== body.id));
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
}
break;
}
@@ -192,12 +192,6 @@ export class RoleService implements OnApplicationShutdown {
case 'followingMoreThanOrEq': {
return user.followingCount >= value.value;
}
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
default:
return false;
}
@@ -214,9 +208,9 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
return [...assignedRoles, ...matchedCondRoles];
}
@@ -231,11 +225,11 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else {
@@ -301,7 +295,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)),
@@ -321,7 +315,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public async getAdministratorIds(): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const administratorRoles = roles.filter(r => r.isAdministrator);
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(administratorRoles.map(r => r.id)),
@@ -400,6 +394,6 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisForPubsub.off('message', this.onMessage);
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -1,16 +1,11 @@
import { URL } from 'node:url';
import * as http from 'node:http';
import * as https from 'node:https';
import { Inject, Injectable } from '@nestjs/common';
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler';
import S3 from 'aws-sdk/clients/s3.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Meta } from '@/models/entities/Meta.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
@Injectable()
export class S3Service {
@@ -23,47 +18,25 @@ export class S3Service {
}
@bindThis
public getS3Client(meta: Meta): S3Client {
public getS3(meta: Meta) {
const u = meta.objectStorageEndpoint
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
const handlerOption: NodeHttpHandlerOptions = {};
if (meta.objectStorageUseSSL) {
handlerOption.httpsAgent = agent as https.Agent;
} else {
handlerOption.httpAgent = agent as http.Agent;
}
return new S3Client({
endpoint: meta.objectStorageEndpoint ? u : undefined,
credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? {
accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey,
} : undefined,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),
return new S3({
endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0
? meta.objectStorageEndpoint
: undefined,
accessKeyId: meta.objectStorageAccessKey!,
secretAccessKey: meta.objectStorageSecretKey!,
region: meta.objectStorageRegion ?? undefined,
sslEnabled: meta.objectStorageUseSSL,
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted
? false
: meta.objectStorageS3ForcePathStyle,
httpOptions: {
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
},
});
}
@bindThis
public async upload(meta: Meta, input: PutObjectCommandInput) {
const client = this.getS3Client(meta);
return new Upload({
client,
params: input,
partSize: (client.config.endpoint && (await client.config.endpoint()).hostname === 'storage.googleapis.com')
? 500 * 1024 * 1024
: 8 * 1024 * 1024,
}).done();
}
@bindThis
public delete(meta: Meta, input: DeleteObjectCommandInput) {
const client = this.getS3Client(meta);
return client.send(new DeleteObjectCommand(input));
}
}

View File

@@ -1,30 +1,40 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import Redis from 'ioredis';
import { IdService } from '@/core/IdService.js';
import type { User } from '@/models/entities/User.js';
import type { Blocking } from '@/models/entities/Blocking.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.js';
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { Cache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable()
export class UserBlockingService implements OnModuleInit {
export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger;
private userFollowingService: UserFollowingService;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -37,20 +47,47 @@ export class UserBlockingService implements OnModuleInit {
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
onModuleInit() {
this.userFollowingService = this.moduleRef.get('UserFollowingService');
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'blockingCreated': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
}
break;
}
case 'blockingDeleted': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
}
break;
}
default:
break;
}
}
}
@bindThis
@@ -58,8 +95,8 @@ export class UserBlockingService implements OnModuleInit {
await Promise.all([
this.cancelRequest(blocker, blockee),
this.cancelRequest(blockee, blocker),
this.userFollowingService.unfollow(blocker, blockee),
this.userFollowingService.unfollow(blockee, blocker),
this.unFollow(blocker, blockee),
this.unFollow(blockee, blocker),
this.removeFromList(blockee, blocker),
]);
@@ -74,9 +111,6 @@ export class UserBlockingService implements OnModuleInit {
await this.blockingsRepository.insert(blocking);
this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
@@ -114,6 +148,7 @@ export class UserBlockingService implements OnModuleInit {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
@@ -138,6 +173,54 @@ export class UserBlockingService implements OnModuleInit {
}
}
@bindThis
private async unFollow(follower: User, followee: User) {
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
});
if (following == null) {
return;
}
await Promise.all([
this.followingsRepository.delete(following.id),
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
this.perUserFollowingChart.update(follower, followee, false),
]);
// Publish unfollow event
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
});
}
// リモートにフォローをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
}
// リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
}
@bindThis
private async removeFromList(listOwner: User, user: User) {
const userLists = await this.userListsRepository.findBy({
@@ -171,9 +254,6 @@ export class UserBlockingService implements OnModuleInit {
await this.blockingsRepository.delete(blocking.id);
this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
@@ -188,6 +268,17 @@ export class UserBlockingService implements OnModuleInit {
@bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
where: {
blockerId,
},
select: ['blockeeId'],
}).then(records => records.map(record => record.blockeeId)));
return blockedUserIds.includes(blockeeId);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -0,0 +1,88 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { UsersRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserCacheService implements OnApplicationShutdown {
public userByIdCache: Cache<User>;
public localUserByNativeTokenCache: Cache<LocalUser | null>;
public localUserByIdCache: Cache<LocalUser>;
public uriPersonCache: Cache<User | null>;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new Cache<User>(Infinity);
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity);
this.localUserByIdCache = new Cache<LocalUser>(Infinity);
this.uriPersonCache = new Cache<User | null>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token, user);
this.localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
break;
}
case 'follow': {
const follower = this.userByIdCache.get(body.followerId);
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
break;
}
default:
break;
}
}
}
@bindThis
public findById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -1,5 +1,4 @@
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { Inject, Injectable } from '@nestjs/common';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js';
@@ -18,8 +17,6 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@@ -38,12 +35,8 @@ type Remote = RemoteUser | {
type Both = Local | Remote;
@Injectable()
export class UserFollowingService implements OnModuleInit {
private userBlockingService: UserBlockingService;
export class UserFollowingService {
constructor(
private moduleRef: ModuleRef,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -59,12 +52,11 @@ export class UserFollowingService implements OnModuleInit {
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
@@ -74,10 +66,6 @@ export class UserFollowingService implements OnModuleInit {
) {
}
onModuleInit() {
this.userBlockingService = this.moduleRef.get('UserBlockingService');
}
@bindThis
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
const [follower, followee] = await Promise.all([
@@ -182,8 +170,6 @@ export class UserFollowingService implements OnModuleInit {
}
});
this.cacheService.userFollowingsCache.refresh(follower.id);
const req = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
@@ -214,18 +200,14 @@ export class UserFollowingService implements OnModuleInit {
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.federatedInstanceService.fetch(follower.host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true);
}
this.instanceChart.updateFollowing(i.host, true);
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.federatedInstanceService.fetch(followee.host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true);
}
this.instanceChart.updateFollowers(i.host, true);
});
}
//#endregion
@@ -237,6 +219,7 @@ export class UserFollowingService implements OnModuleInit {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
@@ -290,8 +273,6 @@ export class UserFollowingService implements OnModuleInit {
await this.followingsRepository.delete(following.id);
this.cacheService.userFollowingsCache.refresh(follower.id);
this.decrementFollowing(follower, followee);
// Publish unfollow event
@@ -299,6 +280,7 @@ export class UserFollowingService implements OnModuleInit {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
@@ -338,18 +320,14 @@ export class UserFollowingService implements OnModuleInit {
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.federatedInstanceService.fetch(follower.host).then(i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false);
}
this.instanceChart.updateFollowing(i.host, false);
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.federatedInstanceService.fetch(followee.host).then(i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
this.instanceChart.updateFollowers(i.host, false);
});
}
//#endregion
@@ -591,6 +569,7 @@ export class UserFollowingService implements OnModuleInit {
detail: true,
});
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));

View File

@@ -1,34 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { RedisKVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairService {
private cache: RedisKVCache<UserKeypair>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new RedisKVCache<UserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity,
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
}
@bindThis
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId);
}
}

View File

@@ -0,0 +1,24 @@
import { Inject, Injectable } from '@nestjs/common';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairStoreService {
private cache: Cache<UserKeypair>;
constructor(
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new Cache<UserKeypair>(Infinity);
}
@bindThis
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId, () => this.userKeypairsRepository.findOneByOrFail({ userId: userId }));
}
}

View File

@@ -1,47 +1,34 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type { MutingsRepository, Muting } from '@/models/index.js';
import type { UsersRepository, MutingsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class UserMutingService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private idService: IdService,
private cacheService: CacheService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
) {
}
@bindThis
public async mute(user: User, target: User, expiresAt: Date | null = null): Promise<void> {
public async mute(user: User, target: User): Promise<void> {
await this.mutingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
expiresAt: expiresAt ?? null,
muterId: user.id,
muteeId: target.id,
});
this.cacheService.userMutingsCache.refresh(user.id);
}
@bindThis
public async unmute(mutings: Muting[]): Promise<void> {
if (mutings.length === 0) return;
await this.mutingsRepository.delete({
id: In(mutings.map(m => m.id)),
});
const muterIds = [...new Set(mutings.map(m => m.muterId))];
for (const muterId of muterIds) {
this.cacheService.userMutingsCache.refresh(muterId);
}
}
}

View File

@@ -37,7 +37,7 @@ export class VideoProcessingService {
});
});
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280);
} finally {
cleanup();
}

View File

@@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
private webhooks: Webhook[] = [];
constructor(
@Inject(DI.redisForPubsub)
private redisForPubsub: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
) {
//this.onMessage = this.onMessage.bind(this);
this.redisForPubsub.on('message', this.onMessage);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
@@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisForPubsub.off('message', this.onMessage);
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { Cache } from '@/misc/cache.js';
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { CacheService } from '@/core/CacheService.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
import { RemoteUser, User } from '@/models/entities/User.js';
@@ -31,8 +31,8 @@ export type UriParseResult = {
@Injectable()
export class ApDbResolverService {
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
private publicKeyCache: Cache<UserPublickey | null>;
private publicKeyByUserIdCache: Cache<UserPublickey | null>;
constructor(
@Inject(DI.config)
@@ -47,11 +47,11 @@ export class ApDbResolverService {
@Inject(DI.userPublickeysRepository)
private userPublickeysRepository: UserPublickeysRepository,
private cacheService: CacheService,
private userCacheService: UserCacheService,
private apPersonService: ApPersonService,
) {
this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
}
@bindThis
@@ -107,11 +107,11 @@ export class ApDbResolverService {
if (parsed.local) {
if (parsed.type !== 'users') return null;
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) ?? null;
} else {
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
uri: parsed.uri,
}));
}
@@ -138,7 +138,7 @@ export class ApDbResolverService {
if (key == null) return null;
return {
user: await this.cacheService.findUserById(key.userId) as RemoteUser,
user: await this.userCacheService.findById(key.userId) as RemoteUser,
key,
};
}

View File

@@ -14,15 +14,13 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { Poll } from '@/models/entities/Poll.js';
import type { PollVote } from '@/models/entities/PollVote.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
@@ -52,11 +50,10 @@ export class ApRendererService {
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService,
private userKeypairService: UserKeypairService,
private userKeypairStoreService: UserKeypairStoreService,
private apMfmService: ApMfmService,
private mfmService: MfmService,
) {
@@ -275,7 +272,11 @@ export class ApRendererService {
if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', '');
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
// TODO: cache
const emoji = await this.emojisRepository.findOneBy({
name,
host: IsNull(),
});
if (emoji) object.tag = [this.renderEmoji(emoji)];
}
@@ -472,7 +473,7 @@ export class ApRendererService {
...hashtagTags,
];
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const person = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
@@ -639,7 +640,7 @@ export class ApRendererService {
@bindThis
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const ldSignature = this.ldSignatureService.use();
ldSignature.debug = false;
@@ -700,9 +701,13 @@ export class ApRendererService {
private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
const emojis = await Promise.all(
names.map(name => this.emojisRepository.findOneBy({
name,
host: IsNull(),
})),
);
return emojis;
return emojis.filter(emoji => emoji != null) as Emoji[];
}
}

View File

@@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { User } from '@/models/entities/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
@@ -131,7 +131,7 @@ export class ApRequestService {
@Inject(DI.config)
private config: Config,
private userKeypairService: UserKeypairService,
private userKeypairStoreService: UserKeypairStoreService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
@@ -143,7 +143,7 @@ export class ApRequestService {
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
const body = JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedPost({
key: {
@@ -170,7 +170,7 @@ export class ApRequestService {
*/
@bindThis
public async signedGet(url: string, user: { id: User['id'] }) {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({
key: {

View File

@@ -1,6 +1,5 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -342,17 +341,15 @@ export class ApNoteService {
if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({
host,
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
});
return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replaceAll(':', '');
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name);
const exists = await this.emojisRepository.findOneBy({
host,
name,
});
if (exists) {
if ((tag.updated != null && exists.updatedAt == null)

View File

@@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
import { truncate } from '@/misc/truncate.js';
import type { CacheService } from '@/core/CacheService.js';
import type { UserCacheService } from '@/core/UserCacheService.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type Logger from '@/logger.js';
@@ -30,8 +30,6 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -50,13 +48,11 @@ const summaryLength = 2048;
export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService;
private userEntityService: UserEntityService;
private driveFileEntityService: DriveFileEntityService;
private idService: IdService;
private globalEventService: GlobalEventService;
private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService;
private cacheService: CacheService;
private userCacheService: UserCacheService;
private apResolverService: ApResolverService;
private apNoteService: ApNoteService;
private apImageService: ApImageService;
@@ -96,10 +92,9 @@ export class ApPersonService implements OnModuleInit {
//private userEntityService: UserEntityService,
//private idService: IdService,
//private globalEventService: GlobalEventService,
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private cacheService: CacheService,
//private userCacheService: UserCacheService,
//private apResolverService: ApResolverService,
//private apNoteService: ApNoteService,
//private apImageService: ApImageService,
@@ -115,13 +110,11 @@ export class ApPersonService implements OnModuleInit {
onModuleInit() {
this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.cacheService = this.moduleRef.get('CacheService');
this.userCacheService = this.moduleRef.get('UserCacheService');
this.apResolverService = this.moduleRef.get('ApResolverService');
this.apNoteService = this.moduleRef.get('ApNoteService');
this.apImageService = this.moduleRef.get('ApImageService');
@@ -210,14 +203,14 @@ export class ApPersonService implements OnModuleInit {
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = this.cacheService.uriPersonCache.get(uri);
const cached = this.userCacheService.uriPersonCache.get(uri);
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(this.config.url + '/')) {
const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id });
if (u) this.cacheService.uriPersonCache.set(uri, u);
if (u) this.userCacheService.uriPersonCache.set(uri, u);
return u;
}
@@ -225,7 +218,7 @@ export class ApPersonService implements OnModuleInit {
const exist = await this.usersRepository.findOneBy({ uri });
if (exist) {
this.cacheService.uriPersonCache.set(uri, exist);
this.userCacheService.uriPersonCache.set(uri, exist);
return exist;
}
//#endregion
@@ -334,12 +327,10 @@ export class ApPersonService implements OnModuleInit {
}
// Register host
this.federatedInstanceService.fetch(host).then(async i => {
this.federatedInstanceService.fetch(host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.instanceChart.newUser(i.host);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host);
}
});
this.usersChart.update(user!, true);
@@ -359,44 +350,32 @@ export class ApPersonService implements OnModuleInit {
const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user!.id, {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarBlurhash,
bannerBlurhash,
});
user!.avatarId = avatarId;
user!.bannerId = bannerId;
user!.avatarUrl = avatarUrl;
user!.bannerUrl = bannerUrl;
user!.avatarBlurhash = avatarBlurhash;
user!.bannerBlurhash = bannerBlurhash;
//#endregion
user!.avatarId = avatarId;
user!.bannerId = bannerId;
//#endregion
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [] as Emoji[];
});
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion
await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user!;
return user!;
}
/**
@@ -478,14 +457,10 @@ export class ApPersonService implements OnModuleInit {
if (avatar) {
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
}
if (banner) {
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
}
// Update user

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennasRepository } from '@/models/index.js';
import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Antenna } from '@/models/entities/Antenna.js';
import { bindThis } from '@/decorators.js';
@@ -10,6 +10,9 @@ export class AntennaEntityService {
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
) {
}
@@ -19,6 +22,8 @@ export class AntennaEntityService {
): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
return {
id: antenna.id,
createdAt: antenna.createdAt.toISOString(),
@@ -33,7 +38,7 @@ export class AntennaEntityService {
withReplies: antenna.withReplies,
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
hasUnreadNote,
};
}
}

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