Compare commits
169 Commits
13.11.2
...
13.12.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
14e364a74a | ||
![]() |
e1bc832c0d | ||
![]() |
2d84e04240 | ||
![]() |
be226ec187 | ||
![]() |
6db37d4fcb | ||
![]() |
b3b8ee46f0 | ||
![]() |
b45bc3fd5d | ||
![]() |
5c08f2b93b | ||
![]() |
5f62cefe31 | ||
![]() |
8dab46470e | ||
![]() |
8c70bbe74d | ||
![]() |
9ee002285d | ||
![]() |
febb9f388c | ||
![]() |
2cfed3395e | ||
![]() |
53498991bb | ||
![]() |
ae80dc9b1e | ||
![]() |
2c606028b3 | ||
![]() |
1f9f63df7c | ||
![]() |
dbc24ce587 | ||
![]() |
58c3fc6cd2 | ||
![]() |
363eb73fb3 | ||
![]() |
43593603f1 | ||
![]() |
bcd123371b | ||
![]() |
f3e43a0fc6 | ||
![]() |
b3ec47c3f4 | ||
![]() |
a2e475f2e8 | ||
![]() |
379c5a8091 | ||
![]() |
8dc5edde76 | ||
![]() |
e9ba896431 | ||
![]() |
cbd183a7a9 | ||
![]() |
d535ec21a2 | ||
![]() |
0cbdbf24f1 | ||
![]() |
4495969d7f | ||
![]() |
f5e9886c70 | ||
![]() |
1631e62739 | ||
![]() |
e48926b01d | ||
![]() |
25580e8afc | ||
![]() |
5b7482d8f4 | ||
![]() |
5f4d20ac1d | ||
![]() |
7de59a80a2 | ||
![]() |
d28866f71a | ||
![]() |
149ddebf16 | ||
![]() |
351bee325b | ||
![]() |
0ab50f87a2 | ||
![]() |
09764b909b | ||
![]() |
2d3d986d13 | ||
![]() |
87657d0acf | ||
![]() |
9d5911d4e4 | ||
![]() |
e2d9c0efe2 | ||
![]() |
8fb5457c01 | ||
![]() |
0ad7869249 | ||
![]() |
e8177ee311 | ||
![]() |
8fbca63cec | ||
![]() |
5124db57d2 | ||
![]() |
6e0f998fb2 | ||
![]() |
52a1d96218 | ||
![]() |
a986203b38 | ||
![]() |
2aa75f5489 | ||
![]() |
b9b9cd0c78 | ||
![]() |
b2a28ad9d4 | ||
![]() |
f3206d094d | ||
![]() |
59dc9516d0 | ||
![]() |
62af89d433 | ||
![]() |
b57ee4dd96 | ||
![]() |
8876ae09ed | ||
![]() |
34492f3c9a | ||
![]() |
918a96da24 | ||
![]() |
e461fb169e | ||
![]() |
5ddffa728a | ||
![]() |
eb0e2ceef7 | ||
![]() |
2718d86171 | ||
![]() |
d437e148db | ||
![]() |
eacdc0136f | ||
![]() |
5a7a1d0be9 | ||
![]() |
9145302b3a | ||
![]() |
ca49ac28b8 | ||
![]() |
7b721c2124 | ||
![]() |
87ff004c73 | ||
![]() |
18df1c7a52 | ||
![]() |
1dac961784 | ||
![]() |
8b833c88ad | ||
![]() |
4054f9cccf | ||
![]() |
5cae078e5e | ||
![]() |
8dc60cd327 | ||
![]() |
af738d9ca9 | ||
![]() |
40debf20d8 | ||
![]() |
3af99d075e | ||
![]() |
795cb1ecf4 | ||
![]() |
e89d0aa815 | ||
![]() |
206baa13e6 | ||
![]() |
7cc797062d | ||
![]() |
67d218fe2b | ||
![]() |
dc8a3f210b | ||
![]() |
e1f9ab77f8 | ||
![]() |
0f7defc14a | ||
![]() |
323af09ae9 | ||
![]() |
d868f8f356 | ||
![]() |
8c9cb9ee15 | ||
![]() |
3a61af326e | ||
![]() |
f5c502a436 | ||
![]() |
b8dacaaac8 | ||
![]() |
65ff2c2498 | ||
![]() |
b26807b59b | ||
![]() |
471b836a44 | ||
![]() |
aa289c9cb0 | ||
![]() |
614f12386e | ||
![]() |
cc27c1486d | ||
![]() |
d2d17847dc | ||
![]() |
fa60f54bc5 | ||
![]() |
0ddc79bb91 | ||
![]() |
9ad250bbb8 | ||
![]() |
d2aba9b693 | ||
![]() |
6c5b5f59dd | ||
![]() |
973e70bacc | ||
![]() |
15761a0fa8 | ||
![]() |
38fdc73d01 | ||
![]() |
bcbf06ac8c | ||
![]() |
98383b2aa9 | ||
![]() |
5a8748b2b0 | ||
![]() |
69adbdef15 | ||
![]() |
da0c295114 | ||
![]() |
83d0f819be | ||
![]() |
c47a0f78ff | ||
![]() |
7d3b7986e5 | ||
![]() |
9469b26eb2 | ||
![]() |
168fe0e376 | ||
![]() |
14f30afd3c | ||
![]() |
a67439981b | ||
![]() |
5f67ca434d | ||
![]() |
21dfce2cbb | ||
![]() |
47c7b4b9cc | ||
![]() |
55c10d0d88 | ||
![]() |
93dcd1c98e | ||
![]() |
2423fb8d38 | ||
![]() |
463446795d | ||
![]() |
dffefdad95 | ||
![]() |
e014c91899 | ||
![]() |
5cac1515fd | ||
![]() |
97abfd48ce | ||
![]() |
605f149235 | ||
![]() |
7d11cf8ec9 | ||
![]() |
9bb6c536c0 | ||
![]() |
2a7ba37996 | ||
![]() |
3f57119aea | ||
![]() |
ddb1ab7fae | ||
![]() |
8913e561db | ||
![]() |
dcbaca4260 | ||
![]() |
6839441ac6 | ||
![]() |
62b6c4d09b | ||
![]() |
1a7e5fb865 | ||
![]() |
78a2feb24c | ||
![]() |
04511ac141 | ||
![]() |
4c0ef07f6f | ||
![]() |
3ff5a5ae29 | ||
![]() |
6ea057f8f8 | ||
![]() |
b7d056fb22 | ||
![]() |
e3aeab8122 | ||
![]() |
72031e49fc | ||
![]() |
d06d1e8682 | ||
![]() |
5c3a4a8224 | ||
![]() |
49749b46c4 | ||
![]() |
5d56799070 | ||
![]() |
81d2c5a4a7 | ||
![]() |
0db88a5a3b | ||
![]() |
f9720e0e6e | ||
![]() |
77f91d67b4 | ||
![]() |
da83322200 | ||
![]() |
b463490d9f | ||
![]() |
5002effd65 |
@@ -95,15 +95,13 @@ redis:
|
||||
# #prefix: example-prefix
|
||||
# #db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
# ┌───────────────────────────┐
|
||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
#meilisearch:
|
||||
# host: meilisearch
|
||||
# port: 7700
|
||||
# apiKey: ''
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
@@ -95,15 +95,13 @@ redis:
|
||||
# #prefix: example-prefix
|
||||
# #db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
# ┌───────────────────────────┐
|
||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
#meilisearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
# port: 7700
|
||||
# apiKey: ''
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
@@ -133,16 +131,20 @@ id: 'aid'
|
||||
#clusterLimit: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
#deliverJobConcurrency: 128
|
||||
#inboxJobConcurrency: 16
|
||||
#relashionshipJobConcurrency: 16
|
||||
# What's relashionshipJob?:
|
||||
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
#deliverJobPerSec: 128
|
||||
#inboxJobPerSec: 16
|
||||
#relashionshipJobPerSec: 64
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
#deliverJobMaxAttempts: 12
|
||||
#inboxJobMaxAttempts: 8
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
@@ -4,7 +4,10 @@
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "18.16.0"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
|
||||
|
@@ -95,15 +95,13 @@ redis:
|
||||
# #prefix: example-prefix
|
||||
# #db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
# ┌───────────────────────────┐
|
||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
#meilisearch:
|
||||
# host: meilisearch
|
||||
# port: 7700
|
||||
# apiKey: ''
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
@@ -8,7 +8,6 @@ build/
|
||||
built/
|
||||
db/
|
||||
docker-compose.yml
|
||||
elasticsearch/
|
||||
node_modules/
|
||||
packages/*/node_modules
|
||||
redis/
|
||||
|
25
.github/labeler.yml
vendored
25
.github/labeler.yml
vendored
@@ -1,12 +1,21 @@
|
||||
'⚙️Server':
|
||||
'packages/backend':
|
||||
- packages/backend/**/*
|
||||
|
||||
'🖥️Client':
|
||||
- packages/frontend/**/*
|
||||
|
||||
'🧪Test':
|
||||
- cypress/**/*
|
||||
'packages/backend:test':
|
||||
- packages/backend/test/**/*
|
||||
|
||||
'‼️ wrong locales':
|
||||
- any: ['locales/*.yml', '!locales/ja-JP.yml']
|
||||
'packages/frontend':
|
||||
- packages/frontend/**/*
|
||||
|
||||
'packages/frontend:test':
|
||||
- cypress/**/*
|
||||
|
||||
'packages/sw':
|
||||
- packages/sw/**/*
|
||||
|
||||
'packages/misskey-js':
|
||||
- packages/misskey-js/**/*
|
||||
|
||||
'packages/misskey-js:test':
|
||||
- packages/misskey-js/test/**/*
|
||||
- packages/misskey-js/test-d/**/*
|
||||
|
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -19,5 +19,6 @@ https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
||||
## Checklist
|
||||
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] Test working in a local environment
|
||||
- [ ] (If needed) Add story of storybook
|
||||
- [ ] (If needed) Update CHANGELOG.md
|
||||
- [ ] (If possible) Add tests
|
||||
|
2
.github/workflows/api-misskey-js.yml
vendored
2
.github/workflows/api-misskey-js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -17,11 +17,11 @@ jobs:
|
||||
submodules: true
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
|
4
.github/workflows/storybook.yml
vendored
4
.github/workflows/storybook.yml
vendored
@@ -20,12 +20,12 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
install: false
|
||||
start: pnpm start:test
|
||||
wait-on: 'http://localhost:61812'
|
||||
headless: false
|
||||
headed: true
|
||||
browser: ${{ matrix.browser }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
|
42
.github/workflows/test-production.yml
vendored
Normal file
42
.github/workflows/test-production.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Test (production install and build)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
jobs:
|
||||
production:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Copy Configure
|
||||
run: cp .github/misskey/test.yml .config/default.yml
|
||||
- name: Build
|
||||
run: pnpm build
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,7 +44,7 @@ built
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
/elasticsearch
|
||||
/meili_data
|
||||
npm-debug.log
|
||||
*.pem
|
||||
run.bat
|
||||
|
@@ -1 +1 @@
|
||||
v18.13.0
|
||||
18.16.0
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -6,5 +6,6 @@
|
||||
"files.associations": {
|
||||
"*.test.ts": "typescript"
|
||||
},
|
||||
"jest.jestCommandLine": "pnpm run jest",
|
||||
"jest.autoRun": "off"
|
||||
}
|
74
CHANGELOG.md
74
CHANGELOG.md
@@ -11,9 +11,81 @@
|
||||
-
|
||||
|
||||
-->
|
||||
## 13.x.x (unreleased)
|
||||
|
||||
### NOTE
|
||||
- Node.js 18.6.0以上が必要になりました
|
||||
|
||||
### General
|
||||
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||
* 一度引っ越したアカウントは利用に制限がかかります
|
||||
- Meilisearchを全文検索に使用できるようになりました
|
||||
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
|
||||
- ユーザーへの自分用メモ機能
|
||||
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
|
||||
(自分自身に対してもメモを追加できます。)
|
||||
* ユーザーメニューから追加できます。
|
||||
(デスクトップ表示ではusernameの右側のボタンからも追加可能)
|
||||
- チャンネルに色を設定できるようになりました。各ノートに設定した色のインジケーターが表示されます。
|
||||
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
|
||||
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
|
||||
- ロールに強制的にNSFWを付与するポリシーを追加
|
||||
* アップロード済みのファイルはNSFWにならない為注意してください。
|
||||
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
||||
- 管理者が予約ユーザー名を設定できるようになりました。
|
||||
- Fix: フォローリクエストの通知が残る問題を修正
|
||||
|
||||
### Client
|
||||
- チャンネル内検索ができるように
|
||||
- チャンネル検索ですべてのチャンネルの取得/表示ができるように
|
||||
- 通知の表示をカスタマイズできるように
|
||||
- ドライブのファイル一覧から直接ノートを作成できるように
|
||||
- ノートメニューからRenoteしたユーザーの一覧を見れるように
|
||||
- コントロールパネルのカスタム絵文字ページおよびaboutのカスタム絵文字の検索インプットで、`:emojiname1::emojiname2:`のように検索して絵文字を検索できるように
|
||||
* 絵文字ピッカーから入力可能になります
|
||||
- データセーバーモードを追加
|
||||
* 画像が全て隠れた状態で表示されるようになります
|
||||
- 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように
|
||||
- 新しい実績を追加
|
||||
- Fix: AiScript APIのMk:dialogで何も返していなかったのをNULLを返すように修正
|
||||
- Fix: リアクションをホバーした時のユーザーリストで猫耳が切れてしまっていた問題を修正
|
||||
|
||||
### Server
|
||||
- channel/searchのqueryが空の場合に全てのチャンネルを返すように変更
|
||||
- 環境変数MISSKEY_CONFIG_YMLで設定ファイルをdefault.ymlから変更可能に
|
||||
- Fix: 他のサーバーの情報が取得できないことがある問題を修正
|
||||
- Fix: エクスポートデータの拡張子がunknownになる問題を修正
|
||||
- Fix: Content-Dispositionのパースでエラーが発生した場合にダウンロードが完了しない問題を修正
|
||||
- Fix: API: i/update avatarIdとbannerIdにnullを渡した時、画像がリセットされない問題を修正
|
||||
- Fix: 1:1ではない画像のリアクション通知バッジが左や上に寄ってしまっていたのを中央に来るように修正
|
||||
- Fix: .wav, .flacが再生できない問題を修正(新しくアップロードされたファイルのみ修正が適用されます)
|
||||
- Fix: メモリの使用量を`used - buffers - cached`ではなく`total - available`で求めるように(環境によって正常に計測できていなかったため)
|
||||
|
||||
## 13.11.3
|
||||
|
||||
### General
|
||||
- 指定したロールを持つユーザーのノートのみが流れるロールタイムラインを追加
|
||||
- Deckのカラムとしても追加可能
|
||||
- カスタム絵文字関連の改善
|
||||
* ノートなどに含まれるemojis(populateEmojiの結果)は(プロキシされたURLではなく)オリジナルのURLを指すように
|
||||
* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように
|
||||
- カスタム絵文字でリアクションできないことがある問題を修正
|
||||
|
||||
### Client
|
||||
- チャンネルのピン留めされたノートの順番が正しくない問題を修正
|
||||
|
||||
### Server
|
||||
- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io
|
||||
- Misskey Webでのサーバーサイドエラー画面を改善
|
||||
- Misskey Webでのサーバーサイドエラーのログが残るように
|
||||
- ノート作成時のアンテナ追加パフォーマンスを改善
|
||||
- アンテナとロールTLのuntil/sinceプロパティが動くように
|
||||
|
||||
## 13.11.2
|
||||
|
||||
### Note
|
||||
- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照)
|
||||
|
||||
### General
|
||||
- チャンネルの検索用ページの追加
|
||||
|
||||
@@ -84,6 +156,8 @@
|
||||
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
|
||||
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
|
||||
- Add Minimizing ("folding") of windows
|
||||
- 「データセーバー」モードを追加
|
||||
- 非NSFWメディアが隠れている際にも「閲覧注意」が出てしまう問題を修正
|
||||
|
||||
### Server
|
||||
- PostgreSQLのレプリケーション対応
|
||||
|
@@ -165,6 +165,11 @@ pnpm jest -- foo.ts
|
||||
### e2e tests
|
||||
TODO
|
||||
|
||||
## Environment Variable
|
||||
|
||||
- `MISSKEY_CONFIG_YML`: Specify the file path of config.yml instead of default.yml (e.g. `2nd.yml`).
|
||||
- `MISSKEY_WEBFINGER_USE_HTTP`: If it's set true, WebFinger requests will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION.
|
||||
|
||||
## Continuous integration
|
||||
Misskey uses GitHub Actions for executing automated tests.
|
||||
Configuration files are located in [`/.github/workflows`](/.github/workflows).
|
||||
@@ -245,7 +250,6 @@ You can override the default story by creating a impl story file (`MyComponent.s
|
||||
|
||||
```ts
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-duplicates */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MyComponent from './MyComponent.vue';
|
||||
export const Default = {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=18.13.0-bullseye
|
||||
ARG NODE_VERSION=18.16.0-bullseye
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
|
@@ -116,15 +116,13 @@ redis:
|
||||
# #prefix: example-prefix
|
||||
# #db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
# ┌───────────────────────────┐
|
||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
#meilisearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
# port: 7700
|
||||
# apiKey: ''
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
@@ -52,6 +52,11 @@ describe('After setup instance', () => {
|
||||
cy.intercept('POST', '/api/signup').as('signup');
|
||||
|
||||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
@@ -71,6 +76,11 @@ describe('After setup instance', () => {
|
||||
|
||||
// ユーザー名が重複している場合の挙動確認
|
||||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-password] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
|
||||
|
@@ -7,7 +7,7 @@ services:
|
||||
links:
|
||||
- db
|
||||
- redis
|
||||
# - es
|
||||
# - meilisearch
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -48,16 +48,18 @@ services:
|
||||
interval: 5s
|
||||
retries: 20
|
||||
|
||||
# es:
|
||||
# meilisearch:
|
||||
# restart: always
|
||||
# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
|
||||
# image: getmeili/meilisearch:v1.1.1
|
||||
# environment:
|
||||
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
# - "TAKE_FILE_OWNERSHIP=111"
|
||||
# - MEILI_NO_ANALYTICS=true
|
||||
# - MEILI_ENV=production
|
||||
# env_file:
|
||||
# - .config/meilisearch.env
|
||||
# networks:
|
||||
# - internal_network
|
||||
# volumes:
|
||||
# - ./elasticsearch:/usr/share/elasticsearch/data
|
||||
# - ./meili_data:/meili_data
|
||||
|
||||
networks:
|
||||
internal_network:
|
||||
|
@@ -45,7 +45,7 @@ gulp.task('build:backend:script', () => {
|
||||
});
|
||||
|
||||
gulp.task('build:backend:style', () => {
|
||||
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css'])
|
||||
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css'])
|
||||
.pipe(cssnano({
|
||||
zindex: false
|
||||
}))
|
||||
|
@@ -20,6 +20,7 @@ noNotes: "Keine Notizen gefunden"
|
||||
noNotifications: "Keine Benachrichtigungen gefunden"
|
||||
instance: "Instanz"
|
||||
settings: "Einstellungen"
|
||||
notificationSettings: "Benachrichtigungseinstellungen"
|
||||
basicSettings: "Allgemeine Einstellungen"
|
||||
otherSettings: "Weitere Einstellungen"
|
||||
openInWindow: "In einem Fenster öffnen"
|
||||
@@ -1407,6 +1408,8 @@ _channel:
|
||||
following: "Gefolgt"
|
||||
usersCount: "{n} Teilnehmer"
|
||||
notesCount: "{n} Notizen"
|
||||
nameAndDescription: "Name und Beschreibung"
|
||||
nameOnly: "Nur Name"
|
||||
_menuDisplay:
|
||||
sideFull: "Seitlich"
|
||||
sideIcon: "Seitlich (Icons)"
|
||||
@@ -1887,6 +1890,7 @@ _deck:
|
||||
channel: "Kanal"
|
||||
mentions: "Erwähnungen"
|
||||
direct: "Direktnachrichten"
|
||||
roleTimeline: "Rollenchronik"
|
||||
_dialog:
|
||||
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
|
||||
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"
|
||||
|
@@ -20,6 +20,7 @@ noNotes: "No notes"
|
||||
noNotifications: "No notifications"
|
||||
instance: "Instance"
|
||||
settings: "Settings"
|
||||
notificationSettings: "Notification Settings"
|
||||
basicSettings: "Basic Settings"
|
||||
otherSettings: "Other Settings"
|
||||
openInWindow: "Open in window"
|
||||
@@ -904,6 +905,7 @@ remoteOnly: "Remote only"
|
||||
failedToUpload: "Upload failed"
|
||||
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially NSFW."
|
||||
cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "This file could not be uploaded because it exceeds the maximum allowed size."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "Automatic NSFW-Marking"
|
||||
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
|
||||
@@ -1407,6 +1409,8 @@ _channel:
|
||||
following: "Followed"
|
||||
usersCount: "{n} Participants"
|
||||
notesCount: "{n} Notes"
|
||||
nameAndDescription: "Name and description"
|
||||
nameOnly: "Name only"
|
||||
_menuDisplay:
|
||||
sideFull: "Side"
|
||||
sideIcon: "Side (Icons)"
|
||||
@@ -1887,6 +1891,7 @@ _deck:
|
||||
channel: "Channel"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct notes"
|
||||
roleTimeline: "Role Timeline"
|
||||
_dialog:
|
||||
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
|
||||
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."
|
||||
|
@@ -20,6 +20,7 @@ noNotes: "Nessuna nota!"
|
||||
noNotifications: "Nessuna notifica"
|
||||
instance: "Istanza"
|
||||
settings: "Impostazioni"
|
||||
notificationSettings: "Preferenze di notifica"
|
||||
basicSettings: "Impostazioni generali"
|
||||
otherSettings: "Altre impostazioni"
|
||||
openInWindow: "Apri in una finestra"
|
||||
@@ -786,7 +787,7 @@ gallery: "Galleria"
|
||||
recentPosts: "Le più recenti"
|
||||
popularPosts: "Le più visualizzate"
|
||||
shareWithNote: "Condividere in nota"
|
||||
ads: "Pubblicità"
|
||||
ads: "Banner"
|
||||
expiration: "Scadenza"
|
||||
startingperiod: "Periodo di inizio"
|
||||
memo: "Promemoria"
|
||||
@@ -991,6 +992,7 @@ largeNoteReactions: "Ingrandisci le reazioni"
|
||||
noteIdOrUrl: "ID della Nota o URL"
|
||||
accountMigration: "Migrazione del profilo"
|
||||
accountMoved: "Questo profilo ha migrato altrove:"
|
||||
forceShowAds: "Mostra sempre i banner"
|
||||
_accountMigration:
|
||||
moveTo: "Migrare questo profilo verso un un altro"
|
||||
moveToLabel: "Profilo verso cui migrare"
|
||||
@@ -1406,6 +1408,8 @@ _channel:
|
||||
following: "Seguiti"
|
||||
usersCount: "{n} partecipanti"
|
||||
notesCount: "{n} note"
|
||||
nameAndDescription: "Nome e descrizione"
|
||||
nameOnly: "Solo il nome"
|
||||
_menuDisplay:
|
||||
sideFull: "Laterale"
|
||||
sideIcon: "Laterale (solo icone)"
|
||||
|
@@ -263,14 +263,16 @@ noMoreHistory: "これより過去の履歴はありません"
|
||||
startMessaging: "チャットを開始"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
agree: "同意する"
|
||||
agreeBelow: "下記に同意する"
|
||||
basicNotesBeforeCreateAccount: "基本的な注意事項"
|
||||
tos: "利用規約"
|
||||
termsOfService: "利用規約"
|
||||
start: "始める"
|
||||
home: "ホーム"
|
||||
remoteUserCaution: "リモートユーザーのため、情報が不完全です。"
|
||||
activity: "アクティビティ"
|
||||
images: "画像"
|
||||
image: "画像"
|
||||
birthday: "誕生日"
|
||||
yearsOld: "{age}歳"
|
||||
registeredDate: "登録日"
|
||||
@@ -474,6 +476,8 @@ createAccount: "アカウントを作成"
|
||||
existingAccount: "既存のアカウント"
|
||||
regenerate: "再生成"
|
||||
fontSize: "フォントサイズ"
|
||||
mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの高さ"
|
||||
limitTo: "{x}を上限に"
|
||||
noFollowRequests: "フォロー申請はありません"
|
||||
openImageInNewTab: "画像を新しいタブで開く"
|
||||
dashboard: "ダッシュボード"
|
||||
@@ -699,6 +703,8 @@ contact: "連絡先"
|
||||
useSystemFont: "システムのデフォルトのフォントを使う"
|
||||
clips: "クリップ"
|
||||
experimentalFeatures: "実験的機能"
|
||||
experimental: "実験的"
|
||||
thisIsExperimentalFeature: "これは実験的な機能です。仕様が変更されたり、正常に動作しなかったりする可能性があります。"
|
||||
developer: "開発者"
|
||||
makeExplorable: "アカウントを見つけやすくする"
|
||||
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
|
||||
@@ -905,6 +911,7 @@ remoteOnly: "リモートのみ"
|
||||
failedToUpload: "アップロード失敗"
|
||||
cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。"
|
||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
||||
beta: "ベータ"
|
||||
enableAutoSensitive: "自動NSFW判定"
|
||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
||||
@@ -938,6 +945,7 @@ didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
|
||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
|
||||
roles: "ロール"
|
||||
role: "ロール"
|
||||
noRole: "ロールはありません"
|
||||
normalUser: "一般ユーザー"
|
||||
undefined: "未定義"
|
||||
assign: "アサイン"
|
||||
@@ -947,6 +955,10 @@ manageCustomEmojis: "カスタム絵文字の管理"
|
||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||
cannotPerformTemporary: "一時的に利用できません"
|
||||
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
||||
invalidParamError: "パラメータエラー"
|
||||
invalidParamErrorDescription: "リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。"
|
||||
permissionDeniedError: "操作が拒否されました"
|
||||
permissionDeniedErrorDescription: "このアカウントにはこの操作を行うための権限がありません。"
|
||||
preset: "プリセット"
|
||||
selectFromPresets: "プリセットから選択"
|
||||
achievements: "実績"
|
||||
@@ -990,18 +1002,53 @@ enableChartsForFederatedInstances: "リモートサーバーのチャートを
|
||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||
largeNoteReactions: "ノートのリアクションを大きく表示"
|
||||
noteIdOrUrl: "ノートIDまたはURL"
|
||||
accountMigration: "アカウントの引っ越し"
|
||||
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
|
||||
video: "動画"
|
||||
videos: "動画"
|
||||
dataSaver: "データセーバー"
|
||||
accountMigration: "アカウントの移行"
|
||||
accountMoved: "このユーザーは新しいアカウントに移行しました:"
|
||||
accountMovedShort: "このアカウントは移行されています"
|
||||
operationForbidden: "この操作はできません"
|
||||
forceShowAds: "常に広告を表示する"
|
||||
addMemo: "メモを追加"
|
||||
editMemo: "メモを編集"
|
||||
reactionsList: "リアクション一覧"
|
||||
renotesList: "Renote一覧"
|
||||
notificationDisplay: "通知の表示"
|
||||
leftTop: "左上"
|
||||
rightTop: "右上"
|
||||
leftBottom: "左下"
|
||||
rightBottom: "右下"
|
||||
stackAxis: "スタック方向"
|
||||
vertical: "縦"
|
||||
horizontal: "横"
|
||||
position: "位置"
|
||||
serverRules: "サーバールール"
|
||||
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。"
|
||||
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
|
||||
continue: "続ける"
|
||||
preservedUsernames: "予約ユーザー名"
|
||||
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
|
||||
createNoteFromTheFile: "このファイルからノートを作成"
|
||||
|
||||
_serverRules:
|
||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||
|
||||
_accountMigration:
|
||||
moveTo: "このアカウントを新しいアカウントに引っ越す"
|
||||
moveToLabel: "引っ越し先のアカウント:"
|
||||
moveAccountDescription: "この操作は取り消せません。まずは引っ越し先のアカウントでこのアカウントに対しエイリアスを作成したことを確認してください。エイリアス作成後、引っ越し先のアカウントをこのように入力してください:@person@instance.com"
|
||||
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
||||
moveFromLabel: "引っ越し元のアカウント:"
|
||||
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
|
||||
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用できなくなります。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
|
||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||
moveFromSub: "別のアカウントへエイリアスを作成"
|
||||
moveFromLabel: "移行元のアカウント #{n}"
|
||||
moveFromDescription: "別のアカウントからこのアカウントに移行したい場合、ここでエイリアスを作成しておく必要があります。\n移行元のアカウントをこのように入力してください: @username@server.example.com\n削除するには、入力欄を空にして保存します(非推奨)。"
|
||||
moveTo: "このアカウントを新しいアカウントへ移行"
|
||||
moveToLabel: "移行先のアカウント:"
|
||||
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできません。"
|
||||
moveAccountDescription: "新しいアカウントへ移行します。\n ・フォロワーが新しいアカウントを自動でフォローします\n ・このアカウントからのフォローは全て解除されます\n ・このアカウントではノートの作成などができなくなります\n\nフォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。\nリスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。\n\n(この説明はこのサーバー(Misskey v13.12.0以降)の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。)"
|
||||
moveAccountHowTo: "アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。\nエイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com"
|
||||
startMigration: "移行する"
|
||||
migrationConfirm: "本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。"
|
||||
movedAndCannotBeUndone: "\nアカウントは移行されています。\n移行を取り消すことはできません。"
|
||||
postMigrationNote: "このアカウントからのフォロー解除は移行操作から24時間後に実行されます。\nこのアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。"
|
||||
movedTo: "移行先のアカウント:"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
@@ -1174,6 +1221,9 @@ _achievements:
|
||||
_client30min:
|
||||
title: "ひとやすみ"
|
||||
description: "クライアントを起動してから30分以上経過した"
|
||||
_client60min:
|
||||
title: "Misskeyの見すぎ"
|
||||
description: "クライアントを起動してから60分以上経過した"
|
||||
_noteDeletedWithin1min:
|
||||
title: "いまのなし"
|
||||
description: "投稿してから1分以内にその投稿を削除した"
|
||||
@@ -1263,6 +1313,8 @@ _role:
|
||||
iconUrl: "アイコン画像のURL"
|
||||
asBadge: "バッジとして表示"
|
||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||
isExplorable: "ロールタイムラインを公開"
|
||||
descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。"
|
||||
displayOrder: "表示順"
|
||||
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
|
||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||
@@ -1279,6 +1331,7 @@ _role:
|
||||
canInvite: "サーバー招待コードの発行"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
antennaMax: "アンテナの作成可能数"
|
||||
wordMuteMax: "ワードミュートの最大文字数"
|
||||
@@ -1943,6 +1996,7 @@ _deck:
|
||||
channel: "チャンネル"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
roleTimeline: "ロールタイムライン"
|
||||
|
||||
_dialog:
|
||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||
|
@@ -20,6 +20,7 @@ noNotes: "ノートはあらへん"
|
||||
noNotifications: "通知はあらへん"
|
||||
instance: "サーバー"
|
||||
settings: "設定"
|
||||
notificationSettings: "通知の設定"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "ほかの設定"
|
||||
openInWindow: "ウィンドウで開くで"
|
||||
@@ -1407,6 +1408,8 @@ _channel:
|
||||
following: "フォロー中やで"
|
||||
usersCount: "{n}人が参加中やで"
|
||||
notesCount: "{n}こ投稿があるで"
|
||||
nameAndDescription: "名前と説明"
|
||||
nameOnly: "名前だけ"
|
||||
_menuDisplay:
|
||||
sideFull: "横"
|
||||
sideIcon: "横(アイコン)"
|
||||
@@ -1887,6 +1890,7 @@ _deck:
|
||||
channel: "チャンネル"
|
||||
mentions: "あんた宛て"
|
||||
direct: "ダイレクト"
|
||||
roleTimeline: "ロールタイムライン"
|
||||
_dialog:
|
||||
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
|
||||
charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}"
|
||||
|
@@ -20,6 +20,7 @@ noNotes: "没有帖文"
|
||||
noNotifications: "无通知"
|
||||
instance: "服务器"
|
||||
settings: "设置"
|
||||
notificationSettings: "通知设置"
|
||||
basicSettings: "基本设置"
|
||||
otherSettings: "其他设置"
|
||||
openInWindow: "在新窗口中打开"
|
||||
@@ -1407,6 +1408,8 @@ _channel:
|
||||
following: "正在关注"
|
||||
usersCount: "有{n}人参与"
|
||||
notesCount: "有{n}个帖子"
|
||||
nameAndDescription: "名称与描述"
|
||||
nameOnly: "仅名称"
|
||||
_menuDisplay:
|
||||
sideFull: "横向"
|
||||
sideIcon: "横向(图标)"
|
||||
@@ -1887,6 +1890,7 @@ _deck:
|
||||
channel: "频道"
|
||||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
roleTimeline: "角色时间线"
|
||||
_dialog:
|
||||
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
|
||||
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
|
||||
|
@@ -20,6 +20,7 @@ noNotes: "無貼文。"
|
||||
noNotifications: "沒有通知"
|
||||
instance: "實例"
|
||||
settings: "設定"
|
||||
notificationSettings: "通知選項"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "其他設定"
|
||||
openInWindow: "在新視窗開啟"
|
||||
@@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "如果不使用https進行API連接,請關閉"
|
||||
objectStorageUseProxy: "使用網路代理"
|
||||
objectStorageUseProxyDesc: "如果不使用代理進行API連接,請關閉"
|
||||
objectStorageSetPublicRead: "上傳時設定為\"public-read\""
|
||||
s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 會強制將儲存槽名稱指定為 URL 中路徑的一部分,而不是主機名。 使用自託管 Minio 之類的可能需要啟用。"
|
||||
serverLogs: "伺服器日誌"
|
||||
deleteAll: "刪除所有記錄"
|
||||
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
|
||||
@@ -560,7 +562,7 @@ inboxUrl: "收件夾URL"
|
||||
addedRelays: "已加入的中繼"
|
||||
serviceworkerInfo: "您需要啟用推送通知"
|
||||
deletedNote: "已删除的貼文"
|
||||
invisibleNote: "隱藏的貼文"
|
||||
invisibleNote: "私密的貼文"
|
||||
enableInfiniteScroll: "啟用自動滾動頁面模式"
|
||||
visibility: "可見性"
|
||||
poll: "投票"
|
||||
@@ -919,6 +921,7 @@ pushNotificationNotSupported: "瀏覽器或實例不支援推播通知"
|
||||
sendPushNotificationReadMessage: "通知與訊息如果已讀的話,就將推播通知刪除"
|
||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會增加設備的電池消耗。"
|
||||
windowMaximize: "最大化"
|
||||
windowMinimize: "最小化"
|
||||
windowRestore: "復原"
|
||||
caption: "標題"
|
||||
loggedInAsBot: "以機器人帳戶登入中"
|
||||
@@ -960,6 +963,9 @@ copyErrorInfo: "複製錯誤資訊"
|
||||
joinThisServer: "在此伺服器上註冊"
|
||||
exploreOtherServers: "探索其他伺服器"
|
||||
letsLookAtTimeline: "看看時間軸"
|
||||
disableFederationConfirm: "要停止聯邦功能嗎?"
|
||||
disableFederationConfirmWarn: "即使停止了聯邦功能,貼文也不會變成私密的。在大部分的情況下,沒有必要停止聯邦功能。"
|
||||
disableFederationOk: "停止聯邦功能"
|
||||
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
|
||||
emailNotSupported: "這個伺服器不支援寄送郵件"
|
||||
postToTheChannel: "發布到頻道"
|
||||
@@ -985,6 +991,7 @@ showClipButtonInNoteFooter: "將摘錄添加至貼文"
|
||||
largeNoteReactions: "將貼文的反應放大顯示"
|
||||
noteIdOrUrl: "貼文ID或URL"
|
||||
accountMigration: "遷移帳戶"
|
||||
accountMoved: "這個使用者已遷移至新的帳戶:"
|
||||
forceShowAds: "總是顯示廣告"
|
||||
_accountMigration:
|
||||
moveTo: "將這個帳戶遷移至新的帳戶"
|
||||
@@ -1401,6 +1408,8 @@ _channel:
|
||||
following: "關注中"
|
||||
usersCount: "有{n}人參與"
|
||||
notesCount: "有{n}個貼文"
|
||||
nameAndDescription: "名稱與說明"
|
||||
nameOnly: "僅名稱"
|
||||
_menuDisplay:
|
||||
sideFull: "側向"
|
||||
sideIcon: "側向(圖示)"
|
||||
@@ -1881,6 +1890,7 @@ _deck:
|
||||
channel: "頻道"
|
||||
mentions: "提及"
|
||||
direct: "指定使用者"
|
||||
roleTimeline: "角色時間軸"
|
||||
_dialog:
|
||||
charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}"
|
||||
charactersBelow: "低於最少字數!現在 {current} / 限制 {max}"
|
||||
|
16
package.json
16
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.11.2",
|
||||
"version": "13.12.0-beta.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"packageManager": "pnpm@8.3.1",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
@@ -51,19 +51,19 @@
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"typescript": "5.0.3"
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"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.59.2",
|
||||
"@typescript-eslint/parser": "5.59.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.9.0",
|
||||
"eslint": "8.37.0",
|
||||
"cypress": "12.11.0",
|
||||
"eslint": "8.39.0",
|
||||
"start-server-and-test": "2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.2.0"
|
||||
"@tensorflow/tfjs-core": "4.4.0"
|
||||
}
|
||||
}
|
||||
|
18
packages/backend/migration/1680702787050-UserMemo.js
Normal file
18
packages/backend/migration/1680702787050-UserMemo.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export class UserMemo1680702787050 {
|
||||
name = 'UserMemo1680702787050'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "user_memo" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "targetUserId" character varying(32) NOT NULL, "memo" character varying(2048) NOT NULL, CONSTRAINT "PK_e9aaa58f7d3699a84d79078f4d9" PRIMARY KEY ("id")); COMMENT ON COLUMN "user_memo"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "user_memo"."targetUserId" IS 'The ID of target user.'; COMMENT ON COLUMN "user_memo"."memo" IS 'Memo.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_650b49c5639b5840ee6a2b8f83" ON "user_memo" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_66ac4a82894297fd09ba61f3d3" ON "user_memo" ("targetUserId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_faef300913c738265638ba3ebc" ON "user_memo" ("userId", "targetUserId") `);
|
||||
await queryRunner.query(`ALTER TABLE "user_memo" ADD CONSTRAINT "FK_650b49c5639b5840ee6a2b8f83e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_memo" ADD CONSTRAINT "FK_66ac4a82894297fd09ba61f3d35" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_memo" DROP CONSTRAINT "FK_66ac4a82894297fd09ba61f3d35"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_memo" DROP CONSTRAINT "FK_650b49c5639b5840ee6a2b8f83e"`);
|
||||
await queryRunner.query(`DROP TABLE "user_memo"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1681400427971-serverRules.js
Normal file
11
packages/backend/migration/1681400427971-serverRules.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export class ServerRules1681400427971 {
|
||||
name = 'ServerRules1681400427971'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "serverRules" character varying(280) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverRules"`);
|
||||
}
|
||||
}
|
12
packages/backend/migration/1681870960239-RoleTLSetting.js
Normal file
12
packages/backend/migration/1681870960239-RoleTLSetting.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export class RoleTLSetting1681870960239 {
|
||||
name = 'RoleTLSetting1681870960239'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "isExplorable" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "isExplorable"`);
|
||||
}
|
||||
|
||||
}
|
13
packages/backend/migration/1682190963894-movedAt.js
Normal file
13
packages/backend/migration/1682190963894-movedAt.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export class MovedAt1682190963894 {
|
||||
name = 'MovedAt1682190963894'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "movedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."movedAt" IS 'When the user moved to another account'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."movedAt" IS 'When the user moved to another account'`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedAt"`);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
export class PreservedUsernames1682754135458 {
|
||||
name = 'PreservedUsernames1682754135458'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "preservedUsernames" character varying(1024) array NOT NULL DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "preservedUsernames"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1682985520254-channelColor.js
Normal file
11
packages/backend/migration/1682985520254-channelColor.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export class ChannelColor1682985520254 {
|
||||
name = 'ChannelColor1682985520254'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "channel" ADD "color" character varying(16) NOT NULL DEFAULT '#86b300'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "color"`);
|
||||
}
|
||||
}
|
@@ -23,33 +23,33 @@
|
||||
},
|
||||
"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",
|
||||
"@tensorflow/tfjs": "4.2.0",
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
"@swc/core-darwin-arm64": "1.3.56",
|
||||
"@swc/core-darwin-x64": "1.3.56",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.56",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.56",
|
||||
"@swc/core-linux-arm64-musl": "1.3.56",
|
||||
"@swc/core-linux-x64-gnu": "1.3.56",
|
||||
"@swc/core-linux-x64-musl": "1.3.56",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.56",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.56",
|
||||
"@swc/core-win32-x64-msvc": "1.3.56",
|
||||
"@tensorflow/tfjs": "4.4.0",
|
||||
"@tensorflow/tfjs-node": "4.4.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",
|
||||
"@aws-sdk/client-s3": "3.321.1",
|
||||
"@aws-sdk/lib-storage": "3.321.1",
|
||||
"@aws-sdk/node-http-handler": "3.321.1",
|
||||
"@bull-board/api": "5.1.2",
|
||||
"@bull-board/fastify": "5.1.2",
|
||||
"@bull-board/ui": "5.1.2",
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
"@fastify/cors": "8.2.1",
|
||||
"@fastify/http-proxy": "9.0.0",
|
||||
"@fastify/multipart": "7.5.0",
|
||||
"@fastify/static": "6.10.0",
|
||||
"@fastify/multipart": "7.6.0",
|
||||
"@fastify/static": "6.10.1",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.4.0",
|
||||
"@nestjs/core": "9.4.0",
|
||||
@@ -57,7 +57,7 @@
|
||||
"@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.56",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
@@ -73,25 +73,26 @@
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.29.3",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.15.0",
|
||||
"fastify": "4.17.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.2.1",
|
||||
"file-type": "18.3.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "12.6.0",
|
||||
"happy-dom": "8.9.0",
|
||||
"happy-dom": "9.10.2",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "4.28.5",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "21.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.1.1",
|
||||
"jsrsasign": "10.7.0",
|
||||
"meilisearch": "0.32.3",
|
||||
"jsrsasign": "10.8.6",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -111,7 +112,7 @@
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.3.0",
|
||||
"pureimage": "0.3.17",
|
||||
"qrcode": "1.5.1",
|
||||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.18.0",
|
||||
@@ -119,13 +120,13 @@
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"rndstr": "1.0.0",
|
||||
"rss-parser": "3.12.0",
|
||||
"rxjs": "7.8.0",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.10.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"semver": "7.3.8",
|
||||
"sharp": "0.32.0",
|
||||
"semver": "7.5.0",
|
||||
"sharp": "0.32.1",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
@@ -133,23 +134,23 @@
|
||||
"systeminformation": "5.17.12",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.5",
|
||||
"tsc-alias": "1.8.6",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.13",
|
||||
"typescript": "5.0.3",
|
||||
"typeorm": "0.3.15",
|
||||
"typescript": "5.0.4",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.5.0",
|
||||
"web-push": "3.6.1",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.13.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.5.0",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@swc/jest": "0.2.26",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.2",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
@@ -159,14 +160,13 @@
|
||||
"@types/content-disposition": "0.5.5",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/jest": "29.5.1",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "21.1.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/node": "18.16.3",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/oauth": "0.9.1",
|
||||
@@ -180,7 +180,7 @@
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/sharp": "0.32.0",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
@@ -190,11 +190,11 @@
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.2",
|
||||
"@typescript-eslint/parser": "5.59.2",
|
||||
"aws-sdk-client-mock": "^2.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.37.0",
|
||||
"eslint": "8.39.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.5.0",
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { MeiliSearch } from 'meilisearch';
|
||||
import { DI } from './di-symbols.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
@@ -22,10 +23,25 @@ const $db: Provider = {
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $meilisearch: Provider = {
|
||||
provide: DI.meilisearch,
|
||||
useFactory: (config) => {
|
||||
if (config.meilisearch) {
|
||||
return new MeiliSearch({
|
||||
host: `http://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
apiKey: config.meilisearch.apiKey,
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $redis: Provider = {
|
||||
provide: DI.redis,
|
||||
useFactory: (config) => {
|
||||
return new Redis({
|
||||
return new Redis.Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
@@ -40,7 +56,7 @@ const $redis: Provider = {
|
||||
const $redisForPub: Provider = {
|
||||
provide: DI.redisForPub,
|
||||
useFactory: (config) => {
|
||||
const redis = new Redis({
|
||||
const redis = new Redis.Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||
@@ -56,7 +72,7 @@ const $redisForPub: Provider = {
|
||||
const $redisForSub: Provider = {
|
||||
provide: DI.redisForSub,
|
||||
useFactory: (config) => {
|
||||
const redis = new Redis({
|
||||
const redis = new Redis.Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||
@@ -73,8 +89,8 @@ const $redisForSub: Provider = {
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisForPub, $redisForSub],
|
||||
exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
|
||||
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
|
||||
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
|
||||
})
|
||||
export class GlobalModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
/**
|
||||
@@ -57,13 +57,10 @@ export type Source = {
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
elasticsearch: {
|
||||
meilisearch?: {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl?: boolean;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
index?: string;
|
||||
port: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
proxy?: string;
|
||||
@@ -84,8 +81,10 @@ export type Source = {
|
||||
|
||||
deliverJobConcurrency?: number;
|
||||
inboxJobConcurrency?: number;
|
||||
relashionshipJobConcurrency?: number;
|
||||
deliverJobPerSec?: number;
|
||||
inboxJobPerSec?: number;
|
||||
relashionshipJobPerSec?: number;
|
||||
deliverJobMaxAttempts?: number;
|
||||
inboxJobMaxAttempts?: number;
|
||||
|
||||
@@ -132,9 +131,11 @@ const dir = `${_dirname}/../../../.config`;
|
||||
/**
|
||||
* Path of configuration file
|
||||
*/
|
||||
const path = process.env.NODE_ENV === 'test'
|
||||
? `${dir}/test.yml`
|
||||
: `${dir}/default.yml`;
|
||||
const path = process.env.MISSKEY_CONFIG_YML
|
||||
? resolve(dir, process.env.MISSKEY_CONFIG_YML)
|
||||
: process.env.NODE_ENV === 'test'
|
||||
? resolve(dir, 'test.yml')
|
||||
: resolve(dir, 'default.yml');
|
||||
|
||||
export function loadConfig() {
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||
|
@@ -56,6 +56,11 @@ export const FILE_TYPE_BROWSERSAFE = [
|
||||
'audio/webm',
|
||||
|
||||
'audio/aac',
|
||||
|
||||
// see https://github.com/misskey-dev/misskey/pull/10686
|
||||
'audio/flac',
|
||||
'audio/wav',
|
||||
// backward compatibility
|
||||
'audio/x-flac',
|
||||
'audio/vnd.wave',
|
||||
];
|
||||
|
@@ -1,55 +1,90 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { IsNull, In, MoreThan, Not } from 'typeorm';
|
||||
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import { User } from '@/models/entities/User.js';
|
||||
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMoveService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private apPersonService: ApPersonService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private accountUpdateService: AccountUpdateService,
|
||||
private proxyAccountService: ProxyAccountService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private instanceChart: InstanceChart,
|
||||
private metaService: MetaService,
|
||||
private relayService: RelayService,
|
||||
private cacheService: CacheService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a local account to a remote account.
|
||||
* Move a local account to a new account.
|
||||
*
|
||||
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
|
||||
*/
|
||||
@bindThis
|
||||
public async moveToRemote(src: LocalUser, dst: User): Promise<unknown> {
|
||||
// Make sure that the destination is a remote account.
|
||||
if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote');
|
||||
if (!dst.uri) throw new Error('destination uri is empty');
|
||||
public async moveFromLocal(src: LocalUser, dst: LocalUser | RemoteUser): Promise<unknown> {
|
||||
const srcUri = this.userEntityService.getUserUri(src);
|
||||
const dstUri = this.userEntityService.getUserUri(dst);
|
||||
|
||||
// add movedToUri to indicate that the user has moved
|
||||
const update = {} as Partial<User>;
|
||||
update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri];
|
||||
update.movedToUri = dst.uri;
|
||||
const update = {} as Partial<LocalUser>;
|
||||
update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri];
|
||||
update.movedToUri = dstUri;
|
||||
update.movedAt = new Date();
|
||||
await this.usersRepository.update(src.id, update);
|
||||
Object.assign(src, update);
|
||||
|
||||
// Update cache
|
||||
this.cacheService.uriPersonCache.set(srcUri, src);
|
||||
|
||||
const srcPerson = await this.apRendererService.renderPerson(src);
|
||||
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
||||
@@ -64,51 +99,249 @@ export class AccountMoveService {
|
||||
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
|
||||
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
|
||||
|
||||
// follow the new account and unfollow the old one
|
||||
const followings = await this.followingsRepository.find({
|
||||
relations: {
|
||||
follower: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: src.id,
|
||||
followerHost: IsNull(), // follower is local
|
||||
},
|
||||
// Unfollow after 24 hours
|
||||
const followings = await this.followingsRepository.findBy({
|
||||
followerId: src.id,
|
||||
});
|
||||
for (const following of followings) {
|
||||
if (!following.follower) continue;
|
||||
try {
|
||||
await this.userFollowingService.follow(following.follower, dst);
|
||||
await this.userFollowingService.unfollow(following.follower, src);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
this.queueService.createDelayedUnfollowJob(followings.map(following => ({
|
||||
from: { id: src.id },
|
||||
to: { id: following.followeeId },
|
||||
})), process.env.NODE_ENV === 'test' ? 10000 : 1000 * 60 * 60 * 24);
|
||||
|
||||
await this.postMoveProcess(src, dst);
|
||||
|
||||
return iObj;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async postMoveProcess(src: User, dst: User): Promise<void> {
|
||||
// Copy blockings and mutings, and update lists
|
||||
try {
|
||||
await Promise.all([
|
||||
this.copyBlocking(src, dst),
|
||||
this.copyMutings(src, dst),
|
||||
this.updateLists(src, dst),
|
||||
]);
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
}
|
||||
|
||||
// follow the new account
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
const followings = await this.followingsRepository.findBy({
|
||||
followeeId: src.id,
|
||||
followerHost: IsNull(), // follower is local
|
||||
followerId: proxy ? Not(proxy.id) : undefined,
|
||||
});
|
||||
const followJobs = followings.map(following => ({
|
||||
from: { id: following.followerId },
|
||||
to: { id: dst.id },
|
||||
})) as RelationshipJobData[];
|
||||
|
||||
// Decrease following count instead of unfollowing.
|
||||
try {
|
||||
await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src);
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
}
|
||||
|
||||
// Should be queued because this can cause a number of follow per one move.
|
||||
this.queueService.createFollowJob(followJobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async copyBlocking(src: ThinUser, dst: ThinUser): Promise<void> {
|
||||
// Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving.
|
||||
// So block the destination account here.
|
||||
const srcBlockings = await this.blockingsRepository.findBy({ blockeeId: src.id });
|
||||
const dstBlockings = await this.blockingsRepository.findBy({ blockeeId: dst.id });
|
||||
const blockerIds = dstBlockings.map(blocking => blocking.blockerId);
|
||||
// reblock the destination account
|
||||
const blockJobs: RelationshipJobData[] = [];
|
||||
for (const blocking of srcBlockings) {
|
||||
if (blockerIds.includes(blocking.blockerId)) continue; // skip if already blocked
|
||||
blockJobs.push({ from: { id: blocking.blockerId }, to: { id: dst.id } });
|
||||
}
|
||||
// no need to unblock the old account because it may be still functional
|
||||
this.queueService.createBlockJob(blockJobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async copyMutings(src: ThinUser, dst: ThinUser): Promise<void> {
|
||||
// Insert new mutings with the same values except mutee
|
||||
const oldMutings = await this.mutingsRepository.findBy([
|
||||
{ muteeId: src.id, expiresAt: IsNull() },
|
||||
{ muteeId: src.id, expiresAt: MoreThan(new Date()) },
|
||||
]);
|
||||
if (oldMutings.length === 0) return;
|
||||
|
||||
// Check if the destination account is already indefinitely muted by the muter
|
||||
const existingMutingsMuterUserIds = await this.mutingsRepository.findBy(
|
||||
{ muteeId: dst.id, expiresAt: IsNull() },
|
||||
).then(mutings => mutings.map(muting => muting.muterId));
|
||||
|
||||
const newMutings: Map<string, { muterId: string; muteeId: string; createdAt: Date; expiresAt: Date | null; }> = new Map();
|
||||
|
||||
// 重複しないようにIDを生成
|
||||
const genId = (): string => {
|
||||
let id: string;
|
||||
do {
|
||||
id = this.idService.genId();
|
||||
} while (newMutings.has(id));
|
||||
return id;
|
||||
};
|
||||
for (const muting of oldMutings) {
|
||||
if (existingMutingsMuterUserIds.includes(muting.muterId)) continue; // skip if already muted indefinitely
|
||||
newMutings.set(genId(), {
|
||||
...muting,
|
||||
createdAt: new Date(),
|
||||
muteeId: dst.id,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayToInsert = Array.from(newMutings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||
await this.mutingsRepository.insert(arrayToInsert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alias of an old remote account.
|
||||
* Update lists while moving accounts.
|
||||
* - No removal of the old account from the lists
|
||||
* - Users number limit is not checked
|
||||
*
|
||||
* The user's new profile will be published to the followers.
|
||||
* @param src ThinUser (old account)
|
||||
* @param dst User (new account)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
@bindThis
|
||||
public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
|
||||
await this.usersRepository.update(me.id, updates);
|
||||
|
||||
// Publish meUpdated event
|
||||
const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
public async updateLists(src: ThinUser, dst: User): Promise<void> {
|
||||
// Return if there is no list to be updated.
|
||||
const oldJoinings = await this.userListJoiningsRepository.find({
|
||||
where: {
|
||||
userId: src.id,
|
||||
},
|
||||
});
|
||||
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
|
||||
if (oldJoinings.length === 0) return;
|
||||
|
||||
if (me.isLocked === false) {
|
||||
await this.userFollowingService.acceptAllFollowRequests(me);
|
||||
const existingUserListIds = await this.userListJoiningsRepository.find({
|
||||
where: {
|
||||
userId: dst.id,
|
||||
},
|
||||
}).then(joinings => joinings.map(joining => joining.userListId));
|
||||
|
||||
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
||||
|
||||
// 重複しないようにIDを生成
|
||||
const genId = (): string => {
|
||||
let id: string;
|
||||
do {
|
||||
id = this.idService.genId();
|
||||
} while (newJoinings.has(id));
|
||||
return id;
|
||||
};
|
||||
for (const joining of oldJoinings) {
|
||||
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
|
||||
newJoinings.set(genId(), {
|
||||
createdAt: new Date(),
|
||||
userId: dst.id,
|
||||
userListId: joining.userListId,
|
||||
});
|
||||
}
|
||||
|
||||
this.accountUpdateService.publishToFollowers(me.id);
|
||||
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||
await this.userListJoiningsRepository.insert(arrayToInsert);
|
||||
|
||||
return iObj;
|
||||
// Have the proxy account follow the new account in the same way as UserListService.push
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
if (proxy) {
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise<void> {
|
||||
if (localFollowerIds.length === 0) return;
|
||||
|
||||
// Set the old account's following and followers counts to 0.
|
||||
await this.usersRepository.update({ id: oldAccount.id }, { followersCount: 0, followingCount: 0 });
|
||||
|
||||
// Decrease following counts of local followers by 1.
|
||||
await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1);
|
||||
|
||||
// Decrease follower counts of local followees by 1.
|
||||
const oldFollowings = await this.followingsRepository.findBy({ followerId: oldAccount.id });
|
||||
if (oldFollowings.length > 0) {
|
||||
await this.usersRepository.decrement({ id: In(oldFollowings.map(following => following.followeeId)) }, 'followersCount', 1);
|
||||
}
|
||||
|
||||
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
|
||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: expensive?
|
||||
for (const followerId of localFollowerIds) {
|
||||
this.perUserFollowingChart.update({ id: followerId, host: null }, oldAccount, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
|
||||
*
|
||||
* @param dst movedToUrlを指定するユーザー
|
||||
* @param check
|
||||
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
|
||||
* @returns Promise<LocalUser | RemoteUser | null>
|
||||
*/
|
||||
@bindThis
|
||||
public async validateAlsoKnownAs(
|
||||
dst: LocalUser | RemoteUser,
|
||||
check: (oldUser: LocalUser | RemoteUser | null, newUser: LocalUser | RemoteUser) => boolean | Promise<boolean> = () => true,
|
||||
instant = false,
|
||||
): Promise<LocalUser | RemoteUser | null> {
|
||||
let resultUser: LocalUser | RemoteUser | null = null;
|
||||
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||
await this.apPersonService.updatePerson(dst.uri);
|
||||
}
|
||||
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
|
||||
}
|
||||
|
||||
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) return null;
|
||||
|
||||
const dstUri = this.userEntityService.getUserUri(dst);
|
||||
|
||||
for (const srcUri of dst.alsoKnownAs) {
|
||||
try {
|
||||
let src = await this.apPersonService.fetchPerson(srcUri);
|
||||
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
|
||||
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||
await this.apPersonService.updatePerson(srcUri);
|
||||
}
|
||||
|
||||
src = await this.apPersonService.fetchPerson(srcUri) ?? src;
|
||||
}
|
||||
|
||||
if (src.movedToUri === dstUri) {
|
||||
if (await check(resultUser, src)) {
|
||||
resultUser = src;
|
||||
}
|
||||
if (instant && resultUser) return resultUser;
|
||||
}
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
}
|
||||
}
|
||||
|
||||
return resultUser;
|
||||
}
|
||||
}
|
||||
|
@@ -64,6 +64,7 @@ export const ACHIEVEMENT_TYPES = [
|
||||
'iLoveMisskey',
|
||||
'foundTreasure',
|
||||
'client30min',
|
||||
'client60min',
|
||||
'noteDeletedWithin1min',
|
||||
'postedAtLateNight',
|
||||
'postedAt0min0sec',
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { Antenna } from '@/models/entities/Antenna.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
@@ -91,14 +91,24 @@ 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',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||
public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise<void> {
|
||||
const antennas = await this.getAntennas();
|
||||
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
||||
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
|
||||
for (const antenna of matchedAntennas) {
|
||||
redisPipeline.xadd(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||
}
|
||||
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { promisify } from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import redisLock from 'redis-lock';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
|
@@ -50,6 +50,7 @@ import { WebhookService } from './WebhookService.js';
|
||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||
import { UtilityService } from './UtilityService.js';
|
||||
import { FileInfoService } from './FileInfoService.js';
|
||||
import { SearchService } from './SearchService.js';
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
import FederationChart from './chart/charts/federation.js';
|
||||
import NotesChart from './chart/charts/notes.js';
|
||||
@@ -171,6 +172,8 @@ const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', u
|
||||
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
const $NotesChart: Provider = { provide: 'NotesChart', useExisting: NotesChart };
|
||||
@@ -295,6 +298,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
WebhookService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -413,6 +417,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$WebhookService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -532,6 +537,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
WebhookService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
UsersChart,
|
||||
@@ -649,6 +655,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$WebhookService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
$UsersChart,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
@@ -13,6 +13,7 @@ import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import type { Serialized } from '@/server/api/stream/types.js';
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
@@ -44,7 +45,13 @@ export class CustomEmojiService {
|
||||
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
|
||||
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
|
||||
fromRedisConverter: (value) => {
|
||||
if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す)
|
||||
return new Map(JSON.parse(value).map((x: Serialized<Emoji>) => [x.name, {
|
||||
...x,
|
||||
updatedAt: x.updatedAt ? new Date(x.updatedAt) : null,
|
||||
}]));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,6 +197,22 @@ export class CustomEmojiService {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
license: license,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(id: Emoji['id']) {
|
||||
@@ -267,16 +290,7 @@ export class CustomEmojiService {
|
||||
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
|
||||
if (emoji == null) return null;
|
||||
|
||||
const isLocal = emoji.host == null;
|
||||
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
const url = isLocal
|
||||
? emojiUrl
|
||||
: this.config.proxyRemoteFiles
|
||||
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
|
||||
: emojiUrl;
|
||||
|
||||
return url;
|
||||
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -86,9 +86,13 @@ export class DownloadService {
|
||||
|
||||
const contentDisposition = res.headers['content-disposition'];
|
||||
if (contentDisposition != null) {
|
||||
const parsed = parse(contentDisposition);
|
||||
if (parsed.parameters.filename) {
|
||||
filename = parsed.parameters.filename;
|
||||
try {
|
||||
const parsed = parse(contentDisposition);
|
||||
if (parsed.parameters.filename) {
|
||||
filename = parsed.parameters.filename;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`Failed to parse content-disposition: ${contentDisposition}`, { stack: e });
|
||||
}
|
||||
}
|
||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||
|
@@ -59,6 +59,8 @@ type AddFileArgs = {
|
||||
uri?: string | null;
|
||||
/** Mark file as sensitive */
|
||||
sensitive?: boolean | null;
|
||||
/** Extension to force */
|
||||
ext?: string | null;
|
||||
|
||||
requestIp?: string | null;
|
||||
requestHeaders?: Record<string, string> | null;
|
||||
@@ -125,7 +127,7 @@ export class DriveService {
|
||||
/***
|
||||
* Save file
|
||||
* @param path Path for original
|
||||
* @param name Name for original
|
||||
* @param name Name for original (should be extention corrected)
|
||||
* @param type Content-Type for original
|
||||
* @param hash Hash for original
|
||||
* @param size Size for original
|
||||
@@ -151,7 +153,7 @@ export class DriveService {
|
||||
}
|
||||
|
||||
// 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、
|
||||
// 許可されているファイル形式でしか拡張子をつけない
|
||||
// 許可されているファイル形式でしかURLに拡張子をつけない
|
||||
if (!FILE_TYPE_BROWSERSAFE.includes(type)) {
|
||||
ext = '';
|
||||
}
|
||||
@@ -173,7 +175,7 @@ export class DriveService {
|
||||
//#region Uploads
|
||||
this.registerLogger.info(`uploading original: ${key}`);
|
||||
const uploads = [
|
||||
this.upload(key, fs.createReadStream(path), type, ext, name),
|
||||
this.upload(key, fs.createReadStream(path), type, null, name),
|
||||
];
|
||||
|
||||
if (alts.webpublic) {
|
||||
@@ -189,7 +191,7 @@ export class DriveService {
|
||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||
|
||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext));
|
||||
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`));
|
||||
}
|
||||
|
||||
await Promise.all(uploads);
|
||||
@@ -396,8 +398,9 @@ export class DriveService {
|
||||
);
|
||||
}
|
||||
|
||||
// Expire oldest file (without avatar or banner) of remote user
|
||||
@bindThis
|
||||
private async deleteOldFile(user: RemoteUser) {
|
||||
private async expireOldFile(user: RemoteUser, driveCapacity: number) {
|
||||
const q = this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.andWhere('file.isLink = FALSE');
|
||||
@@ -410,12 +413,17 @@ export class DriveService {
|
||||
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
|
||||
}
|
||||
|
||||
//This selete is hard coded, be careful if change database schema
|
||||
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
|
||||
q.orderBy('file.id', 'ASC');
|
||||
|
||||
const oldFile = await q.getOne();
|
||||
const fileList = await q.getRawMany();
|
||||
const exceedFileIds = fileList.filter((x: any) => x.acc_usage > driveCapacity).map((x: any) => x.file_id);
|
||||
|
||||
if (oldFile) {
|
||||
this.deleteFile(oldFile, true);
|
||||
for (const fileId of exceedFileIds) {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: fileId });
|
||||
if (file == null) continue;
|
||||
this.deleteFile(file, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,10 +445,16 @@ export class DriveService {
|
||||
sensitive = null,
|
||||
requestIp = null,
|
||||
requestHeaders = null,
|
||||
ext = null,
|
||||
}: AddFileArgs): Promise<DriveFile> {
|
||||
let skipNsfwCheck = false;
|
||||
const instance = await this.metaService.fetch();
|
||||
if (user == null) skipNsfwCheck = true;
|
||||
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
||||
if (user == null) {
|
||||
skipNsfwCheck = true;
|
||||
} else if (userRoleNSFW) {
|
||||
skipNsfwCheck = true;
|
||||
}
|
||||
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
|
||||
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
|
||||
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
||||
@@ -468,7 +482,7 @@ export class DriveService {
|
||||
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
|
||||
// extを付加してデータベースの文字数制限に当たることはまずない
|
||||
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
|
||||
info.type.ext,
|
||||
ext ?? info.type.ext,
|
||||
);
|
||||
|
||||
if (user && !force) {
|
||||
@@ -489,22 +503,19 @@ export class DriveService {
|
||||
//#region Check drive usage
|
||||
if (user && !isLink) {
|
||||
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
|
||||
const isLocalUser = this.userEntityService.isLocalUser(user);
|
||||
|
||||
const policies = await this.roleService.getUserPolicies(user.id);
|
||||
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
|
||||
this.registerLogger.debug('drive capacity override applied');
|
||||
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
|
||||
|
||||
this.registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
|
||||
|
||||
// If usage limit exceeded
|
||||
if (usage + info.size > driveCapacity) {
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
if (driveCapacity < usage + info.size) {
|
||||
if (isLocalUser) {
|
||||
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
|
||||
} else {
|
||||
// (アバターまたはバナーを含まず)最も古いファイルを削除する
|
||||
this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser);
|
||||
}
|
||||
await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser, driveCapacity - info.size);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
@@ -565,6 +576,7 @@ export class DriveService {
|
||||
|
||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||
if (userRoleNSFW) file.isSensitive = true;
|
||||
|
||||
if (url !== null) {
|
||||
file.src = url;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
@@ -23,8 +23,8 @@ export class FederatedInstanceService {
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
|
||||
lifetime: 1000 * 60 * 60 * 24, // 24h
|
||||
memoryCacheLifetime: 1000 * 60 * 30, // 30m
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
|
||||
toRedisConverter: (value) => JSON.stringify(value),
|
||||
fromRedisConverter: (value) => {
|
||||
@@ -65,15 +65,18 @@ export class FederatedInstanceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
public async update(id: Instance['id'], data: Partial<Instance>): Promise<void> {
|
||||
const result = await this.instancesRepository.createQueryBuilder().update()
|
||||
.set(data)
|
||||
.where('id = :id', { id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
|
||||
const updated = result.raw[0];
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
if (cached == null) return;
|
||||
|
||||
this.federatedInstanceCache.set(host, {
|
||||
...cached,
|
||||
...data,
|
||||
});
|
||||
this.federatedInstanceCache.set(updated.host, updated);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import type { DOMWindow } from 'jsdom';
|
||||
|
||||
type NodeInfo = {
|
||||
@@ -42,6 +43,7 @@ export class FetchInstanceMetadataService {
|
||||
private appLockService: AppLockService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('metadata', 'cyan');
|
||||
}
|
||||
@@ -96,7 +98,7 @@ export class FetchInstanceMetadataService {
|
||||
if (favicon) updates.faviconUrl = favicon;
|
||||
if (themeColor) updates.themeColor = themeColor;
|
||||
|
||||
await this.instancesRepository.update(instance.id, updates);
|
||||
await this.federatedInstanceService.update(instance.id, updates);
|
||||
|
||||
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
|
||||
} catch (e) {
|
||||
|
@@ -5,7 +5,7 @@ import * as stream from 'node:stream';
|
||||
import * as util from 'node:util';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FSWatcher } from 'chokidar';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import * as fileType from 'file-type';
|
||||
import FFmpeg from 'fluent-ffmpeg';
|
||||
import isSvg from 'is-svg';
|
||||
import probeImageSize from 'probe-image-size';
|
||||
@@ -301,21 +301,34 @@ export class FileInfoService {
|
||||
return fs.promises.access(path).then(() => true, () => false);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public fixMime(mime: string | fileType.MimeType): string {
|
||||
// see https://github.com/misskey-dev/misskey/pull/10686
|
||||
if (mime === "audio/x-flac") {
|
||||
return "audio/flac";
|
||||
}
|
||||
if (mime === "audio/vnd.wave") {
|
||||
return "audio/wav";
|
||||
}
|
||||
|
||||
return mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME Type and extension
|
||||
*/
|
||||
@bindThis
|
||||
public async detectType(path: string): Promise<{
|
||||
mime: string;
|
||||
ext: string | null;
|
||||
}> {
|
||||
mime: string;
|
||||
ext: string | null;
|
||||
}> {
|
||||
// Check 0 byte
|
||||
const fileSize = await this.getFileSize(path);
|
||||
if (fileSize === 0) {
|
||||
return TYPE_OCTET_STREAM;
|
||||
}
|
||||
|
||||
const type = await fileTypeFromFile(path);
|
||||
const type = await fileType.fileTypeFromFile(path);
|
||||
|
||||
if (type) {
|
||||
// XMLはSVGかもしれない
|
||||
@@ -324,7 +337,7 @@ export class FileInfoService {
|
||||
}
|
||||
|
||||
return {
|
||||
mime: type.mime,
|
||||
mime: this.fixMime(type.mime),
|
||||
ext: type.ext,
|
||||
};
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { UserList } from '@/models/entities/UserList.js';
|
||||
@@ -14,11 +14,13 @@ import type {
|
||||
MainStreamTypes,
|
||||
NoteStreamTypes,
|
||||
UserListStreamTypes,
|
||||
RoleTimelineStreamTypes,
|
||||
} from '@/server/api/stream/types.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Role } from '@/models';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalEventService {
|
||||
@@ -81,6 +83,11 @@ export class GlobalEventService {
|
||||
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: Role['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
|
||||
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishNotesStream(note: Packed<'Note'>): void {
|
||||
this.publish('notesStream', null, note);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Meta } from '@/models/entities/Meta.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { setImmediate } from 'node:timers/promises';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { In, DataSource } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
@@ -46,6 +46,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
|
||||
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
||||
|
||||
@@ -198,6 +199,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private apRendererService: ApRendererService,
|
||||
private roleService: RoleService,
|
||||
private metaService: MetaService,
|
||||
private searchService: SearchService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
@@ -493,14 +495,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
});
|
||||
|
||||
// Antenna
|
||||
for (const antenna of (await this.antennaService.getAntennas())) {
|
||||
this.antennaService.checkHitAntenna(antenna, note, user).then(hit => {
|
||||
if (hit) {
|
||||
this.antennaService.addNoteToAntenna(antenna, note, user);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.antennaService.addNoteToAntennas(note, user);
|
||||
|
||||
if (data.reply) {
|
||||
this.saveReply(data.reply, note);
|
||||
@@ -554,6 +549,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
this.globalEventService.publishNotesStream(noteObj);
|
||||
|
||||
this.roleService.addNoteToRoleTimeline(noteObj);
|
||||
|
||||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -733,17 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
private index(note: Note) {
|
||||
if (note.text == null || this.config.elasticsearch == null) return;
|
||||
/*
|
||||
es!.index({
|
||||
index: this.config.elasticsearch.index ?? 'misskey_note',
|
||||
id: note.id.toString(),
|
||||
body: {
|
||||
text: normalizeForSearch(note.text),
|
||||
userId: note.userId,
|
||||
userHost: note.userHost,
|
||||
},
|
||||
});*/
|
||||
if (note.text == null && note.cw == null) return;
|
||||
|
||||
this.searchService.indexNote(note);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -111,7 +111,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
|
||||
|
||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import push from 'web-push';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { Packed } from '@/misc/json-schema';
|
||||
|
@@ -3,7 +3,7 @@ import Bull from 'bull';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from '../queue/types.js';
|
||||
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
|
||||
|
||||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
||||
return new Bull<T>(name, {
|
||||
@@ -41,7 +41,8 @@ export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||
export type DbQueue = Bull.Queue<DbJobData>;
|
||||
export type DbQueue = Bull.Queue<DbJobData<keyof DbJobMap>>;
|
||||
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||
export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>;
|
||||
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
||||
|
||||
@@ -75,6 +76,12 @@ const $db: Provider = {
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $relationship: Provider = {
|
||||
provide: 'queue:relationship',
|
||||
useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $objectStorage: Provider = {
|
||||
provide: 'queue:objectStorage',
|
||||
useFactory: (config: Config) => q(config, 'objectStorage'),
|
||||
@@ -96,6 +103,7 @@ const $webhookDeliver: Provider = {
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
],
|
||||
@@ -105,6 +113,7 @@ const $webhookDeliver: Provider = {
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
],
|
||||
|
@@ -6,9 +6,10 @@ import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { ThinUser } from '../queue/types.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import Bull from 'bull';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
@@ -21,6 +22,7 @@ export class QueueService {
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
) {}
|
||||
@@ -56,7 +58,7 @@ export class QueueService {
|
||||
activity: activity,
|
||||
signature,
|
||||
};
|
||||
|
||||
|
||||
return this.inboxQueue.add(data, {
|
||||
attempts: this.config.inboxJobMaxAttempts ?? 8,
|
||||
timeout: 5 * 60 * 1000, // 5min
|
||||
@@ -71,7 +73,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createDeleteDriveFilesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('deleteDriveFiles', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -81,7 +83,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportCustomEmojisJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportCustomEmojis', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -91,7 +93,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportNotesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportNotes', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -101,7 +103,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportFavoritesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportFavorites', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -111,7 +113,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) {
|
||||
return this.dbQueue.add('exportFollowing', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
excludeMuting,
|
||||
excludeInactive,
|
||||
}, {
|
||||
@@ -123,7 +125,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportMuteJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportMuting', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -133,7 +135,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportBlockingJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportBlocking', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -143,7 +145,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createExportUserListsJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportUserLists', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
@@ -153,7 +155,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importFollowing', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
@@ -161,10 +163,16 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel }));
|
||||
return this.dbQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importMuting', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
@@ -175,7 +183,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importBlocking', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
@@ -183,10 +191,32 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportBlockingToDbJob(user: ThinUser, targets: string[]) {
|
||||
const jobs = targets.map(rel => this.generateToDbJobData('importBlockingToDb', { user, target: rel }));
|
||||
return this.dbQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
|
||||
name: string,
|
||||
data: D,
|
||||
opts: Bull.JobOptions,
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
data,
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importUserLists', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
@@ -197,7 +227,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return this.dbQueue.add('importCustomEmojis', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
fileId: fileId,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
@@ -208,7 +238,7 @@ export class QueueService {
|
||||
@bindThis
|
||||
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
|
||||
return this.dbQueue.add('deleteAccount', {
|
||||
user: user,
|
||||
user: { id: user.id },
|
||||
soft: opts.soft,
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
@@ -216,6 +246,58 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
|
||||
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[]) {
|
||||
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createDelayedUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[], delay: number) {
|
||||
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel, { delay }));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
|
||||
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createUnblockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
|
||||
const jobs = blockings.map(rel => this.generateRelationshipJobData('unblock', rel));
|
||||
return this.relationshipQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
|
||||
name: string,
|
||||
data: RelationshipJobData,
|
||||
opts: Bull.JobOptions,
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
data: {
|
||||
from: { id: data.from.id },
|
||||
to: { id: data.to.id },
|
||||
silent: data.silent,
|
||||
requestId: data.requestId,
|
||||
},
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
...opts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createDeleteObjectStorageFileJob(key: string) {
|
||||
return this.objectStorageQueue.add('deleteFile', {
|
||||
@@ -246,7 +328,7 @@ export class QueueService {
|
||||
createdAt: Date.now(),
|
||||
eventId: uuid(),
|
||||
};
|
||||
|
||||
|
||||
return this.webhookDeliverQueue.add(data, {
|
||||
attempts: 4,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
@@ -264,7 +346,7 @@ export class QueueService {
|
||||
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
this.deliverQueue.clean(0, 'delayed');
|
||||
|
||||
|
||||
this.inboxQueue.once('cleaned', (jobs, status) => {
|
||||
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@@ -33,7 +33,7 @@ export class RemoteUserResolveService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async resolveUser(username: string, host: string | null): Promise<User> {
|
||||
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
|
||||
const usernameLower = username.toLowerCase();
|
||||
|
||||
if (host == null) {
|
||||
@@ -44,7 +44,7 @@ export class RemoteUserResolveService {
|
||||
} else {
|
||||
return u;
|
||||
}
|
||||
});
|
||||
}) as LocalUser;
|
||||
}
|
||||
|
||||
host = this.utilityService.toPuny(host);
|
||||
@@ -57,7 +57,7 @@ export class RemoteUserResolveService {
|
||||
} else {
|
||||
return u;
|
||||
}
|
||||
});
|
||||
}) as LocalUser;
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
||||
@@ -109,7 +109,7 @@ export class RemoteUserResolveService {
|
||||
if (u == null) {
|
||||
throw new Error('user not found');
|
||||
} else {
|
||||
return u;
|
||||
return u as LocalUser | RemoteUser;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
||||
@@ -13,6 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Packed } from '@/misc/json-schema';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
export type RolePolicies = {
|
||||
@@ -24,6 +25,7 @@ export type RolePolicies = {
|
||||
canSearchNotes: boolean;
|
||||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
alwaysMarkNsfw: boolean;
|
||||
pinLimit: number;
|
||||
antennaLimit: number;
|
||||
wordMuteLimit: number;
|
||||
@@ -44,6 +46,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canSearchNotes: false,
|
||||
canHideAds: false,
|
||||
driveCapacityMb: 100,
|
||||
alwaysMarkNsfw: false,
|
||||
pinLimit: 5,
|
||||
antennaLimit: 5,
|
||||
wordMuteLimit: 200,
|
||||
@@ -64,6 +67,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||
public static NotAssignedError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@@ -275,6 +281,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||
@@ -398,6 +405,25 @@ export class RoleService implements OnApplicationShutdown {
|
||||
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
|
||||
const roles = await this.getUserRoles(note.userId);
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
|
||||
for (const role of roles) {
|
||||
redisPipeline.xadd(
|
||||
`roleTimeline:${role.id}`,
|
||||
'MAXLEN', '~', '1000',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
|
||||
}
|
||||
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
|
169
packages/backend/src/core/SearchService.ts
Normal file
169
packages/backend/src/core/SearchService.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Note } from '@/models/entities/Note.js';
|
||||
import { User } from '@/models/index.js';
|
||||
import type { NotesRepository } from '@/models/index.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Index, MeiliSearch } from 'meilisearch';
|
||||
|
||||
type K = string;
|
||||
type V = string | number | boolean;
|
||||
type Q =
|
||||
{ op: '=', k: K, v: V } |
|
||||
{ op: '!=', k: K, v: V } |
|
||||
{ op: '>', k: K, v: number } |
|
||||
{ op: '<', k: K, v: number } |
|
||||
{ op: '>=', k: K, v: number } |
|
||||
{ op: '<=', k: K, v: number } |
|
||||
{ op: 'and', qs: Q[] } |
|
||||
{ op: 'or', qs: Q[] } |
|
||||
{ op: 'not', q: Q };
|
||||
|
||||
function compileValue(value: V): string {
|
||||
if (typeof value === 'string') {
|
||||
return `'${value}'`; // TODO: escape
|
||||
} else if (typeof value === 'number') {
|
||||
return value.toString();
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value.toString();
|
||||
}
|
||||
throw new Error('unrecognized value');
|
||||
}
|
||||
|
||||
function compileQuery(q: Q): string {
|
||||
switch (q.op) {
|
||||
case '=': return `(${q.k} = ${compileValue(q.v)})`;
|
||||
case '!=': return `(${q.k} != ${compileValue(q.v)})`;
|
||||
case '>': return `(${q.k} > ${compileValue(q.v)})`;
|
||||
case '<': return `(${q.k} < ${compileValue(q.v)})`;
|
||||
case '>=': return `(${q.k} >= ${compileValue(q.v)})`;
|
||||
case '<=': return `(${q.k} <= ${compileValue(q.v)})`;
|
||||
case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`;
|
||||
case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`;
|
||||
case 'not': return `(NOT ${compileQuery(q.q)})`;
|
||||
default: throw new Error('unrecognized query operator');
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
private meilisearchNoteIndex: Index | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meilisearch)
|
||||
private meilisearch: MeiliSearch | null,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
if (meilisearch) {
|
||||
this.meilisearchNoteIndex = meilisearch.index('notes');
|
||||
this.meilisearchNoteIndex.updateSettings({
|
||||
searchableAttributes: [
|
||||
'text',
|
||||
'cw',
|
||||
],
|
||||
sortableAttributes: [
|
||||
'createdAt',
|
||||
],
|
||||
filterableAttributes: [
|
||||
'createdAt',
|
||||
'userId',
|
||||
'userHost',
|
||||
'channelId',
|
||||
],
|
||||
typoTolerance: {
|
||||
enabled: false,
|
||||
},
|
||||
pagination: {
|
||||
maxTotalHits: 10000,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async indexNote(note: Note): Promise<void> {
|
||||
if (note.text == null && note.cw == null) return;
|
||||
if (!['home', 'public'].includes(note.visibility)) return;
|
||||
|
||||
if (this.meilisearch) {
|
||||
this.meilisearchNoteIndex!.addDocuments([{
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.getTime(),
|
||||
userId: note.userId,
|
||||
userHost: note.userHost,
|
||||
channelId: note.channelId,
|
||||
cw: note.cw,
|
||||
text: note.text,
|
||||
}], {
|
||||
primaryKey: 'id',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async searchNote(q: string, me: User | null, opts: {
|
||||
userId?: Note['userId'] | null;
|
||||
channelId?: Note['channelId'] | null;
|
||||
}, pagination: {
|
||||
untilId?: Note['id'];
|
||||
sinceId?: Note['id'];
|
||||
limit?: number;
|
||||
}): Promise<Note[]> {
|
||||
if (this.meilisearch) {
|
||||
const filter: Q = {
|
||||
op: 'and',
|
||||
qs: [],
|
||||
};
|
||||
if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() });
|
||||
if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
|
||||
if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
|
||||
if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
|
||||
const res = await this.meilisearchNoteIndex!.search(q, {
|
||||
sort: ['createdAt:desc'],
|
||||
matchingStrategy: 'all',
|
||||
attributesToRetrieve: ['id', 'createdAt'],
|
||||
filter: compileQuery(filter),
|
||||
limit: pagination.limit,
|
||||
});
|
||||
if (res.hits.length === 0) return [];
|
||||
return await this.notesRepository.findBy({
|
||||
id: In(res.hits.map(x => x.id)),
|
||||
});
|
||||
} else {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
|
||||
|
||||
if (opts.userId) {
|
||||
query.andWhere('note.userId = :userId', { userId: opts.userId });
|
||||
} else if (opts.channelId) {
|
||||
query.andWhere('note.channelId = :channelId', { channelId: opts.channelId });
|
||||
}
|
||||
|
||||
query
|
||||
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
|
||||
return await query.take(pagination.limit).getMany();
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,8 +13,9 @@ import { UsedUsername } from '@/models/entities/UsedUsername.js';
|
||||
import generateUserToken from '@/misc/generate-native-user-token.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import UsersChart from './chart/charts/users.js';
|
||||
import { UtilityService } from './UtilityService.js';
|
||||
import UsersChart from '@/core/chart/charts/users.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
@Injectable()
|
||||
export class SignupService {
|
||||
@@ -34,6 +35,7 @@ export class SignupService {
|
||||
private utilityService: UtilityService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private usersChart: UsersChart,
|
||||
) {
|
||||
}
|
||||
@@ -44,6 +46,7 @@ export class SignupService {
|
||||
password?: string | null;
|
||||
passwordHash?: UserProfile['password'] | null;
|
||||
host?: string | null;
|
||||
ignorePreservedUsernames?: boolean;
|
||||
}) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
let hash = passwordHash;
|
||||
@@ -76,7 +79,17 @@ export class SignupService {
|
||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||
throw new Error('USED_USERNAME');
|
||||
}
|
||||
|
||||
|
||||
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
|
||||
|
||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||
const instance = await this.metaService.fetch(true);
|
||||
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||
if (isPreserved) {
|
||||
throw new Error('USED_USERNAME');
|
||||
}
|
||||
}
|
||||
|
||||
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||
generateKeyPair('rsa', {
|
||||
modulusLength: 4096,
|
||||
@@ -112,9 +125,7 @@ export class SignupService {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: this.utilityService.toPunyNullable(host),
|
||||
token: secret,
|
||||
isRoot: (await this.usersRepository.countBy({
|
||||
host: IsNull(),
|
||||
})) === 0,
|
||||
isRoot: isTheFirstUser,
|
||||
}));
|
||||
|
||||
await transactionalEntityManager.save(new UserKeypair({
|
||||
|
@@ -24,7 +24,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@@ -54,12 +54,12 @@ export class UserBlockingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async block(blocker: User, blockee: User) {
|
||||
public async block(blocker: User, blockee: User, silent = false) {
|
||||
await Promise.all([
|
||||
this.cancelRequest(blocker, blockee),
|
||||
this.cancelRequest(blockee, blocker),
|
||||
this.userFollowingService.unfollow(blocker, blockee),
|
||||
this.userFollowingService.unfollow(blockee, blocker),
|
||||
this.cancelRequest(blocker, blockee, silent),
|
||||
this.cancelRequest(blockee, blocker, silent),
|
||||
this.userFollowingService.unfollow(blocker, blockee, silent),
|
||||
this.userFollowingService.unfollow(blockee, blocker, silent),
|
||||
this.removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
@@ -89,7 +89,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async cancelRequest(follower: User, followee: User) {
|
||||
private async cancelRequest(follower: User, followee: User, silent = false) {
|
||||
const request = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
@@ -110,7 +110,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
||||
this.userEntityService.pack(followee, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
@@ -20,7 +20,10 @@ import { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import Logger from '../logger.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
|
||||
@@ -43,7 +46,10 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -69,6 +75,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private instanceChart: InstanceChart,
|
||||
) {
|
||||
@@ -79,11 +86,11 @@ export class UserFollowingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise<void> {
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||||
]);
|
||||
]) as [LocalUser | RemoteUser, LocalUser | RemoteUser];
|
||||
|
||||
// check blocking
|
||||
const [blocking, blocked] = await Promise.all([
|
||||
@@ -133,13 +140,27 @@ export class UserFollowingService implements OnModuleInit {
|
||||
if (followed) autoAccept = true;
|
||||
}
|
||||
|
||||
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
|
||||
if (followee.isLocked && !autoAccept) {
|
||||
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
|
||||
follower,
|
||||
(oldSrc, newSrc) => this.followingsRepository.exist({
|
||||
where: {
|
||||
followeeId: followee.id,
|
||||
followerId: newSrc.id,
|
||||
},
|
||||
}),
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
if (!autoAccept) {
|
||||
await this.createFollowRequest(follower, followee, requestId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.insertFollowingDoc(followee, follower);
|
||||
await this.insertFollowingDoc(followee, follower, silent);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
@@ -155,6 +176,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
follower: {
|
||||
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']
|
||||
},
|
||||
silent = false,
|
||||
): Promise<void> {
|
||||
if (follower.id === followee.id) return;
|
||||
|
||||
@@ -205,35 +227,43 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
//#region Increment counts
|
||||
await Promise.all([
|
||||
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
||||
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
||||
const [followeeUser, followerUser] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: followee.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: follower.id }),
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
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.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
// Neither followee nor follower has moved.
|
||||
if (!followeeUser.movedToUri && !followerUser.movedToUri) {
|
||||
//#region Increment counts
|
||||
await Promise.all([
|
||||
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
||||
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
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.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.perUserFollowingChart.update(follower, followee, true);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.perUserFollowingChart.update(follower, followee, true);
|
||||
|
||||
// Publish follow event
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
@@ -278,12 +308,18 @@ export class UserFollowingService implements OnModuleInit {
|
||||
},
|
||||
silent = false,
|
||||
): Promise<void> {
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
const following = await this.followingsRepository.findOne({
|
||||
relations: {
|
||||
follower: true,
|
||||
followee: true,
|
||||
},
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (following == null) {
|
||||
if (following === null || !following.follower || !following.followee) {
|
||||
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||||
return;
|
||||
}
|
||||
@@ -292,7 +328,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||
|
||||
this.decrementFollowing(follower, followee);
|
||||
this.decrementFollowing(following.follower, following.followee);
|
||||
|
||||
// Publish unfollow event
|
||||
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
||||
@@ -311,50 +347,87 @@ export class UserFollowingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser), follower));
|
||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||||
// local user has null host
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as PartialRemoteUser, followee as PartialLocalUser), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async decrementFollowing(
|
||||
follower: { id: User['id']; host: User['host']; },
|
||||
followee: { id: User['id']; host: User['host']; },
|
||||
follower: User,
|
||||
followee: User,
|
||||
): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
//#region Decrement following / followers counts
|
||||
await Promise.all([
|
||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
||||
]);
|
||||
//#endregion
|
||||
// Neither followee nor follower has moved.
|
||||
if (!follower.movedToUri && !followee.movedToUri) {
|
||||
//#region Decrement following / followers counts
|
||||
await Promise.all([
|
||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
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.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
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.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.perUserFollowingChart.update(follower, followee, false);
|
||||
} else {
|
||||
// Adjust following/followers counts
|
||||
for (const user of [follower, followee]) {
|
||||
if (user.movedToUri) continue; // No need to update if the user has already moved.
|
||||
|
||||
const nonMovedFollowees = await this.followingsRepository.count({
|
||||
relations: {
|
||||
followee: true,
|
||||
},
|
||||
where: {
|
||||
followerId: user.id,
|
||||
followee: {
|
||||
movedToUri: IsNull(),
|
||||
}
|
||||
}
|
||||
});
|
||||
const nonMovedFollowers = await this.followingsRepository.count({
|
||||
relations: {
|
||||
follower: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
follower: {
|
||||
movedToUri: IsNull(),
|
||||
}
|
||||
}
|
||||
});
|
||||
await this.usersRepository.update(
|
||||
{ id: user.id },
|
||||
{ followingCount: nonMovedFollowees, followersCount: nonMovedFollowers },
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: adjust charts
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.perUserFollowingChart.update(follower, followee, false);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -410,7 +483,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
|
||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||
}
|
||||
}
|
||||
@@ -425,7 +498,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
},
|
||||
): Promise<void> {
|
||||
if (this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser | PartialRemoteUser, followee as PartialRemoteUser), follower));
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||
@@ -470,7 +543,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
await this.insertFollowingDoc(followee, follower);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as PartialLocalUser, request.requestId!), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
}
|
||||
|
||||
@@ -557,15 +630,22 @@ export class UserFollowingService implements OnModuleInit {
|
||||
*/
|
||||
@bindThis
|
||||
private async removeFollow(followee: Both, follower: Both): Promise<void> {
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
const following = await this.followingsRepository.findOne({
|
||||
relations: {
|
||||
followee: true,
|
||||
follower: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (!following) return;
|
||||
if (!following || !following.followee || !following.follower) return;
|
||||
|
||||
await this.followingsRepository.delete(following.id);
|
||||
this.decrementFollowing(follower, followee);
|
||||
|
||||
this.decrementFollowing(following.follower, following.followee);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { UserKeypairsRepository } from '@/models/index.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
|
@@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListService {
|
||||
@@ -29,6 +30,7 @@ export class UserListService {
|
||||
private roleService: RoleService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private proxyAccountService: ProxyAccountService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -47,14 +49,14 @@ export class UserListService {
|
||||
userId: target.id,
|
||||
userListId: list.id,
|
||||
} as UserListJoining);
|
||||
|
||||
|
||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||
|
||||
|
||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||
if (this.userEntityService.isRemoteUser(target)) {
|
||||
const proxy = await this.proxyAccountService.fetch();
|
||||
if (proxy) {
|
||||
this.userFollowingService.follow(proxy, target);
|
||||
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ export class UserSuspendService {
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
@@ -65,7 +65,7 @@ export class UserSuspendService {
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにUndo Delete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
|
@@ -43,7 +43,8 @@ export class WebfingerService {
|
||||
const m = query.match(/^([^@]+)@(.*)/);
|
||||
if (m) {
|
||||
const hostname = m[2];
|
||||
return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
|
||||
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
|
||||
return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid query (${query})`);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/index.js';
|
||||
import type { Webhook } from '@/models/entities/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@@ -8,7 +8,7 @@ import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import { getApId } from './type.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import type { IObject } from './type.js';
|
||||
@@ -101,7 +101,7 @@ export class ApDbResolverService {
|
||||
* AP Person => Misskey User in DB
|
||||
*/
|
||||
@bindThis
|
||||
public async getUserFromApId(value: string | IObject): Promise<User | null> {
|
||||
public async getUserFromApId(value: string | IObject): Promise<LocalUser | RemoteUser | null> {
|
||||
const parsed = this.parseUri(value);
|
||||
|
||||
if (parsed.local) {
|
||||
@@ -109,11 +109,11 @@ export class ApDbResolverService {
|
||||
|
||||
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
||||
id: parsed.id,
|
||||
}).then(x => x ?? undefined)) ?? null;
|
||||
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
|
||||
} else {
|
||||
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
||||
uri: parsed.uri,
|
||||
}));
|
||||
})) as RemoteUser | null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
@@ -13,13 +13,15 @@ import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
@@ -76,6 +78,8 @@ export class ApInboxService {
|
||||
private apNoteService: ApNoteService,
|
||||
private apPersonService: ApPersonService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private cacheService: CacheService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
@@ -140,7 +144,7 @@ export class ApInboxService {
|
||||
} else if (isFlag(activity)) {
|
||||
await this.flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
//await this.move(actor, activity);
|
||||
await this.move(actor, activity);
|
||||
} else {
|
||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||
}
|
||||
@@ -158,6 +162,7 @@ export class ApInboxService {
|
||||
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
|
||||
}
|
||||
|
||||
// don't queue because the sender may attempt again when timeout
|
||||
await this.userFollowingService.follow(actor, followee, activity.id);
|
||||
return 'ok';
|
||||
}
|
||||
@@ -596,6 +601,7 @@ export class ApInboxService {
|
||||
throw e;
|
||||
});
|
||||
|
||||
// don't queue because the sender may attempt again when timeout
|
||||
if (isFollow(object)) return await this.undoFollow(actor, object);
|
||||
if (isBlock(object)) return await this.undoBlock(actor, object);
|
||||
if (isLike(object)) return await this.undoLike(actor, object);
|
||||
@@ -736,53 +742,7 @@ export class ApInboxService {
|
||||
// fetch the new and old accounts
|
||||
const targetUri = getApHrefNullable(activity.target);
|
||||
if (!targetUri) return 'skip: invalid activity target';
|
||||
let new_acc = await this.apPersonService.resolvePerson(targetUri);
|
||||
let old_acc = await this.apPersonService.resolvePerson(actor.uri);
|
||||
|
||||
// update them if they're remote
|
||||
if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri);
|
||||
if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri);
|
||||
|
||||
// retrieve updated users
|
||||
new_acc = await this.apPersonService.resolvePerson(targetUri);
|
||||
old_acc = await this.apPersonService.resolvePerson(actor.uri);
|
||||
|
||||
// check if alsoKnownAs of the new account is valid
|
||||
let isValidMove = true;
|
||||
if (old_acc.uri) {
|
||||
if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
|
||||
isValidMove = false;
|
||||
}
|
||||
} else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
|
||||
isValidMove = false;
|
||||
}
|
||||
if (!isValidMove) {
|
||||
return 'skip: accounts invalid';
|
||||
}
|
||||
|
||||
// add target uri to movedToUri in order to indicate that the user has moved
|
||||
await this.usersRepository.update(old_acc.id, { movedToUri: targetUri });
|
||||
|
||||
// follow the new account and unfollow the old one
|
||||
const followings = await this.followingsRepository.find({
|
||||
relations: {
|
||||
follower: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: old_acc.id,
|
||||
followerHost: IsNull(), // follower is local
|
||||
},
|
||||
});
|
||||
for (const following of followings) {
|
||||
if (!following.follower) continue;
|
||||
try {
|
||||
await this.userFollowingService.follow(following.follower, new_acc);
|
||||
await this.userFollowingService.unfollow(following.follower, old_acc);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do';
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { PartialLocalUser, LocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js';
|
||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||
import type { Relay } from '@/models/entities/Relay.js';
|
||||
@@ -66,7 +66,7 @@ export class ApRendererService {
|
||||
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
||||
return {
|
||||
type: 'Accept',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
};
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export class ApRendererService {
|
||||
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
||||
return {
|
||||
type: 'Add',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
target,
|
||||
object,
|
||||
};
|
||||
@@ -83,7 +83,7 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
public renderAnnounce(object: any, note: Note): IAnnounce {
|
||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||
|
||||
let to: string[] = [];
|
||||
let cc: string[] = [];
|
||||
@@ -103,7 +103,7 @@ export class ApRendererService {
|
||||
|
||||
return {
|
||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||
actor: `${this.config.url}/users/${note.userId}`,
|
||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||
type: 'Announce',
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
@@ -126,7 +126,7 @@ export class ApRendererService {
|
||||
return {
|
||||
type: 'Block',
|
||||
id: `${this.config.url}/blocks/${block.id}`,
|
||||
actor: `${this.config.url}/users/${block.blockerId}`,
|
||||
actor: this.userEntityService.genLocalUserUri(block.blockerId),
|
||||
object: block.blockee.uri,
|
||||
};
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export class ApRendererService {
|
||||
public renderCreate(object: IObject, note: Note): ICreate {
|
||||
const activity = {
|
||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||
actor: `${this.config.url}/users/${note.userId}`,
|
||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||
type: 'Create',
|
||||
published: note.createdAt.toISOString(),
|
||||
object,
|
||||
@@ -151,7 +151,7 @@ export class ApRendererService {
|
||||
public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete {
|
||||
return {
|
||||
type: 'Delete',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
};
|
||||
@@ -188,7 +188,7 @@ export class ApRendererService {
|
||||
public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag {
|
||||
return {
|
||||
type: 'Flag',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
content,
|
||||
object,
|
||||
};
|
||||
@@ -199,7 +199,7 @@ export class ApRendererService {
|
||||
return {
|
||||
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
||||
type: 'Follow',
|
||||
actor: `${this.config.url}/users/${relayActor.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(relayActor.id),
|
||||
object: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
};
|
||||
}
|
||||
@@ -210,21 +210,21 @@ export class ApRendererService {
|
||||
*/
|
||||
@bindThis
|
||||
public async renderFollowUser(id: User['id']) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: id });
|
||||
return this.userEntityService.isLocalUser(user) ? `${this.config.url}/users/${user.id}` : user.uri;
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
||||
return this.userEntityService.getUserUri(user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderFollow(
|
||||
follower: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
followee: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
follower: PartialLocalUser | PartialRemoteUser,
|
||||
followee: PartialLocalUser | PartialRemoteUser,
|
||||
requestId?: string,
|
||||
): IFollow {
|
||||
return {
|
||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||
type: 'Follow',
|
||||
actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri!,
|
||||
object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri!,
|
||||
actor: this.userEntityService.getUserUri(follower)!,
|
||||
object: this.userEntityService.getUserUri(followee)!,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export class ApRendererService {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
||||
type: 'Key',
|
||||
owner: `${this.config.url}/users/${user.id}`,
|
||||
owner: this.userEntityService.genLocalUserUri(user.id),
|
||||
publicKeyPem: createPublicKey(key.publicKey).export({
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
@@ -284,21 +284,21 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderMention(mention: User): IApMention {
|
||||
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
||||
return {
|
||||
type: 'Mention',
|
||||
href: this.userEntityService.isRemoteUser(mention) ? mention.uri! : `${this.config.url}/users/${(mention as LocalUser).id}`,
|
||||
href: this.userEntityService.getUserUri(mention)!,
|
||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderMove(
|
||||
src: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
dst: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
src: PartialLocalUser | PartialRemoteUser,
|
||||
dst: PartialLocalUser | PartialRemoteUser,
|
||||
): IMove {
|
||||
const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
|
||||
const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
|
||||
const actor = this.userEntityService.getUserUri(src)!;
|
||||
const target = this.userEntityService.getUserUri(dst)!;
|
||||
return {
|
||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||
actor,
|
||||
@@ -351,7 +351,7 @@ export class ApRendererService {
|
||||
}
|
||||
}
|
||||
|
||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||
|
||||
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
||||
|
||||
@@ -376,7 +376,7 @@ export class ApRendererService {
|
||||
}) : [];
|
||||
|
||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u));
|
||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
||||
|
||||
const files = await getPromisedFiles(note.fileIds);
|
||||
|
||||
@@ -450,7 +450,7 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
public async renderPerson(user: LocalUser) {
|
||||
const id = `${this.config.url}/users/${user.id}`;
|
||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||
const isSystem = !!user.username.match(/\./);
|
||||
|
||||
const [avatar, banner, profile] = await Promise.all([
|
||||
@@ -538,7 +538,7 @@ export class ApRendererService {
|
||||
return {
|
||||
type: 'Question',
|
||||
id: `${this.config.url}/questions/${note.id}`,
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
content: note.text ?? '',
|
||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||
name: text,
|
||||
@@ -555,7 +555,7 @@ export class ApRendererService {
|
||||
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
||||
return {
|
||||
type: 'Reject',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
};
|
||||
}
|
||||
@@ -564,7 +564,7 @@ export class ApRendererService {
|
||||
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
||||
return {
|
||||
type: 'Remove',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
target,
|
||||
object,
|
||||
};
|
||||
@@ -585,7 +585,7 @@ export class ApRendererService {
|
||||
return {
|
||||
type: 'Undo',
|
||||
...(id ? { id } : {}),
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
};
|
||||
@@ -595,7 +595,7 @@ export class ApRendererService {
|
||||
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
type: 'Update',
|
||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object,
|
||||
@@ -607,14 +607,14 @@ export class ApRendererService {
|
||||
public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
type: 'Create',
|
||||
to: [pollOwner.uri],
|
||||
published: new Date().toISOString(),
|
||||
object: {
|
||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
||||
type: 'Note',
|
||||
attributedTo: `${this.config.url}/users/${user.id}`,
|
||||
attributedTo: this.userEntityService.genLocalUserUri(user.id),
|
||||
to: [pollOwner.uri],
|
||||
inReplyTo: note.uri,
|
||||
name: poll.choices[vote.choice],
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -151,7 +151,7 @@ export class Resolver {
|
||||
return Promise.all(
|
||||
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })),
|
||||
)
|
||||
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, url)));
|
||||
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as LocalUser | RemoteUser, followee as LocalUser | RemoteUser, url)));
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApImageService {
|
||||
@@ -48,8 +49,8 @@ export class ApImageService {
|
||||
throw new Error('invalid image: url not privided');
|
||||
}
|
||||
|
||||
if (!image.url.startsWith('https://')) {
|
||||
throw new Error('invalid image: unexpected shcema of url: ' + image.url);
|
||||
if (!checkHttps(image.url)) {
|
||||
throw new Error('invalid image: unexpected schema of url: ' + image.url);
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the Image: ${image.url}`);
|
||||
|
@@ -32,6 +32,7 @@ import { ApQuestionService } from './ApQuestionService.js';
|
||||
import { ApImageService } from './ApImageService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject, IPost } from '../type.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApNoteService {
|
||||
@@ -71,7 +72,7 @@ export class ApNoteService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public validateNote(object: any, uri: string) {
|
||||
public validateNote(object: IObject, uri: string) {
|
||||
const expectHost = this.utilityService.extractDbHost(uri);
|
||||
|
||||
if (object == null) {
|
||||
@@ -85,9 +86,10 @@ export class ApNoteService {
|
||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||
}
|
||||
|
||||
if (object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.attributedTo)}`);
|
||||
|
||||
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
|
||||
if (object.attributedTo && actualHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -129,13 +131,13 @@ export class ApNoteService {
|
||||
|
||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
if (note.id && !note.id.startsWith('https://')) {
|
||||
if (note.id && !checkHttps(note.id)) {
|
||||
throw new Error('unexpected shcema of note.id: ' + note.id);
|
||||
}
|
||||
|
||||
const url = getOneApHrefNullable(note.url);
|
||||
|
||||
if (url && !url.startsWith('https://')) {
|
||||
if (url && !checkHttps(url)) {
|
||||
throw new Error('unexpected shcema of note url: ' + url);
|
||||
}
|
||||
|
||||
@@ -148,7 +150,7 @@ export class ApNoteService {
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
}
|
||||
|
||||
|
||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||
let visibility = noteAudience.visibility;
|
||||
const visibleUsers = noteAudience.visibleUsers;
|
||||
|
@@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser, 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';
|
||||
@@ -42,6 +42,8 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import type { ApImageService } from './ApImageService.js';
|
||||
import type { IActor, IObject } from '../type.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
@@ -66,6 +68,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
private usersChart: UsersChart;
|
||||
private instanceChart: InstanceChart;
|
||||
private apLoggerService: ApLoggerService;
|
||||
private accountMoveService: AccountMoveService;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@@ -131,9 +134,16 @@ export class ApPersonService implements OnModuleInit {
|
||||
this.usersChart = this.moduleRef.get('UsersChart');
|
||||
this.instanceChart = this.moduleRef.get('InstanceChart');
|
||||
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
||||
this.accountMoveService = this.moduleRef.get('AccountMoveService');
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
||||
private punyHost(url: string): string {
|
||||
const urlObj = new URL(url);
|
||||
const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and convert to actor object
|
||||
* @param x Fetched object
|
||||
@@ -141,7 +151,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
*/
|
||||
@bindThis
|
||||
private validateActor(x: IObject, uri: string): IActor {
|
||||
const expectHost = this.utilityService.toPuny(new URL(uri).hostname);
|
||||
const expectHost = this.punyHost(uri);
|
||||
|
||||
if (x == null) {
|
||||
throw new Error('invalid Actor: object is null');
|
||||
@@ -182,7 +192,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
x.summary = truncate(x.summary, summaryLength);
|
||||
}
|
||||
|
||||
const idHost = this.utilityService.toPuny(new URL(x.id!).hostname);
|
||||
const idHost = this.punyHost(x.id);
|
||||
if (idHost !== expectHost) {
|
||||
throw new Error('invalid Actor: id has different host');
|
||||
}
|
||||
@@ -192,7 +202,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
throw new Error('invalid Actor: publicKey.id is not a string');
|
||||
}
|
||||
|
||||
const publicKeyIdHost = this.utilityService.toPuny(new URL(x.publicKey.id).hostname);
|
||||
const publicKeyIdHost = this.punyHost(x.publicKey.id);
|
||||
if (publicKeyIdHost !== expectHost) {
|
||||
throw new Error('invalid Actor: publicKey.id has different host');
|
||||
}
|
||||
@@ -202,27 +212,27 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Personをフェッチします。
|
||||
* uriからUser(Person)をフェッチします。
|
||||
*
|
||||
* Misskeyに対象のPersonが登録されていればそれを返します。
|
||||
* Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
|
||||
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
const cached = this.cacheService.uriPersonCache.get(uri);
|
||||
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
|
||||
if (cached) return cached;
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
if (uri.startsWith(this.config.url + '/')) {
|
||||
if (uri.startsWith(`${this.config.url}/`)) {
|
||||
const id = uri.split('/').pop();
|
||||
const u = await this.usersRepository.findOneBy({ id });
|
||||
const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
|
||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||
return u;
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
const exist = await this.usersRepository.findOneBy({ uri });
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
|
||||
|
||||
if (exist) {
|
||||
this.cacheService.uriPersonCache.set(uri, exist);
|
||||
@@ -237,7 +247,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
* Personを作成します。
|
||||
*/
|
||||
@bindThis
|
||||
public async createPerson(uri: string, resolver?: Resolver): Promise<User> {
|
||||
public async createPerson(uri: string, resolver?: Resolver): Promise<RemoteUser> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
if (uri.startsWith(this.config.url)) {
|
||||
@@ -252,7 +262,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
this.logger.info(`Creating the Person: ${person.id}`);
|
||||
|
||||
const host = this.utilityService.toPuny(new URL(object.id).hostname);
|
||||
const host = this.punyHost(object.id);
|
||||
|
||||
const { fields } = this.analyzeAttachments(person.attachment ?? []);
|
||||
|
||||
@@ -264,8 +274,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !url.startsWith('https://')) {
|
||||
throw new Error('unexpected shcema of person url: ' + url);
|
||||
if (url && !checkHttps(url)) {
|
||||
throw new Error('unexpected schema of person url: ' + url);
|
||||
}
|
||||
|
||||
// Create user
|
||||
@@ -282,6 +292,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
name: truncate(person.name, nameLength),
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
movedToUri: person.movedTo,
|
||||
movedAt: person.movedTo ? new Date() : null,
|
||||
alsoKnownAs: person.alsoKnownAs,
|
||||
isExplorable: !!person.discoverable,
|
||||
username: person.preferredUsername,
|
||||
@@ -404,23 +415,26 @@ export class ApPersonService implements OnModuleInit {
|
||||
/**
|
||||
* Personの情報を更新します。
|
||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
||||
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。
|
||||
*
|
||||
* @param uri URI of Person
|
||||
* @param resolver Resolver
|
||||
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
||||
* @param movePreventUris ここに指定されたURIがPersonのmovedToに指定されていたり10回より多く回っている場合これ以上アカウント移行を行わない(無限ループ防止)
|
||||
*/
|
||||
@bindThis
|
||||
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
|
||||
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (uri.startsWith(this.config.url + '/')) {
|
||||
if (uri.startsWith(`${this.config.url}/`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser;
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
||||
|
||||
if (exist == null) {
|
||||
if (exist === null) {
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
@@ -459,8 +473,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !url.startsWith('https://')) {
|
||||
throw new Error('unexpected shcema of person url: ' + url);
|
||||
if (url && !checkHttps(url)) {
|
||||
throw new Error('unexpected schema of person url: ' + url);
|
||||
}
|
||||
|
||||
const updates = {
|
||||
@@ -478,7 +492,16 @@ export class ApPersonService implements OnModuleInit {
|
||||
movedToUri: person.movedTo ?? null,
|
||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||
isExplorable: !!person.discoverable,
|
||||
} as Partial<User>;
|
||||
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||
|
||||
const moving =
|
||||
// 移行先がない→ある
|
||||
(!exist.movedToUri && updates.movedToUri) ||
|
||||
// 移行先がある→別のもの
|
||||
(exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
|
||||
// 移行先がある→ない、ない→ないは無視
|
||||
|
||||
if (moving) updates.movedAt = new Date();
|
||||
|
||||
if (avatar) {
|
||||
updates.avatarId = avatar.id;
|
||||
@@ -523,6 +546,31 @@ export class ApPersonService implements OnModuleInit {
|
||||
});
|
||||
|
||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
||||
|
||||
const updated = { ...exist, ...updates };
|
||||
|
||||
this.cacheService.uriPersonCache.set(uri, updated);
|
||||
|
||||
// 移行処理を行う
|
||||
if (updated.movedAt && (
|
||||
// 初めて移行する場合はmovedAtがnullなので移行処理を許可
|
||||
exist.movedAt == null ||
|
||||
// 以前のmovingから14日以上経過した場合のみ移行処理を許可
|
||||
// (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく)
|
||||
exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime()
|
||||
)) {
|
||||
this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`);
|
||||
return this.processRemoteMove(updated, movePreventUris)
|
||||
.then(result => {
|
||||
this.logger.info(`Processing Move Finished [${result}] @${updated.username}@${updated.host} (${uri})`);
|
||||
return result;
|
||||
})
|
||||
.catch(e => {
|
||||
this.logger.info(`Processing Move Failed @${updated.username}@${updated.host} (${uri})`, { stack: e });
|
||||
});
|
||||
}
|
||||
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -532,7 +580,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
|
||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
@@ -607,4 +655,53 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* リモート由来のアカウント移行処理を行います
|
||||
* @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提)
|
||||
* @param movePreventUris ここに列挙されたURIにsrc.movedToUriが含まれる場合、移行処理はしない(無限ループ防止)
|
||||
*/
|
||||
@bindThis
|
||||
private async processRemoteMove(src: RemoteUser, movePreventUris: string[] = []): Promise<string> {
|
||||
if (!src.movedToUri) return 'skip: no movedToUri';
|
||||
if (src.uri === src.movedToUri) return 'skip: movedTo itself (src)'; // ???
|
||||
if (movePreventUris.length > 10) return 'skip: too many moves';
|
||||
|
||||
// まずサーバー内で検索して様子見
|
||||
let dst = await this.fetchPerson(src.movedToUri);
|
||||
|
||||
if (dst && this.userEntityService.isLocalUser(dst)) {
|
||||
// targetがローカルユーザーだった場合データベースから引っ張ってくる
|
||||
dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as LocalUser;
|
||||
} else if (dst) {
|
||||
if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
|
||||
|
||||
// targetを見つけたことがあるならtargetをupdatePersonする
|
||||
await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]);
|
||||
dst = await this.fetchPerson(src.movedToUri) ?? dst;
|
||||
} else {
|
||||
if (src.movedToUri.startsWith(`${this.config.url}/`)) {
|
||||
// ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている
|
||||
return 'failed: movedTo is local but not found';
|
||||
}
|
||||
|
||||
// targetが知らない人だったらresolvePerson
|
||||
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
|
||||
dst = await this.resolvePerson(src.movedToUri);
|
||||
}
|
||||
|
||||
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
|
||||
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
|
||||
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
|
||||
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
||||
return 'skip: dst.alsoKnownAs is empty';
|
||||
}
|
||||
if (!dst.alsoKnownAs?.includes(src.uri)) {
|
||||
return 'skip: alsoKnownAs does not include from.uri';
|
||||
}
|
||||
|
||||
await this.accountMoveService.postMoveProcess(src, dst);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
@@ -74,6 +74,7 @@ export class ChannelEntityService {
|
||||
userId: channel.userId,
|
||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||
pinnedNoteIds: channel.pinnedNoteIds,
|
||||
color: channel.color,
|
||||
usersCount: channel.usersCount,
|
||||
notesCount: channel.notesCount,
|
||||
|
||||
@@ -84,7 +85,7 @@ export class ChannelEntityService {
|
||||
} : {}),
|
||||
|
||||
...(detailed ? {
|
||||
pinnedNotes: await this.noteEntityService.packMany(pinnedNotes, me),
|
||||
pinnedNotes: (await this.noteEntityService.packMany(pinnedNotes, me)).sort((a, b) => channel.pinnedNoteIds.indexOf(a.id) - channel.pinnedNoteIds.indexOf(b.id)),
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
|
@@ -335,6 +335,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
channel: channel ? {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
color: channel.color,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri ?? undefined,
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
|
||||
import type { AccessTokensRepository, FollowRequestsRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Notification } from '@/models/entities/Notification.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
@@ -35,6 +35,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
@Inject(DI.noteReactionsRepository)
|
||||
private noteReactionsRepository: NoteReactionsRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@Inject(DI.accessTokensRepository)
|
||||
private accessTokensRepository: AccessTokensRepository,
|
||||
|
||||
@@ -131,6 +134,15 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
});
|
||||
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
||||
|
||||
// 既に解決されたフォローリクエストの通知を除外
|
||||
const followRequestNotifications = validNotifications.filter(x => x.type === 'receiveFollowRequest');
|
||||
if (followRequestNotifications.length > 0) {
|
||||
const reqs = await this.followRequestsRepository.find({
|
||||
where: { followerId: In(followRequestNotifications.map(x => x.notifierId!)) },
|
||||
});
|
||||
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
||||
}
|
||||
|
||||
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
|
||||
packedNotes,
|
||||
packedUsers,
|
||||
|
@@ -59,6 +59,7 @@ export class RoleEntityService {
|
||||
isPublic: role.isPublic,
|
||||
isAdministrator: role.isAdministrator,
|
||||
isModerator: role.isModerator,
|
||||
isExplorable: role.isExplorable,
|
||||
asBadge: role.asBadge,
|
||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||
displayOrder: role.displayOrder,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In, Not } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import Ajv from 'ajv';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -9,10 +9,9 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
@@ -35,13 +34,13 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
|
||||
const ajv = new Ajv();
|
||||
|
||||
function isLocalUser(user: User): user is LocalUser;
|
||||
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
||||
function isLocalUser<T extends { host: User['host'] }>(user: T): user is (T & { host: null; });
|
||||
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
||||
return user.host == null;
|
||||
}
|
||||
|
||||
function isRemoteUser(user: User): user is RemoteUser;
|
||||
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
||||
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is (T & { host: string; });
|
||||
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||
return !isLocalUser(user);
|
||||
}
|
||||
@@ -113,6 +112,9 @@ export class UserEntityService implements OnModuleInit {
|
||||
|
||||
@Inject(DI.pagesRepository)
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
@Inject(DI.userMemosRepository)
|
||||
private userMemosRepository: UserMemoRepository,
|
||||
|
||||
//private noteEntityService: NoteEntityService,
|
||||
//private driveFileEntityService: DriveFileEntityService,
|
||||
@@ -277,6 +279,17 @@ export class UserEntityService implements OnModuleInit {
|
||||
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getUserUri(user: LocalUser | PartialLocalUser | RemoteUser | PartialRemoteUser): string {
|
||||
return this.isRemoteUser(user)
|
||||
? user.uri : this.genLocalUserUri(user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public genLocalUserUri(userId: string): string {
|
||||
return `${this.config.url}/users/${userId}`;
|
||||
}
|
||||
|
||||
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
|
||||
src: User['id'] | User,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
@@ -366,8 +379,11 @@ export class UserEntityService implements OnModuleInit {
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
uri: user.uri,
|
||||
movedToUri: user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null,
|
||||
alsoKnownAs: user.alsoKnownAs,
|
||||
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
||||
alsoKnownAs: user.alsoKnownAs
|
||||
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
|
||||
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[])
|
||||
: null,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||
@@ -409,6 +425,10 @@ export class UserEntityService implements OnModuleInit {
|
||||
isAdministrator: role.isAdministrator,
|
||||
displayOrder: role.displayOrder,
|
||||
}))),
|
||||
memo: meId == null ? null : await this.userMemosRepository.findOneBy({
|
||||
userId: meId,
|
||||
targetUserId: user.id,
|
||||
}).then(row => row?.memo ?? null),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail && isMe ? {
|
||||
|
@@ -40,7 +40,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||
const stats = {
|
||||
cpu: roundCpu(cpu),
|
||||
mem: {
|
||||
used: round(memStats.used - memStats.buffers - memStats.cached),
|
||||
used: round(memStats.total - memStats.available),
|
||||
active: round(memStats.active),
|
||||
},
|
||||
net: {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
export const DI = {
|
||||
config: Symbol('config'),
|
||||
db: Symbol('db'),
|
||||
meilisearch: Symbol('meilisearch'),
|
||||
redis: Symbol('redis'),
|
||||
redisForPub: Symbol('redisForPub'),
|
||||
redisForSub: Symbol('redisForSub'),
|
||||
@@ -70,5 +71,6 @@ export const DI = {
|
||||
roleAssignmentsRepository: Symbol('roleAssignmentsRepository'),
|
||||
flashsRepository: Symbol('flashsRepository'),
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Redis from 'ioredis';
|
||||
import * as Redis from 'ioredis';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
export class RedisKVCache<T> {
|
||||
@@ -8,7 +8,7 @@ export class RedisKVCache<T> {
|
||||
private memoryCache: MemoryKVCache<T>;
|
||||
private fetcher: (key: string) => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T;
|
||||
private fromRedisConverter: (value: string) => T | undefined;
|
||||
|
||||
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
|
||||
lifetime: RedisKVCache<T>['lifetime'];
|
||||
@@ -38,7 +38,7 @@ export class RedisKVCache<T> {
|
||||
await this.redisClient.set(
|
||||
`kvcache:${this.name}:${key}`,
|
||||
this.toRedisConverter(value),
|
||||
'ex', Math.round(this.lifetime / 1000),
|
||||
'EX', Math.round(this.lifetime / 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export class RedisSingleCache<T> {
|
||||
private memoryCache: MemorySingleCache<T>;
|
||||
private fetcher: () => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T;
|
||||
private fromRedisConverter: (value: string) => T | undefined;
|
||||
|
||||
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
|
||||
lifetime: RedisSingleCache<T>['lifetime'];
|
||||
@@ -122,7 +122,7 @@ export class RedisSingleCache<T> {
|
||||
await this.redisClient.set(
|
||||
`singlecache:${this.name}`,
|
||||
this.toRedisConverter(value),
|
||||
'ex', Math.round(this.lifetime / 1000),
|
||||
'EX', Math.round(this.lifetime / 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
4
packages/backend/src/misc/check-https.ts
Normal file
4
packages/backend/src/misc/check-https.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function checkHttps(url: string) {
|
||||
return url.startsWith('https://') ||
|
||||
(url.startsWith('http://') && process.env.NODE_ENV !== 'production');
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
|
||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
@@ -388,6 +388,12 @@ const $roleAssignmentsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userMemosRepository: Provider = {
|
||||
provide: DI.userMemosRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(UserMemo),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
@@ -456,6 +462,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||
$roleAssignmentsRepository,
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -522,6 +529,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||
$roleAssignmentsRepository,
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
@@ -64,6 +64,12 @@ export class Channel {
|
||||
})
|
||||
public pinnedNoteIds: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 16,
|
||||
default: '#86b300',
|
||||
})
|
||||
public color: string;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
|
@@ -405,4 +405,16 @@ export class Meta {
|
||||
default: { },
|
||||
})
|
||||
public policies: Record<string, any>;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 280,
|
||||
array: true,
|
||||
default: '{}',
|
||||
})
|
||||
public serverRules: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
||||
})
|
||||
public preservedUsernames: string[];
|
||||
}
|
||||
|
@@ -151,6 +151,11 @@ export class Role {
|
||||
})
|
||||
public isAdministrator: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isExplorable: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
@@ -75,6 +75,12 @@ export class User {
|
||||
})
|
||||
public movedToUri: string | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'When the user moved to another account',
|
||||
})
|
||||
public movedAt: Date | null;
|
||||
|
||||
@Column('simple-array', {
|
||||
nullable: true,
|
||||
comment: 'URIs the user is known as too',
|
||||
@@ -253,11 +259,23 @@ export type LocalUser = User & {
|
||||
uri: null;
|
||||
}
|
||||
|
||||
export type PartialLocalUser = Partial<User> & {
|
||||
id: User['id'];
|
||||
host: null;
|
||||
uri: null;
|
||||
}
|
||||
|
||||
export type RemoteUser = User & {
|
||||
host: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export type PartialRemoteUser = Partial<User> & {
|
||||
id: User['id'];
|
||||
host: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
|
42
packages/backend/src/models/entities/UserMemo.ts
Normal file
42
packages/backend/src/models/entities/UserMemo.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './User.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'targetUserId'], { unique: true })
|
||||
export class UserMemo {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The ID of author.',
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The ID of target user.',
|
||||
})
|
||||
public targetUserId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public targetUser: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 2048,
|
||||
comment: 'Memo.',
|
||||
})
|
||||
public memo: string;
|
||||
}
|
@@ -55,6 +55,7 @@ import { UserPending } from '@/models/entities/UserPending.js';
|
||||
import { UserProfile } from '@/models/entities/UserProfile.js';
|
||||
import { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||
import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
|
||||
import { UserMemo } from '@/models/entities/UserMemo.js';
|
||||
import { Webhook } from '@/models/entities/Webhook.js';
|
||||
import { Channel } from '@/models/entities/Channel.js';
|
||||
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
|
||||
@@ -129,6 +130,7 @@ export {
|
||||
RoleAssignment,
|
||||
Flash,
|
||||
FlashLike,
|
||||
UserMemo,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
|
||||
@@ -195,3 +197,4 @@ export type RolesRepository = Repository<Role>;
|
||||
export type RoleAssignmentsRepository = Repository<RoleAssignment>;
|
||||
export type FlashsRepository = Repository<Flash>;
|
||||
export type FlashLikesRepository = Repository<FlashLike>;
|
||||
export type UserMemoRepository = Repository<UserMemo>;
|
||||
|
@@ -59,5 +59,9 @@ export const packedChannelSchema = {
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@@ -80,9 +80,14 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
},
|
||||
alsoKnownAs: {
|
||||
type: 'array',
|
||||
format: 'uri',
|
||||
nullable: true,
|
||||
optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false,
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
@@ -143,6 +148,7 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
fields: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
maxItems: 16,
|
||||
items: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
@@ -156,7 +162,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
maxLength: 4,
|
||||
},
|
||||
},
|
||||
followersCount: {
|
||||
@@ -250,6 +255,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
memo: {
|
||||
type: 'string',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
//#endregion
|
||||
},
|
||||
} as const;
|
||||
|
@@ -70,6 +70,7 @@ import { Role } from '@/models/entities/Role.js';
|
||||
import { RoleAssignment } from '@/models/entities/RoleAssignment.js';
|
||||
import { Flash } from '@/models/entities/Flash.js';
|
||||
import { FlashLike } from '@/models/entities/FlashLike.js';
|
||||
import { UserMemo } from '@/models/entities/UserMemo.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
@@ -183,6 +184,7 @@ export const entities = [
|
||||
RoleAssignment,
|
||||
Flash,
|
||||
FlashLike,
|
||||
UserMemo,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
@@ -52,8 +52,10 @@ export class DbQueueProcessorsService {
|
||||
q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
|
||||
q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
|
||||
q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
|
||||
q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
|
||||
q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
|
||||
q.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
|
||||
q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
|
||||
q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
|
||||
q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
|
||||
q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
|
||||
|
@@ -4,6 +4,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
|
||||
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
|
||||
import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
@@ -32,6 +33,7 @@ import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessor
|
||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -61,9 +63,11 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
|
||||
DeleteAccountProcessorService,
|
||||
DeleteFileProcessorService,
|
||||
CleanRemoteFilesProcessorService,
|
||||
RelationshipProcessorService,
|
||||
SystemQueueProcessorsService,
|
||||
ObjectStorageQueueProcessorsService,
|
||||
DbQueueProcessorsService,
|
||||
RelationshipQueueProcessorsService,
|
||||
WebhookDeliverProcessorService,
|
||||
EndedPollNotificationProcessorService,
|
||||
DeliverProcessorService,
|
||||
|
@@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueProcessorService {
|
||||
@@ -27,6 +28,7 @@ export class QueueProcessorService {
|
||||
private systemQueueProcessorsService: SystemQueueProcessorsService,
|
||||
private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService,
|
||||
private dbQueueProcessorsService: DbQueueProcessorsService,
|
||||
private relationshipQueueProcessorsService: RelationshipQueueProcessorsService,
|
||||
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
private deliverProcessorService: DeliverProcessorService,
|
||||
@@ -52,14 +54,15 @@ export class QueueProcessorService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const systemLogger = this.logger.createSubLogger('system');
|
||||
const deliverLogger = this.logger.createSubLogger('deliver');
|
||||
const webhookLogger = this.logger.createSubLogger('webhook');
|
||||
const inboxLogger = this.logger.createSubLogger('inbox');
|
||||
const dbLogger = this.logger.createSubLogger('db');
|
||||
const relationshipLogger = this.logger.createSubLogger('relationship');
|
||||
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
||||
|
||||
|
||||
this.queueService.systemQueue
|
||||
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
||||
@@ -67,7 +70,7 @@ export class QueueProcessorService {
|
||||
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
|
||||
this.queueService.deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
@@ -75,7 +78,7 @@ export class QueueProcessorService {
|
||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
|
||||
this.queueService.inboxQueue
|
||||
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
||||
@@ -83,7 +86,7 @@ export class QueueProcessorService {
|
||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
|
||||
|
||||
|
||||
this.queueService.dbQueue
|
||||
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
|
||||
@@ -91,7 +94,15 @@ export class QueueProcessorService {
|
||||
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
|
||||
this.queueService.relationshipQueue
|
||||
.on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
this.queueService.objectStorageQueue
|
||||
.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
||||
@@ -99,7 +110,7 @@ export class QueueProcessorService {
|
||||
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
|
||||
this.queueService.webhookDeliverQueue
|
||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
@@ -107,26 +118,27 @@ export class QueueProcessorService {
|
||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
|
||||
this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
|
||||
this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
|
||||
this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
|
||||
this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
|
||||
this.dbQueueProcessorsService.start(this.queueService.dbQueue);
|
||||
this.relationshipQueueProcessorsService.start(this.queueService.relationshipQueue);
|
||||
this.objectStorageQueueProcessorsService.start(this.queueService.objectStorageQueue);
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
repeat: { cron: '55 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
@@ -138,19 +150,19 @@ export class QueueProcessorService {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('clean', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.queueService.systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { cron: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
|
||||
this.systemQueueProcessorsService.start(this.queueService.systemQueue);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user