Compare commits

...

277 Commits

Author SHA1 Message Date
tamaina
4c33e48f75 Merge branch 'develop' into ed25519 2024-07-18 01:13:49 +09:00
tamaina
61d904e8f1 [skip ci] add CHANGELOG 2024-07-18 01:08:14 +09:00
tamaina
8c76c7b8b5 TODO 2024-07-18 00:43:59 +09:00
tamaina
aed28060e7 getApId, InboxProcessorService 2024-07-18 00:43:51 +09:00
woxtu
6942a920c8 refactor(frontend): Improve typing (#14240)
* Improve typing

* Remove redundant promise

* Refactor

* Update packages/frontend/src/scripts/mfm-function-picker.ts

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* Update packages/frontend/src/scripts/mfm-function-picker.ts

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-07-18 00:31:52 +09:00
tamaina
29d9bbf05b modify attachLdSignature 2024-07-18 00:29:15 +09:00
tamaina
c00b61e90b modify comment 2024-07-18 00:24:47 +09:00
tamaina
99113d59f4 refreshAndfindKey → refreshAndFindKey 2024-07-18 00:12:21 +09:00
tamaina
cd19ad694c fix correct → collect 2024-07-18 00:09:20 +09:00
tamaina
95918607f4 modify comment 2024-07-18 00:04:35 +09:00
tamaina
72cda5ca80 modify comment 2024-07-18 00:02:36 +09:00
tamaina
e602f2efda move delete host 2024-07-17 23:55:39 +09:00
tamaina
de677a5b1f // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! 2024-07-17 23:28:37 +09:00
tamaina
1f0e7a40b6 fix 2024-07-17 23:26:57 +09:00
tamaina
b7349e5771 delete request.headers['host']; 2024-07-17 23:03:30 +09:00
tamaina
fe77f216c3 inboxJobPerSec: 64 2024-07-17 22:49:07 +09:00
Chocolate Pie
68bcd91d57 chore: Use clipboard API directly (#14227)
* chore: Use clipboard API directly

* fix: Fix lint
2024-07-17 21:52:05 +09:00
かっこかり
8b4933cc48 fix changelog (wrong category) 2024-07-17 19:08:39 +09:00
tamaina
ffd12d0539 deliverJobConcurrency: 16, deliverJobPerSec: 1024, inboxJobConcurrency: 4 2024-07-17 18:36:04 +09:00
tamaina
bda1de8a67 use pnpm@9.5.0 2024-07-17 18:19:06 +09:00
tamaina
a0c93bbd4d Merge branch 'develop' into ed25519 2024-07-17 18:15:50 +09:00
tamaina
5afc659afa set result to keypairEntityCache 2024-07-17 18:15:17 +09:00
tamaina
41883c451d better comment 2024-07-17 18:01:03 +09:00
tamaina
09b2e71e62 better error handling of this.userPublickeysRepository.delete 2024-07-17 17:57:45 +09:00
tamaina
44f0064301 e2e/timelines.tsで個々のテストに対するtimeoutを削除, maxConcurrency: 32 2024-07-17 17:33:27 +09:00
github-actions[bot]
f0d738d8bf Bump version to 2024.7.0-beta.0 2024-07-17 08:15:21 +00:00
tamaina
38a5e09a36 Revert "maxWorkers: '90%'"
This reverts commit 9e0a93f110.
2024-07-17 17:10:10 +09:00
tamaina
9e0a93f110 maxWorkers: '90%' 2024-07-17 17:08:08 +09:00
tamaina
d3280fe7b3 Revert "genRSAAndEd25519KeyPairではキーを直列に生成する?"
This reverts commit d0aada55c1.
2024-07-17 16:59:01 +09:00
tamaina
c2d084bac4 maxConcurrency: 8 2024-07-17 16:58:55 +09:00
FineArchs
070f0e723d AiScriptを0.19.0にアップデート (#14226)
* Update autogen files

* Update CHANGELOG.md

* Update flash-edit.vue
2024-07-17 16:55:17 +09:00
tamaina
a80a7f6458 maxConcurrency: 16 2024-07-17 16:53:34 +09:00
tamaina
613c1273b8 maxConcurrency: 8 2024-07-17 16:30:54 +09:00
tamaina
d0aada55c1 genRSAAndEd25519KeyPairではキーを直列に生成する? 2024-07-17 16:20:39 +09:00
tamaina
57bfffedae Revert "defaultMaxListeners = 512"
This reverts commit f2c412c180.
2024-07-17 16:17:09 +09:00
tamaina
f2c412c180 defaultMaxListeners = 512 2024-07-17 16:14:24 +09:00
tamaina
7e2c3e4439 remove gen-key-pair.ts 2024-07-17 16:03:44 +09:00
tamaina
c80b16cdf8 refreshAndprepareEd25519KeyPair → refreshAndPrepareEd25519KeyPair 2024-07-17 15:29:13 +09:00
tamaina
3777779aa9 Merge branch 'develop' into ed25519 2024-07-17 15:10:45 +09:00
woxtu
8ebc3b51f7 Fix typo (#14231) 2024-07-16 23:27:05 +09:00
Eiichi Yoshikawa
3b075c9c44 fix(frontend): MkSignin.vueのcredentialRequestからReactivityを削除 (#14223)
* Remove reactivity from credentialRequest in MkSignin.vue

* Update Changelog
2024-07-16 08:38:42 +09:00
おさむのひと
1001277d43 fix: CHANGELOG.mdの記載に漏れがあったのを修正 (#14220) 2024-07-15 22:08:02 +09:00
Kisaragi
ce39c3a2fb chore(backend): registed -> registered (#14213)
* chore(backend): registed -> registered

* Update CHANGELOG.md
2024-07-15 14:58:48 +09:00
Souma
1b84760c19 enhance(backend): Load settings via environment variables (#14179)
* feat(backend): Load settings via environment variables

If they're not loaded from the config file.

* chore(docker): Add hints for environment variables

It supports users to know about them.

* docs(changelog): Add the description about this change

Users can notice what's changed by this PR.

* style(backend): Fix code syntax

To pass the linter.
2024-07-14 21:33:22 +09:00
easrng
16795f18a7 Enhance(frontend): Allow negative delay in MFM (#14200)
Co-authored-by: easrng <me@easrng.net>
2024-07-14 21:31:30 +09:00
Kisaragi
f0b9d70720 ci: cache eslint (#14204)
* ci: cache eslint

* dummy commit to trigger

* fix syntax error
2024-07-14 21:30:57 +09:00
dependabot[bot]
aa0632727f chore(deps): bump actions/setup-node from 4.0.2 to 4.0.3 (#14165)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.2...v4.0.3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-14 21:29:57 +09:00
anatawa12
d47fd4ffe1 fix: error with trying to handle SIGKILL (#14208) 2024-07-14 21:29:35 +09:00
Sayamame-beans
4b9c60ad21 fix(backend): ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正 (#14100)
* fix: mute/block was not considered on users/reactions

* docs(changelog): update changelog

* chore: Apply suggestion from code review

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>

---------

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2024-07-14 20:24:29 +09:00
syuilo
c5607d8633 enhance(backend): configにsignToActivityPubGetの指定が無い場合trueと見做すように
trueの方が望ましいため
2024-07-14 20:14:43 +09:00
かっこかり
722acf5986 fix(frontend): follow-up of #13089 (#14206)
* fix(frontend): #13089 を修正

* fix

* 正規表現を強化

* fix
2024-07-14 17:28:34 +09:00
Kisaragi
09d30fef5b ci: ワークフローが更新されたときにもワークフローが起動するようにする (#14207)
* ci: include themselves in `on.push.paths`
command: find .github/workflows -type f \( -name '*.yaml' -or -name '*.yml' \) | xargs -I {} yq_4.44.2-linux_x86-64 'select(.on.push.paths != null) | .[0] | map("{}")[0]' {} | xargs -I {} ~/.local/bin/yq_4.44.2-linux_x86-64 -i '.on.push.paths += ["{}"]' {}

* ci: include themselves in `on.pull_request.paths`
command: find .github/workflows -type f \( -name '*.yaml' -or -name '*.yml' \) | xargs -I {} yq_4.44.2-linux_x86-64 'select(.on.pull_request.paths != null) | .[0] | map("{}")[0]' {} | xargs -I {} ~/.local/bin/yq_4.44.2-linux_x86-64 -i '.on.pull_request.paths += ["{}"]' {}
2024-07-14 17:27:27 +09:00
かっこかり
b9f3fccfac fix(frontend): Nested RouteのときにRouterViewに当たるキーがルートのpathとぶち当たる可能性があるのを修正 (#14202)
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-07-14 16:21:59 +09:00
Kisaragi
76181385d2 refactor(misskey-js): enable exactOptionalPropertyTypes (#14203)
* refactor(misskey-js): enable exactOptionalPropertyTypes

* refactor(misskey-js): fix error where is appeared by enabling
2024-07-14 15:52:43 +09:00
かっこかり
3c032dd5b9 enhance: 非ログイン時には別サーバーに遷移できるように (#13089)
* enhance: 非ログイン時にはMisskey Hub経由で別サーバーに遷移できるように

* fix

* サーバーサイド照会を削除

* クライアント側の照会動作

* hubを経由せずにリモートで続行できるように

* fix と pleaseLogin誘導箇所の追加

* fix

* fix

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-07-14 15:27:52 +09:00
かっこかり
6dd6fcf88f enhance(frontend): サーバー情報・お問い合わせページを改修 (#14198)
* improve(frontend): サーバー情報・お問い合わせページを改修 (#238)

* Revert "Revert "enhance(frontend): add contact page" (#208)" (This reverts commit 5a329a09c987b3249f97f9d53af67d1bffb09eea.)

* improve(frontend): サーバー情報・お問い合わせページを改修

(cherry picked from commit e72758d8cda3db009c5d1bf1f4141682931b91f8)

* fix

* Update Changelog

* tweak

* lint

* 既存の翻訳を使用するように

---------

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
2024-07-14 14:49:50 +09:00
Kisaragi
31e82fc29a test(backend): kill many any in backend test (partial) (#14054)
* kill any on utils:api

* kill any on timeline test

* use optional chain to kill TS2532 on timeline test
変更前: 該当ノートが見つからなければundefinedに対するプロパティアクセスとしてテストがクラッシュ
変更後: 該当ノートが見つからなければoptional chainがundefinedとして評価されるが、strictEqualの右辺がnon-nullableなためアサーションに失敗しテストがクラッシュ

* kill `as any` for ApMfmService

* kill argument any for api-visibility

* kill argument any across a few tests

* do not return value that has yielded from `await`-ing `Promise<void>`

* force cast

* runtime non-null assertion to coerce

* rewrite `assert.notEqual(expr, null)` to `assert.ok(expr)`
こうすることでassertion type扱いになり、non-nullableになる

* change return type of `failedApiCall` to `void`
戻り値がどこにも使われていない

* split bindings for exports.ts
型が合わなくて文句を言ってくるので適切に分割

* runtime non-null assertion

* runtime non-null assertion

* 何故かうまく行かないので、とりあえずXORしてみる

* Revert "何故かうまく行かないので、とりあえずXORしてみる"

This reverts commit 48cf32c930.

* castAsErrorで安全ではないキャストを隠蔽

* 型アサーションの追加

* 型アサーションの追加

* 型アサーションの追加

* voidで値を返さない

* castAsError

* assert.ok => kill nullability

* もはや明示的な型の指定は必要ない

* castAsError

* castAsError

* 型アサーションの追加

* nullableを一旦抑止

* 変数を分離して型エラーを排除

* 不要なプロパティを削除する処理を隠蔽してanyを排除

* Repository type

* simple type

* assert.ok => kill nullability

* revert `as any` drop
reverts fe95c05b3f partialy

* test: fix invalid assertion
partially revert b99b7b5392

* test: 52d8a54fc7 により型が合うようになった部分の`as any`を除去

* format

* test: apply https://github.com/misskey-dev/misskey/pull/14054#discussion_r1672369526 (part 1)

* test: use non-null assertion to suppress too many error

* Update packages/backend/test/utils.ts

Co-authored-by: anatawa12 <anatawa12@icloud.com>

---------

Co-authored-by: anatawa12 <anatawa12@icloud.com>
2024-07-14 09:33:16 +09:00
Sayamame-beans
7afa593d11 Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に (#14078)
* feat: implement role policy "canUpdateBioMedia"

* docs(changelog): update changelog

* docs(changelog): update changelog

* chore: regenerate misskey-js type definitions

* chore: Apply suggestion from code review

Co-authored-by: anatawa12 <anatawa12@icloud.com>

* chore: fix unnecessarily strict inequality check

* chore: policies should be gotten only once

---------

Co-authored-by: anatawa12 <anatawa12@icloud.com>
2024-07-14 09:31:05 +09:00
かっこかり
58c596cacf fix(backend): 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 (#14195)
* enhance(backend): 公開バッジのみをpackするように (MisskeyIO#652)

(cherry picked from commit b8a90659f35fef49d1d00fb2f9b152226c97643c)

* Update Changelog

* fix

* Update UserEntityService.ts

---------

Co-authored-by: CyberRex <26585194+CyberRex0@users.noreply.github.com>
2024-07-14 09:26:25 +09:00
かっこかり
b5fd6183d2 deps(frontend): AiScript VSCodeのバージョンを上げる (#14199) 2024-07-14 08:00:27 +09:00
Gianni Ceccarelli
c83c831c53 parse notRespondingSince from redis instance cache (#14079)
if we don't do this, we'll get a string, and `DeliverProcessorService`
will error out `i.notRespondingSince.getTime is not a function`
2024-07-13 20:26:48 +09:00
Kisaragi
9fcae7d9b2 refactor(sw): enable noImplicitAny (#14191) 2024-07-13 16:59:08 +09:00
かっこかり
bcc92d546f fix(frontend): ホットキーのレートリミットがallowRepeatを考慮しない問題を修正 (#14192) 2024-07-13 16:15:25 +09:00
かっこかり
1b175ea759 fix(frontend): すでにfocus trap対象の要素にinertがかかっている場合は解除するように (#14189)
* fix(frontend): すでにfocus trap対象の要素にinertがかかっている場合は解除するように

* 他のfocus-trapped要素とのインタラクションがある場合の動作を変更

* typo
2024-07-13 13:02:27 +09:00
syuilo
91de35ecdf fix(backend): デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
Fix #13955
2024-07-13 10:30:28 +09:00
おさむのひと
6cd15275bb fix: サジェストされるユーザのリストアップ方法を見直し (#14180)
* fix: サジェストされるユーザのリストアップ方法を見直し

* fix comment

* fix CHANGELOG.md

* ノートの無いユーザ(updatedAtが無いユーザ)は含めないらしい

* fix test
2024-07-12 21:14:09 +09:00
かっこかり
76b1c74a37 fix(frontend): use proper import path 2024-07-12 16:39:09 +09:00
かっこかり
385969e9f5 fix(frontend): フォーカスの挙動を修正 (#14158)
* fix(frontend): 直前のパターンを記録するように

* fix(frontend): フォーカス/タブ移動に関する挙動を調整 (#226)

Cherry-pick commit e8c030673326871edf3623cf2b8675d68f9e1b13

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>

* focusのデザイン修正

* move scripts

* Modalにfocus trapを追加

* 記録するホットキーはレートリミット式にする

* escキーのハンドリングをMkModalに統一

* fix

* enterで子メニューを開けるように

* lint

* fix focus trap

* improve switch accessibility

* 一部のmodalのフォーカストラップが外れない問題を修正

* fix

* fix

* Revert "記録するホットキーはレートリミット式にする"

This reverts commit 40a7509286.

* Revert "fix(frontend): 直前のパターンを記録するように"

This reverts commit 5372b25940.

* Revert "Revert "fix(frontend): 直前のパターンを記録するように""

This reverts commit a9bb52e799.

* Revert "Revert "記録するホットキーはレートリミット式にする""

This reverts commit bdac34273e.

* 試験的にCypressでのFocustrapを無効化

* fix

* fix focus-trap

* Update Changelog

* ✌️

* fix focustrap invocation logic

* スクロールがsticky headerを考慮するように

* 🎨

* スタイルの微調整

* 🎨

* remove deprecated key aliases

* focusElementが足りなかったので修正

* preview系にfocus時スタイルが足りなかったので修正

* `returnFocusElement` -> `returnFocusTo`

* lint

* Update packages/frontend/src/components/MkModalWindow.vue

* Apply suggestions from code review

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>

* keydownイベントをまとめる

* use correct pesudo-element selector

* fix

* rename

---------

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-07-12 16:25:44 +09:00
かっこかり
121af778a0 enhance(frontend): 未使用のサウンド設定を削除 (#14116)
* enhance(frontend): 未使用のサウンド設定を削除

* Update Changelog

* Update CHANGELOG.md
2024-07-11 18:44:18 +09:00
かっこかり
6b876da44a enhance(frontend): ウェルカムタイムラインのデザインを調整 (#14156)
* enhance(frontend): 非ログイン時のハイライトTLのデザイン調整

* Update Changelog

* fix cw handling

* ホバーしてたらスクロールを止めるように

* fix

* lint
2024-07-11 18:41:04 +09:00
tamaina
f8ac3fe343 リリースPRがないときにrelease-edit-with-push.ymlがfailして見栄えが悪いのを修正 (#14160) 2024-07-11 16:39:06 +09:00
woxtu
679318541a Improve background color specification (#14176) 2024-07-11 16:29:18 +09:00
Kisaragi
52d8a54fc7 feat(misskey-js): POST admin/roles/createの型を具象化 (#14167)
* feat(misskey-js): `POST admin/roles/create`の型を具象化

* fix

* docs: CHANGELOG.md

* test(misskey-js): admin/roles/createの型が合うことを表明

* test(misskey-js): single quote

* test(misskey-js): 無を読もうとして爆発するのを修正

* test(misskey-js): fix comment
2024-07-10 20:40:04 +09:00
かっこかり
02e0a86b12 fix(frontend): remove unused statement
fix #14162
2024-07-10 01:00:40 +09:00
かっこかり
600f16d625 fix(backend): api-docをScalarに変更 (#14152)
* fix(backend): api-docをScalarに変更

* Update Changelog
2024-07-09 21:57:19 +09:00
かっこかり
a5407131d4 fix/refactor(frontend): hotkeyの改修 (#14157)
* improve(frontend): hotkeyの改修 (#234)

(cherry picked from commit 678be147f4db709dadf25d007cc2e679e98a370e)

* Change path, add missing script

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>

* fix

* fix

* add missing keycodes

* fix

* update changelog

---------

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
2024-07-09 17:59:15 +09:00
tamaina
b61f270eae Bump release actions to v2 (develop-stable(master) branches system) (#13941) 2024-07-09 16:08:49 +09:00
woxtu
55c990e0d9 Fix compose file name (#14153) 2024-07-09 12:42:02 +09:00
かっこかり
9ef6c4716c fix(backend): 名前を空白文字列だけにできる問題を修正 (#14119)
* fix(backend): 名前を空白文字列だけにできる問題を修正

* Update Changelog

* fix test

* Unicodeを含める

* fix

* ユーザー名がUnicode制御文字とスペースのみで構成される場合はnullに

* Revert "ユーザー名がUnicode制御文字とスペースのみで構成される場合はnullに"

This reverts commit 6c752a69c0.

* [ci skip] changelog typo
2024-07-07 14:19:00 +09:00
かっこかり
f119f8c2cc feat(misskey-js): multipart/form-dataのリクエストに対応 (#14147)
* feat(misskey-js): multipart/form-dataのリクエストに対応

* lint

* add test

* Update Changelog

* テストを厳しくする

* lint

* multipart/form-dataではnullのプロパティを弾くように
2024-07-07 14:08:18 +09:00
かっこかり
984d582796 fix(frontend): サーバーサイドbootでエラー画面の描画時にDOMが初期化できていないことがあるのを修正 (#14139) 2024-07-07 09:56:09 +09:00
おさむのひと
fe852920c3 fix(backend): parse5関係の型のimport方法を変更 (#14146) 2024-07-07 09:55:06 +09:00
かっこかり
0ea88c07b4 fix changelog 2024-07-06 22:52:41 +09:00
syuilo
8e1d94c6c7 fix import path 2024-07-06 21:46:19 +09:00
かっこかり
b9ed3b2427 fix(dev): devサーバーで/notes/に直でアクセスしたらサーバー側のレスポンスが返ってくる問題を修正 (#14137) 2024-07-06 11:46:43 +09:00
syuilo
6dd2e9fc0b refactor(frontend): refactor popup api and make sure call dispose callback
Close #14122
2024-07-04 13:14:49 +09:00
zyoshoka
fab7d5e484 fix(storybook): build skipping even after updating impl story files (#14124) 2024-07-04 12:33:43 +09:00
anatawa12
5d03efa1bb dev: fix pnpm dev is broken (#14123)
* dev: pnpm dev is broken

* dev: fix crash pnpm dev because of unhandled promise
2024-07-03 06:40:31 +09:00
woxtu
de1fe7cc5a Use built-in API (#14095) 2024-07-02 14:47:07 +09:00
Kisaragi
eafae79869 test(backend): goodbye, Lenna (#14111) 2024-07-02 14:29:44 +09:00
syuilo
427648c4b8 update deps (#14057)
* wip

* locales/index.jsのymlファイル取得ロジックを調節

* regenerate pnpm-lock.yaml

* fix(backend): typecheck fails

* chore(deps): bump ip-cidr from 4.0.0 to 4.0.1 in /packages/backend

* chore: migrate ESLint configs to flat config (#14094)

* chore: migrate ESLint configs to flat config

* fix: update paths

* fix: frontend lint fails

* refactor(misskey-js): lint build.js

* update deps

---------

Co-authored-by: samunohito <46447427+samunohito@users.noreply.github.com>
Co-authored-by: zyoshoka <root@zyoshoka.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2024-07-02 11:38:34 +09:00
zyoshoka
f1b1e2a7cc fix(storybook): prevent infinite remount of component (#14101)
* fix(storybook): prevent infinite remount of component

* fix: disable flaky `.toMatch()` test
2024-07-02 10:57:20 +09:00
tamaina
7353c7397f merge fix 2024-06-29 09:24:14 +09:00
tamaina
7306a6c7c7 Merge branch 'develop' into ed25519 2024-06-29 09:24:02 +09:00
ぬるきゃっと
a6edd50a5d chore(docker-compose): 推奨の名前にする (#14096)
* chore(docker-compose): 推奨の名前にする

5c18e329d5/spec.md (compose-file)

* yaml to yml

* fix

* fix
2024-06-28 11:16:12 +09:00
woxtu
4096dabe1e Add null checking (#14089) 2024-06-27 21:59:19 +09:00
ぬるきゃっと
0e512d4ff6 update: CHANGELOG.md for #14097 (#14099) 2024-06-27 18:23:47 +09:00
ぬるきゃっと
77012f2f29 fix(frontend): テーマプレビューが見れない問題を修正 (#14097)
* fix(frontend): テーマプレビューが見れない問題を修正

* fix: MkPreview.vue, preview.vue
2024-06-27 10:40:46 +09:00
yupix
1c5d0cf536 feat: アンテナの編集画面のボタンにgapを追加 (#14091) 2024-06-26 10:25:18 +09:00
woxtu
634764e1a6 refactor(frontend): Remove unused directives (#14085) 2024-06-24 21:32:12 +09:00
かっこかり
b95a0457a9 fix(frontend): run pnpm build-assets (#14077) 2024-06-23 19:04:01 +09:00
woxtu
b269c43168 Fix type annotations (#14071) 2024-06-23 01:00:12 +09:00
Ibuki Sugiyama
2acbec6891 enhance: update datasaver switch titles (#12834) 2024-06-22 19:50:32 +09:00
zyoshoka
961cb6c5ee fix(backend): fix creating reactions bugs (#13901)
* fix(backend): add fallback for empty string when creating reaction

* fix(backend): prohibit reactions to Renote

* test(backend): add some tests for `notes/reactions/create` endpoint

* Update CHANGELOG.md

* lint

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-22 19:49:38 +09:00
woxtu
00b213373b Remove @types/node-fetch (#13948) 2024-06-22 19:46:29 +09:00
anatawa12
b8b4dc5038 build: install pnpm with corepack on docker build (#13926)
* build: install pnpm with corepack on build

* docs(changelog): Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
2024-06-22 19:45:08 +09:00
Kisaragi
9368eb3038 refactor: say bye to the weird groupBy friends (#13975)
* refactor(frontend): say bye to the weird groupBy friends

* refactor(backend): say bye to the weird groupBy friends
2024-06-22 19:40:55 +09:00
かっこかり
7c22a64b8c fix(backend): 自分以外のクリップ内のノート個数が見えるのを修正 (#14065)
* fix(backend): 自分以外のクリップ内のノート個数が見えることがあるのを修正

* Update Changelog

* fix
2024-06-22 16:52:27 +09:00
かっこかり
bf403aa656 fix(frontend): ベースロールを編集してもUI上では変更が反映されない問題を修正 (#13995)
* fix(frontend): ベースロールを変更してもUI上では変更が反映されない問題を修正

* Update CHANGELOG.md
2024-06-22 15:35:54 +09:00
Kisaragi
faeab96e01 ci: add quote (#13990) 2024-06-22 14:55:24 +09:00
yupix
b50eb511b0 refactor: api/*/update系の必須キーを最低限に (#13824)
* refactor: clips/updateの必須キーをclipIdのみに

* refactor: admin/roles/update の必須キーをroleIdのみに

* feat: pages/update の必須キーをpageIdのみに

* refactor: gallery/posts/update の必須キーをpostidのみに

* feat: misskey-jsの型を更新

* feat: i/webhooks/updateの必須キーをwebhookIdのみに

* feat: admin/ad/updateの必須キーをidのみに

* feat: misskey-jsの型を更新

* chore: update CHANGELOG.md

* docs: update CHANGELOG.md

* fix: secretが更新できなくなる場合がある

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>

* Update packages/backend/src/server/api/endpoints/gallery/posts/update.ts

---------

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-22 14:52:27 +09:00
Kisaragi
ac12ab8629 fix(backend): フィードのノートのMFMはHTMLにレンダーしてから返す (#14006)
* fix(backend): フィードのノートのMFMはHTMLにレンダーしてから返す (test wip)

* chore: beforeEachを使う?

* fix: プレーンテキストにフォールバックしてMFMが含まれていないか調べる方針を実装

* fix: application/jsonだとパースされるのでその作用をキャンセル

* build: fix lint error

* docs: update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-22 12:51:02 +09:00
ぬるきゃっと
ef205fb60e enhance(frontend): WidgetInstanceInfo.vue と WidgetProfile.vue のスタイル調整 (#14028)
* 🎨 WidgetInstanceInfo.vue and WidgetProfile.vue

* 🎨 WidgetInstanceInfo.vue and WidgetProfile.vue

* 🎨 WidgetInstanceInfo.vue and WidgetProfile.vue

* 🎨 WidgetInstanceInfo.vue and WidgetProfile.vue

* 🎨 home.vue
2024-06-22 12:46:30 +09:00
dependabot[bot]
3254f7c5cd chore(deps): bump docker/build-push-action from 5 to 6 (#14039)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-22 12:45:52 +09:00
かっこかり
7e21497edc fix(frontend): リバーシ開始時の自動投稿のURLが正しくない場合があるのを修正 (#14045)
* fix(frontend): リバーシ開始時の自動投稿のURLが正しくない場合があるのを修正

* ✌️
2024-06-22 12:45:37 +09:00
anatawa12
1e78ef1cb8 fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-22 12:44:01 +09:00
zyoshoka
8a9de081f1 fix(backend): fallback if sinceId is older than the oldest in cache when using FTT (#14061)
* fix(backend): fallback if `sinceId` is older than the oldest in cache when using FTT

* Update CHANGELOG.md

* chore: fix description of test
2024-06-22 12:43:03 +09:00
woxtu
4d2eddec2e Replace with vue/no-setup-props-reactivity-loss rule (#14062) 2024-06-22 12:40:00 +09:00
zyoshoka
a9012d3d0c test(frontend): fix component error in MkChart story (#14056) 2024-06-21 17:29:37 +09:00
woxtu
2c84d06a66 Fix type checking (#14052) 2024-06-21 13:48:04 +09:00
syuilo
e88f08ad7d refactor 2024-06-21 13:31:02 +09:00
syuilo
1d6ccd9781 remove unused files 2024-06-21 13:21:27 +09:00
syuilo
811ffbf3a4 remove unused file 2024-06-21 13:18:19 +09:00
syuilo
bf33382082 refactor(backend): remove unnecessary isNotNull sugar 2024-06-21 13:03:51 +09:00
syuilo
1df90cef4c update typescript 2024-06-21 13:03:00 +09:00
woxtu
b683d79f8b Fix type checking (#14047) 2024-06-20 16:24:10 +09:00
woxtu
77ae69355c Enable to iterate over DOM collections (#14040) 2024-06-19 12:19:38 +09:00
woxtu
f37d684fab Add missing styles (#14031) 2024-06-18 19:46:20 +09:00
Kisaragi
a88579ca98 docs: add changelog entry (follow-up of #14036) (#14037) 2024-06-18 12:44:30 +09:00
Ryu jongheon
d0ee0203e1 Fix(backend): Limit antenna/webhook/list to exact amount (#14036)
... not +1
* Update antennas/clips e2e test
2024-06-18 12:18:04 +09:00
zyoshoka
379ce0145b fix(frontend): fix time on MkChart's story (#13958) 2024-06-15 16:35:41 +09:00
かっこかり
34458d767b fix changelog 2024-06-15 15:46:36 +09:00
anatawa12
96fcb9f54c ci: upgrade dockle (#14002) 2024-06-15 14:10:37 +09:00
zyoshoka
d4e2be68ee fix(frontend): chart in MkInstanceCardMini is no longer displayed (#13932)
* fix(frontend): chart in `MkInstanceCardMini` is no longer displayed

* Update CHANGELOG.md

* test: add `MkInstanceCardMini` story

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-15 10:32:51 +09:00
Acid Chicken
1a82a41f92 refactor(backend): get column names from metadata (#13943)
* ci: enable

* chore: stop when generated column found

* chore: get column names from metadata

* ci: disable
2024-06-15 10:28:57 +09:00
Kisaragi
9bddb81efc chore: issueを起票する前にGitHub Discussionsにも誘導する (#13991) 2024-06-15 08:43:11 +09:00
anatawa12
220e112c83 fix rate limit check never ends (#13994) 2024-06-15 08:42:13 +09:00
Kisaragi
c51347d78b docs: update changelog (follow-up of #13949) (#13971) 2024-06-13 11:09:03 +09:00
Kisaragi
dc3629e732 feat(backend): report Retry-After if client hit rate limit (#13949)
* feat(backend): report `Retry-After` if client hit rate limit

* refactor(backend): fix lint error
2024-06-13 10:56:26 +09:00
syuilo
c73d739bd6 node 22 support 2024-06-13 10:40:20 +09:00
sirsegv
1616cb533e Fix json module imports for node 22 (#13875) 2024-06-13 10:18:01 +09:00
おやき
92367cf700 node_modulesをvolume化して高速化+パーミッション問題回避、git submodule update時にローカルにsubmoduleがあっても問題が起こらないようにする (#13956) 2024-06-12 11:08:42 +09:00
Kisaragi
ff3a38a7f5 fix(frontend): 追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
* fix(frontend): 追加情報のラベルを投稿者のサーバーの絵文字で表示する

* docs: update changelog
2024-06-12 10:52:22 +09:00
tamaina
a3d4eae99d fix 2024-06-11 15:50:55 +09:00
tamaina
133970a184 define additionalPublicKeys 2024-06-11 15:42:17 +09:00
tamaina
64004fdea2 publicKeyに配列が入ってもいいようにする
https://github.com/misskey-dev/misskey/pull/13950
2024-06-11 15:32:55 +09:00
tamaina
3717ff35a3 fix 2024-06-11 14:45:09 +09:00
tamaina
f31996eb42 add comment 2024-06-11 14:37:22 +09:00
tamaina
bdaef5f8e1 Merge branch 'develop' into ed25519 2024-06-11 14:32:54 +09:00
zyoshoka
9849aab402 test(#10336): add components/MkC.* stories (#13830)
* test(storybook): add `components/MkC.*` stories

* test(storybook): add some tests

* test: add sleep

* test: comment-out flaky test

* test(storybook): add test for `MkChannelFollowButton`

* chore(storybook): tweak sleep duration in `MkChannelFollowButton` story test

* fix(chromatic): add delay to `MkChannelList`

* chore: replace `mswDecorator` with `mswLoader`

* fix(storybook): tweak some parameters

* chore: serve static files

* fix(chromatic): add delay to `MkCwButton`

* chore: delete logging for debug

* fix: add right click in `MkContextMenu` play

* refactor: remove unused imports
2024-06-08 18:00:54 +09:00
おさむのひと
61fae45390 feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする (#13758)
* feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする

* モデログに対応&エンドポイントを単一オブジェクトでのサポートに変更(API経由で大量に作るシチュエーションもないと思うので)

* fix spdx

* fix migration

* fix migration

* fix models

* add e2e webhook

* tweak

* fix modlog

* fix bugs

* add tests and fix bugs

* add tests and fix bugs

* add tests

* fix path

* regenerate locale

* 混入除去

* 混入除去

* add abuseReportResolved

* fix pnpm-lock.yaml

* add abuseReportResolved test

* fix bugs

* fix ui

* add tests

* fix CHANGELOG.md

* add tests

* add RoleService.getModeratorIds tests

* WebhookServiceをUserとSystemに分割

* fix CHANGELOG.md

* fix test

* insertOneを使う用に

* fix

* regenerate locales

* revert version

* separate webhook job queue

* fix

* 🎨

* Update QueueProcessorService.ts

---------

Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-08 15:34:19 +09:00
Porlam Nicla
e0cf5b2402 配信停止したインスタンス一覧が見れなくなる問題を修正 (#13945)
* 配信停止したインスタンス一覧が見れなくなる問題を修正

* Update CHANGELOG.md
2024-06-07 14:46:46 +09:00
syuilo
8592716139 enhance(backend): improve sentry integration 2024-06-07 13:15:37 +09:00
taichan
00157864e9 fix(backend): チャート生成時にinstance.isSuspendedが読まれてしまう問題の修正 (#13951)
* fix(backend): use sustensionState instead of isSuspended

* Update CHANGELOG.md
2024-06-07 09:00:01 +09:00
syuilo
8f833d742f enhance(backend): improve sentry integration 2024-06-06 11:51:31 +09:00
syuilo
d55e638a23 lint fixes 2024-06-06 11:40:11 +09:00
syuilo
a697a7f97b enhance(backend): improve sentry integration 2024-06-06 11:38:34 +09:00
syuilo
ab69e113f4 enhance(backend): improve sentry integration 2024-06-06 11:20:54 +09:00
syuilo
65d19279a2 fix 2024-06-06 10:11:43 +09:00
syuilo
dbf9e1194b refactor(backend): remove unused logger option 2024-06-06 10:01:50 +09:00
syuilo
d4a8c63264 enhance(backend): sentry integration for job queues 2024-06-06 09:32:04 +09:00
かっこかり
43cccaaee9 fix 2024-06-04 13:15:35 +09:00
かっこかり
27ac3d795e Update about-misskey.vue 2024-06-04 13:14:37 +09:00
tamaina
be102f2622 @misskey-dev/node-http-message-signatures@0.0.10 2024-05-08 22:03:14 +09:00
tamaina
baca55c814 Merge branch 'develop' into ed25519 2024-05-08 21:40:35 +09:00
tamaina
c58b4f8c24 fix spdx 2024-05-02 15:38:21 +09:00
tamaina
d200da8690 Merge branch 'develop' into ed25519 2024-05-02 15:27:21 +09:00
tamaina
aa5181cdfc use transactionalEntityManager 2024-03-10 16:44:27 +00:00
tamaina
d7c32cef70 fetchPersonWithRenewalでエラーが起きたら古いデータを返す 2024-03-10 16:38:53 +00:00
tamaina
76487de5ed とりあえずtryで囲っておく 2024-03-10 16:17:09 +00:00
tamaina
e2b574a97c save array 2024-03-10 16:00:25 +00:00
tamaina
9bfa38e601 log signature parse err 2024-03-09 20:02:33 +00:00
tamaina
eb8495648e ユーザーとキーの同一性チェックはhostの一致にする 2024-03-09 20:01:07 +00:00
tamaina
154a2026ea inbox-delayed use actor if no signature 2024-03-09 19:15:40 +00:00
tamaina
8104963e1d HTTP Signatureがなかったり使えなかったりしそうな場合にLD Signatureを活用するように 2024-03-09 19:06:59 +00:00
tamaina
da4a44b337 JSON.stringify 2024-03-09 17:29:23 +00:00
tamaina
1690e0617e db-resolver 2024-03-09 17:25:24 +00:00
tamaina
70693af4e4 Merge branch 'ed25519' of https://github.com/misskey-dev/misskey into ed25519 2024-03-09 17:22:20 +00:00
tamaina
d168ec7dd5 logger 2024-03-09 17:22:16 +00:00
tamaina
08e3a7c008 Merge branch 'develop' into ed25519 2024-03-10 01:44:20 +09:00
tamaina
4310229ca5 log keyid 2024-03-09 16:39:01 +00:00
tamaina
75a2f1c1e8 fix type error 2024-03-09 12:27:52 +00:00
tamaina
d0da9f32dc @misskey-dev/node-http-message-signatures@0.0.8 2024-03-09 10:25:58 +00:00
tamaina
6907b6505a retry no key 2024-03-09 10:24:55 +00:00
tamaina
74c8f0a483 update package 2024-03-07 15:21:07 +00:00
tamaina
e543ffe368 fix type 2024-03-07 11:38:23 +00:00
tamaina
9973610286 Merge branch 'develop' into ed25519 2024-03-07 10:52:44 +00:00
tamaina
844feb1bb3 update package 2024-03-07 10:52:38 +00:00
tamaina
fef9ebfe06 fix type error 2024-03-06 12:56:03 +00:00
tamaina
39fba74dd1 update node-http-message-signatures 2024-03-06 06:48:03 +00:00
tamaina
a701fed9e5 fix ap-request 2024-03-05 16:50:26 +00:00
tamaina
ab29cbab41 if 2024-03-05 16:44:32 +00:00
tamaina
01b8d2fdb1 fix test 2024-03-05 16:44:11 +00:00
tamaina
0127f89298 getLocalUserPrivateKey 2024-03-05 16:27:13 +00:00
tamaina
689a9ce5f9 PrivateKeyPem 2024-03-05 15:53:24 +00:00
tamaina
834f46537d @misskey-dev/node-http-message-signatures@0.0.3 2024-03-05 15:26:25 +00:00
tamaina
0e509c440e キャッシュ有効チェックはロック取得前に行う 2024-03-05 13:58:29 +00:00
tamaina
6b02efac32 delivre 2024-03-05 13:18:20 +00:00
tamaina
a84de3c02f Merge branch 'develop' into ed25519 2024-03-05 22:03:15 +09:00
tamaina
021801c721 nanka meccha kaeta 2024-03-05 12:55:30 +00:00
tamaina
e4fea42436 addAllKnowingSharedInboxRecipe 2024-03-05 12:30:22 +00:00
tamaina
430f0b7911 秘密鍵の変更は、フラグではなく鍵を引き回すようにする 2024-03-05 11:57:43 +00:00
tamaina
6e4357c378 change logger 2024-03-05 05:49:12 +00:00
tamaina
ac4336db43 log 2024-03-05 05:41:11 +00:00
tamaina
4b9ffb8dc0 logger 2024-03-05 05:38:27 +00:00
tamaina
31bf1dbc95 a 2024-03-05 05:22:09 +00:00
tamaina
2a622b02dc Revert "fix?"
This reverts commit 0082f6f8e8.
2024-03-05 05:20:37 +00:00
tamaina
0082f6f8e8 fix? 2024-03-05 05:15:56 +00:00
tamaina
15782f7f47 循環参照で落ちるのを解消? 2024-03-05 03:31:13 +00:00
tamaina
ac2cf73a14 Merge branch 'develop' into ed25519 2024-03-05 03:50:05 +09:00
tamaina
7d77c7044e Deliver update person when new key generated (not tested)
https://github.com/misskey-dev/misskey/pull/13464#issuecomment-1977049061
2024-03-04 18:47:07 +00:00
tamaina
1af1bc87bd use save 2024-03-04 10:05:45 +00:00
tamaina
821a79ff28 !== true 2024-03-04 09:33:31 +00:00
tamaina
7a334a5e28 fix 2024-03-04 03:12:17 +00:00
tamaina
79249a0514 fix, async 2024-03-04 02:26:16 +00:00
tamaina
eefca034fc digestプリレンダを復活させる
RFC実装時にどうするか考える
2024-03-04 02:20:09 +00:00
tamaina
25cc9e0bf1 refactor public keys add 2024-03-04 00:34:37 +00:00
tamaina
83f635835e commetn 2024-03-04 00:12:11 +00:00
tamaina
941aed6a14 refreshAndfindKey 2024-03-04 00:05:48 +00:00
tamaina
d772eacfa1 refreshAndprepareEd25519KeyPair 2024-03-03 23:57:30 +00:00
tamaina
6a56aea422 use Promise.all in genRSAAndEd25519KeyPair 2024-03-03 23:53:30 +00:00
tamaina
c7eed1c360 fix 2024-03-03 23:49:04 +00:00
tamaina
76b20dc76c Merge branch 'fix-fedupdlck' into ed25519 2024-03-03 23:46:03 +00:00
tamaina
7eb19d5a8e improve test 2024-03-03 23:45:47 +00:00
tamaina
64fcf736cc comment 2024-03-03 23:36:03 +00:00
tamaina
2926f68d8e comment 2024-03-03 23:33:25 +00:00
tamaina
41a461edbe fix 2024-03-03 23:33:08 +00:00
tamaina
2dde845738 fix test 2024-03-03 23:26:35 +00:00
tamaina
862ebe23af Merge branch 'fix-fedupdlck' into ed25519 2024-03-03 23:20:05 +00:00
tamaina
89e1ff699a fix httpMessageSignaturesImplementationLevel validation 2024-03-03 23:19:13 +00:00
tamaina
25d5a8cb7e fix(backend): fetchInstanceMetadataのLockが永遠に解除されない問題を修正
Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-03-03 23:04:33 +00:00
tamaina
aabdb666b7 err msg 2024-03-03 22:03:25 +00:00
tamaina
13af6f2313 clean up 2024-03-03 21:22:47 +00:00
tamaina
a405b62827 update @misskey-dev/node-http-message-signatures 2024-03-03 21:02:23 +00:00
tamaina
e4f70f017e Merge branch 'develop' into ed25519 2024-03-03 20:55:24 +00:00
tamaina
1357b076d0 use requiredInputs
https://github.com/misskey-dev/misskey/pull/13464#discussion_r1509964359
2024-03-02 13:53:52 +00:00
tamaina
30820d9e0a Merge branch 'develop' into ed25519 2024-03-02 13:06:39 +00:00
tamaina
ea6c38cc6b cacheRaw.date 2024-03-01 18:38:31 +00:00
tamaina
d86b8c8752 getAuthUserFromApIdでupdatePersonの頻度を増やす 2024-03-01 18:29:30 +00:00
tamaina
9111b5c482 @misskey-dev/node-http-message-signatures@0.0.0-alpha.11 2024-03-01 16:33:08 +00:00
tamaina
65bd187d85 fix 2024-03-01 15:51:09 +00:00
tamaina
86c9f0b0fb update node-http-message-signatures 2024-03-01 15:08:12 +00:00
tamaina
65fa25a208 Merge branch 'develop' into ed25519 2024-03-01 12:53:01 +00:00
tamaina
67758d2d1e use gen-key-pair fn. from '@misskey-dev/node-http-message-signatures' 2024-03-01 12:52:46 +00:00
tamaina
fd71ad7a5f use const 2024-03-01 07:27:43 +00:00
tamaina
0aa316ee12 Merge branch 'develop' into ed25519 2024-03-01 07:20:22 +00:00
tamaina
743b740775 ✌️ 2024-03-01 07:18:16 +00:00
tamaina
bec6159b4a exactKey 2024-03-01 06:58:43 +00:00
tamaina
54fe8ca600 fetchPersonWithRenewal 2024-03-01 06:49:38 +00:00
tamaina
a5cccf3799 Merge branch 'develop' into ed25519 2024-03-01 05:04:54 +00:00
tamaina
87ded2bd1c ManyToOne 2024-03-01 05:04:02 +00:00
tamaina
16cea7d3b6 add httpMessageSignaturesImplementationLevel to FederationInstance 2024-03-01 04:38:42 +00:00
tamaina
7751d80056 fix test 2024-02-29 22:57:10 +00:00
tamaina
66c0942d7e fix test? 2024-02-29 22:50:46 +00:00
tamaina
5f89b0a2a3 remove digest prerender 2024-02-29 22:41:45 +00:00
tamaina
434520a14e ap-request.tsを復活させる 2024-02-29 22:36:19 +00:00
tamaina
735714d61c signedPost, signedGet 2024-02-29 22:20:48 +00:00
tamaina
fc20ef0181 fix 2024-02-29 21:18:46 +00:00
tamaina
a1e6cb02b8 use @misskey-dev/node-http-message-signatures 2024-02-29 21:05:31 +00:00
tamaina
a4e7d6940b Merge branch 'develop' into ed25519 2024-02-29 20:34:55 +00:00
tamaina
2bc4221f40 Merge branch 'develop' into ed25519 2024-02-28 14:21:25 +00:00
MeiMei
aaacfabc1b perf(federation): Use hint for getAuthUserFromApId (#13470)
* Hint for getAuthUserFromApId

* とどのつまりこれでいいのか?
2024-02-28 16:44:01 +09:00
tamaina
59ae735169 httpMessageSignaturesImplementationLevel: '01' 2024-02-27 04:52:40 +00:00
tamaina
8579cb222f httpMessageSignaturesImplementationLevel 2024-02-27 04:25:51 +00:00
tamaina
f6b7872a02 httpMessageSignaturesSupported 2024-02-27 03:37:02 +00:00
tamaina
9705ec4a47 remove sign additionalPublicKeys signature requirements 2024-02-27 03:33:50 +00:00
tamaina
437e69cfc4 ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM 2024-02-27 03:08:55 +00:00
tamaina
eb8bef486d ED25519_SIGNED_ALGORITHM 2024-02-27 03:06:19 +00:00
tamaina
5876a28f1e httpMessageSignatureAcceptable 2024-02-27 03:01:07 +00:00
tamaina
e2a8f4f880 refresh 2024-02-27 02:15:49 +00:00
tamaina
13e0a64a77 set publicKeyCache lifetime 2024-02-27 01:57:35 +00:00
tamaina
1d780ac010 signatureAlgorithm 2024-02-27 01:41:34 +00:00
tamaina
172546f3ef fix 2024-02-26 21:44:33 +00:00
tamaina
00738b90c2 ✌️ 2024-02-26 21:31:43 +00:00
tamaina
5b7b8503cd getAuthUserFromApIdはmainを選ぶ 2024-02-26 21:27:50 +00:00
tamaina
1835397385 validate additionalPublicKeys 2024-02-26 21:06:05 +00:00
tamaina
02dfe0a3d5 1. ed25519キーペアを発行・Personとして公開鍵を送受信 2024-02-26 20:49:40 +00:00
482 changed files with 19927 additions and 8285 deletions

View File

@@ -1,5 +1,11 @@
# misskey settings
# MISSKEY_URL=https://example.tld/
# db settings
POSTGRES_PASSWORD=example-misskey-pass
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_USER=example-misskey-user
# DATABASE_USER=${POSTGRES_USER}
POSTGRES_DB=misskey
# DATABASE_DB=${POSTGRES_DB}
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

View File

@@ -6,6 +6,7 @@
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
# You can set url from an environment variable instead.
url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
@@ -38,9 +39,11 @@ db:
port: 5432
# Database name
# You can set db from an environment variable instead.
db: misskey
# Auth
# You can set user and pass from environment variables instead.
user: example-misskey-user
pass: example-misskey-pass
@@ -161,12 +164,12 @@ id: 'aidx'
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# deliverJobConcurrency: 16
# inboxJobConcurrency: 4
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 32
# inboxJobPerSec: 64
# Job attempts
# deliverJobMaxAttempts: 12

View File

@@ -230,15 +230,15 @@ id: 'aidx'
#clusterLimit: 1
# Job concurrency per worker
#deliverJobConcurrency: 128
#inboxJobConcurrency: 16
#deliverJobConcurrency: 16
#inboxJobConcurrency: 4
#relationshipJobConcurrency: 16
# What's relationshipJob?:
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
# Job rate limiter
#deliverJobPerSec: 128
#inboxJobPerSec: 32
#deliverJobPerSec: 1024
#inboxJobPerSec: 64
#relationshipJobPerSec: 64
# Job attempts

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
app:
build:
@@ -8,6 +6,7 @@ services:
volumes:
- ../:/workspace:cached
- node_modules:/workspace/node_modules
command: sleep infinity
@@ -46,6 +45,7 @@ services:
volumes:
postgres-data:
redis-data:
node_modules:
networks:
internal_network:

View File

@@ -1,6 +1,6 @@
{
"name": "Misskey",
"dockerComposeFile": "docker-compose.yml",
"dockerComposeFile": "compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {
@@ -10,7 +10,7 @@
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
},
"forwardPorts": [3000],
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
"postCreateCommand": "/bin/bash .devcontainer/init.sh",
"customizations": {
"vscode": {
"extensions": [

View File

@@ -157,12 +157,12 @@ id: 'aidx'
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# deliverJobConcurrency: 16
# inboxJobConcurrency: 4
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 32
# deliverJobPerSec: 1024
# inboxJobPerSec: 64
# Job attempts
# deliverJobMaxAttempts: 12

View File

@@ -2,7 +2,8 @@
set -xe
sudo chown -R node /workspace
sudo chown node node_modules
git config --global --add safe.directory /workspace
git submodule update --init
corepack install
corepack enable

View File

@@ -7,7 +7,7 @@ Dockerfile
build/
built/
db/
docker-compose.yml
.devcontainer/compose.yml
node_modules/
packages/*/node_modules
redis/
@@ -28,4 +28,4 @@ fluent-emojis/
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml
packages/backend/test/compose.yml

View File

@@ -2,3 +2,7 @@ contact_links:
- name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3
about: Chat freely about Misskey
# 仮
- name: 💬 Start discussion
url: https://github.com/misskey-dev/misskey/discussions
about: The official forum to join conversation and ask question

View File

@@ -4,10 +4,11 @@ on:
push:
paths:
- packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
pull_request:
paths:
- packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
jobs:
report:
@@ -20,7 +21,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -14,7 +14,7 @@ jobs:
- name: Checkout head
uses: actions/checkout@v4.1.1
- name: Setup Node.js
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'

View File

@@ -28,7 +28,7 @@ jobs:
- name: setup node
id: setup-node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: pnpm

View File

@@ -6,12 +6,13 @@ on:
paths:
- packages/misskey-js/package.json
- package.json
- .github/workflows/check-misskey-js-version.yml
pull_request:
branches: [ develop ]
paths:
- packages/misskey-js/package.json
- package.json
- .github/workflows/check-misskey-js-version.yml
jobs:
check-version:
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する

View File

@@ -37,7 +37,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true

View File

@@ -48,7 +48,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub
id: build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true

View File

@@ -13,14 +13,16 @@ jobs:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14
steps:
- uses: actions/checkout@v4.1.1
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose_example.yml ./docker-compose.yml
cp ./compose_example.yml ./compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest

View File

@@ -9,7 +9,7 @@ on:
paths:
- packages/backend/**
- .github/workflows/get-api-diff.yml
- .github/workflows/get-api-diff.yml
jobs:
get-from-misskey:
runs-on: ubuntu-latest
@@ -34,7 +34,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -10,15 +10,16 @@ on:
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
- packages/shared/eslint.config.js
- .github/workflows/lint.yml
pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
- packages/shared/eslint.config.js
- .github/workflows/lint.yml
jobs:
pnpm_install:
runs-on: ubuntu-latest
@@ -28,7 +29,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2
- uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -39,6 +40,8 @@ jobs:
needs: [pnpm_install]
runs-on: ubuntu-latest
continue-on-error: true
env:
eslint-cache-version: v1
strategy:
matrix:
workspace:
@@ -52,13 +55,20 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2
- uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- run: pnpm --filter ${{ matrix.workspace }} run eslint
- name: Restore eslint cache
uses: actions/cache@v4.0.2
with:
path: node_modules/.cache/eslint
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
typecheck:
needs: [pnpm_install]
@@ -75,7 +85,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2
- uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -4,10 +4,11 @@ on:
push:
paths:
- locales/**
- .github/workflows/locale.yml
pull_request:
paths:
- locales/**
- .github/workflows/locale.yml
jobs:
locale_verify:
runs-on: ubuntu-latest
@@ -18,7 +19,7 @@ jobs:
fetch-depth: 0
submodules: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2
- uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -26,7 +26,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -3,10 +3,10 @@ name: "Release Manager: sync changelog with PR"
on:
push:
branches:
- release/**
- develop
paths:
- 'CHANGELOG.md'
# - .github/workflows/release-edit-with-push.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -20,24 +20,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PR
run: |
echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
- name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
if: steps.get_pr.outputs.pr_number != ''
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
id: v
# CHANGELOG.mdの内容を取得
- name: Get changelog
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
if: steps.get_pr.outputs.pr_number != ''
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
with:
version: ${{ steps.v.outputs.target_version }}
id: changelog
# PRのnotesを更新
- name: Update PR
if: steps.get_pr.outputs.pr_number != ''
run: |
gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
env:
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}

View File

@@ -33,18 +33,21 @@ jobs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PRs
run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog
# The first line must be `## Unreleased`
@@ -65,15 +68,14 @@ jobs:
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
@@ -82,10 +84,11 @@ jobs:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }}
with:
user: 'github-actions[bot]'
# The script for version increment.
# process.env.CURRENT_VERSION: The current version.
#
@@ -118,8 +121,7 @@ jobs:
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
stable_branch: ${{ vars.STABLE_BRANCH }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}

View File

@@ -16,23 +16,26 @@ jobs:
check:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.get_pr.outputs.ref }}
head: ${{ steps.get_pr.outputs.head }}
base: ${{ steps.get_pr.outputs.base }}
steps:
- uses: actions/checkout@v4
# PR情報を取得
- name: Get PR
run: |
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
id: get_pr
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: check
if: startsWith(needs.check.outputs.ref, 'release/')
if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
with:
pr_number: ${{ github.event.pull_request.number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}

View File

@@ -36,7 +36,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
BRANCH="$HEAD_REF"
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER")
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@@ -9,12 +9,13 @@ on:
- packages/backend/**
# for permissions
- packages/misskey-js/**
- .github/workflows/test-backend.yml
pull_request:
paths:
- packages/backend/**
# for permissions
- packages/misskey-js/**
- .github/workflows/test-backend.yml
jobs:
unit:
runs-on: ubuntu-latest
@@ -45,7 +46,7 @@ jobs:
- name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@@ -92,7 +93,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -11,7 +11,7 @@ on:
- packages/misskey-js/**
# for e2e
- packages/backend/**
- .github/workflows/test-frontend.yml
pull_request:
paths:
- packages/frontend/**
@@ -19,7 +19,7 @@ on:
- packages/misskey-js/**
# for e2e
- packages/backend/**
- .github/workflows/test-frontend.yml
jobs:
vitest:
runs-on: ubuntu-latest
@@ -35,7 +35,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@@ -90,7 +90,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -8,11 +8,12 @@ on:
branches: [ develop ]
paths:
- packages/misskey-js/**
- .github/workflows/test-misskey-js.yml
pull_request:
branches: [ develop ]
paths:
- packages/misskey-js/**
- .github/workflows/test-misskey-js.yml
jobs:
test:
@@ -30,7 +31,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -25,7 +25,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@@ -7,10 +7,11 @@ on:
- develop
paths:
- packages/backend/**
- .github/workflows/validate-api-json.yml
pull_request:
paths:
- packages/backend/**
- .github/workflows/validate-api-json.yml
jobs:
validate-api-json:
runs-on: ubuntu-latest
@@ -26,7 +27,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

4
.gitignore vendored
View File

@@ -35,8 +35,8 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
docker-compose.yml
!/.devcontainer/docker-compose.yml
.devcontainer/compose.yml
!/.devcontainer/compose.yml
# misskey
/build

View File

@@ -1,14 +1,72 @@
## Unreleased
## 2024.7.0
### Note
- デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
### General
-
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Feat: 連合に使うHTTP SignaturesがEd25519鍵に対応するように #13464
- Ed25519署名に対応するサーバーが増えると、deliverで要求されるサーバーリソースが削減されます
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
### Client
-
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
- Enhance: 非ログイン時のハイライトTLのデザインを改善
- Enhance: フロントエンドのアクセシビリティ改善
(Based on https://github.com/taiyme/misskey/pull/226)
- Enhance: サーバー情報ページ・お問い合わせページを改善
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
- Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
- Fix: アンテナの編集画面のボタンに隙間を追加
- Fix: テーマプレビューが見れない問題を修正
- Fix: ショートカットキーが連打できる問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
- Fix: MkSignin.vueのcredentialRequestからReactivityを削除ProxyがPasskey認証処理に渡ることを避けるため
### Server
-
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149
1. フォロー中かつアクティブなユーザ
2. フォロー中かつ非アクティブなユーザ
3. フォローしていないアクティブなユーザ
4. フォローしていない非アクティブなユーザ
また、自分自身のアカウントもサジェストされるようになりました。
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
- Fix: エラーメッセージの誤字を修正 (#14213)
### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
- Feat: `/admin/role/create` のロールポリシーの型を修正
## 2024.5.0

View File

@@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
```
Prepare DB/Redis for testing.
```
docker compose -f packages/backend/test/docker-compose.yml up
docker compose -f packages/backend/test/compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
@@ -185,7 +185,7 @@ 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.
- `MISSKEY_USE_HTTP`: If it's set true, federation requests (like nodeinfo and webfinger) will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION. (was `MISSKEY_WEBFINGER_USE_HTTP`)
## Continuous integration
Misskey uses GitHub Actions for executing automated tests.

View File

@@ -82,6 +82,10 @@ RUN apt-get update \
USER misskey
WORKDIR /misskey
# add package.json to add pnpm
COPY --chown=misskey:misskey ./package.json ./package.json
RUN corepack install
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules

View File

@@ -178,12 +178,12 @@ id: "aidx"
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# deliverJobConcurrency: 16
# inboxJobConcurrency: 4
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 32
# deliverJobPerSec: 1024
# inboxJobPerSec: 64
# Job attempts
# deliverJobMaxAttempts: 12

View File

@@ -1,5 +1,3 @@
version: "3"
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
services:

View File

@@ -1,5 +1,3 @@
version: "3"
services:
web:
build: .
@@ -19,6 +17,8 @@ services:
networks:
- internal_network
- external_network
# env_file:
# - .config/docker.env
volumes:
- ./files:/misskey/files
- ./.config:/misskey/.config:ro

140
locales/index.d.ts vendored
View File

@@ -736,6 +736,22 @@ export interface Locale extends ILocale {
* リモートで表示
*/
"showOnRemote": string;
/**
* リモートで続行
*/
"continueOnRemote": string;
/**
* Misskey Hubからサーバーを選択
*/
"chooseServerOnMisskeyHub": string;
/**
* サーバーのドメインを直接指定
*/
"specifyServerHost": string;
/**
* ドメインを入力してください
*/
"inputHostName": string;
/**
* 全般
*/
@@ -1921,9 +1937,13 @@ export interface Locale extends ILocale {
*/
"onlyOneFileCanBeAttached": string;
/**
* 続行する前に、サインアップまたはサインインが必要です
* 続行する前に、登録またはログインが必要です
*/
"signinRequired": string;
/**
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
*/
"signinOrContinueOnRemote": string;
/**
* 招待
*/
@@ -4984,6 +5004,10 @@ export interface Locale extends ILocale {
* お問い合わせ
*/
"inquiry": string;
/**
* もう一度お試しください。
*/
"tryAgain": string;
"_delivery": {
/**
* 配信状態
@@ -6594,6 +6618,10 @@ export interface Locale extends ILocale {
* ファイルにNSFWを常に付与
*/
"alwaysMarkNsfw": string;
/**
* アイコンとバナーの更新を許可
*/
"canUpdateBioMedia": string;
/**
* ノートのピン留めの最大数
*/
@@ -7515,14 +7543,6 @@ export interface Locale extends ILocale {
* 通知
*/
"notification": string;
/**
* アンテナ受信
*/
"antenna": string;
/**
* チャンネル通知
*/
"channel": string;
/**
* リアクション選択時
*/
@@ -9305,6 +9325,10 @@ export interface Locale extends ILocale {
* Webhookを作成
*/
"createWebhook": string;
/**
* Webhookを編集
*/
"modifyWebhook": string;
/**
* 名前
*/
@@ -9351,6 +9375,72 @@ export interface Locale extends ILocale {
*/
"mention": string;
};
"_systemEvents": {
/**
* ユーザーから通報があったとき
*/
"abuseReport": string;
/**
* ユーザーからの通報を処理したとき
*/
"abuseReportResolved": string;
};
/**
* Webhookを削除しますか
*/
"deleteConfirm": string;
};
"_abuseReport": {
"_notificationRecipient": {
/**
* 通報の通知先を追加
*/
"createRecipient": string;
/**
* 通報の通知先を編集
*/
"modifyRecipient": string;
/**
* 通知先の種類
*/
"recipientType": string;
"_recipientType": {
/**
* メール
*/
"mail": string;
/**
* Webhook
*/
"webhook": string;
"_captions": {
/**
* モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
*/
"mail": string;
/**
* 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
*/
"webhook": string;
};
};
/**
* キーワード
*/
"keywords": string;
/**
* 通知先ユーザー
*/
"notifiedUser": string;
/**
* 使用するWebhook
*/
"notifiedWebhook": string;
/**
* 通知先を削除しますか?
*/
"deleteConfirm": string;
};
};
"_moderationLogTypes": {
/**
@@ -9497,6 +9587,30 @@ export interface Locale extends ILocale {
* ユーザーのバナーを解除
*/
"unsetUserBanner": string;
/**
* SystemWebhookを作成
*/
"createSystemWebhook": string;
/**
* SystemWebhookを更新
*/
"updateSystemWebhook": string;
/**
* SystemWebhookを削除
*/
"deleteSystemWebhook": string;
/**
* 通報の通知先を作成
*/
"createAbuseReportNotificationRecipient": string;
/**
* 通報の通知先を更新
*/
"updateAbuseReportNotificationRecipient": string;
/**
* 通報の通知先を削除
*/
"deleteAbuseReportNotificationRecipient": string;
};
"_fileViewer": {
/**
@@ -9667,7 +9781,7 @@ export interface Locale extends ILocale {
"_dataSaver": {
"_media": {
/**
* メディアの読み込み
* メディアの読み込みを無効化
*/
"title": string;
/**
@@ -9677,7 +9791,7 @@ export interface Locale extends ILocale {
};
"_avatar": {
/**
* アイコン画像
* アイコン画像のアニメーションを無効化
*/
"title": string;
/**
@@ -9687,7 +9801,7 @@ export interface Locale extends ILocale {
};
"_urlPreview": {
/**
* URLプレビューのサムネイル
* URLプレビューのサムネイルを非表示
*/
"title": string;
/**
@@ -9697,7 +9811,7 @@ export interface Locale extends ILocale {
};
"_code": {
/**
* コードハイライト
* コードハイライトを非表示
*/
"title": string;
/**

View File

@@ -52,7 +52,11 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() {
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
// vitestの挙動を調整するため、一度ローカル変数化する必要がある
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
const metaUrl = import.meta.url;
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => {

View File

@@ -180,6 +180,10 @@ addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示"
continueOnRemote: "リモートで続行"
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
specifyServerHost: "サーバーのドメインを直接指定"
inputHostName: "ドメインを入力してください"
general: "全般"
wallpaper: "壁紙"
setWallpaper: "壁紙を設定"
@@ -476,7 +480,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
signinRequired: "続行する前に、登録またはログインが必要です"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
invitations: "招待"
invitationCode: "招待コード"
checking: "確認しています"
@@ -1242,6 +1247,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。"
_delivery:
status: "配信状態"
@@ -1705,6 +1711,7 @@ _role:
canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数"
@@ -1971,8 +1978,6 @@ _sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
antenna: "アンテナ受信"
channel: "チャンネル通知"
reaction: "リアクション選択時"
_soundSettings:
@@ -2468,6 +2473,7 @@ _drivecleaner:
_webhookSettings:
createWebhook: "Webhookを作成"
modifyWebhook: "Webhookを編集"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
@@ -2480,6 +2486,26 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
_systemEvents:
abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき"
deleteConfirm: "Webhookを削除しますか"
_abuseReport:
_notificationRecipient:
createRecipient: "通報の通知先を追加"
modifyRecipient: "通報の通知先を編集"
recipientType: "通知先の種類"
_recipientType:
mail: "メール"
webhook: "Webhook"
_captions:
mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
keywords: "キーワード"
notifiedUser: "通知先ユーザー"
notifiedWebhook: "使用するWebhook"
deleteConfirm: "通知先を削除しますか?"
_moderationLogTypes:
createRole: "ロールを作成"
@@ -2518,6 +2544,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
createSystemWebhook: "SystemWebhookを作成"
updateSystemWebhook: "SystemWebhookを更新"
deleteSystemWebhook: "SystemWebhookを削除"
createAbuseReportNotificationRecipient: "通報の通知先を作成"
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
_fileViewer:
title: "ファイルの詳細"
@@ -2572,16 +2604,16 @@ _externalResourceInstaller:
_dataSaver:
_media:
title: "メディアの読み込み"
title: "メディアの読み込みを無効化"
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
_avatar:
title: "アイコン画像"
title: "アイコン画像のアニメーションを無効化"
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
_urlPreview:
title: "URLプレビューのサムネイル"
title: "URLプレビューのサムネイルを非表示"
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
_code:
title: "コードハイライト"
title: "コードハイライトを非表示"
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
_hemisphere:

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2024.5.0",
"version": "2024.7.0-beta.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@9.0.6",
"packageManager": "pnpm@9.5.0",
"workspaces": [
"packages/frontend",
"packages/backend",
@@ -55,20 +55,22 @@
"js-yaml": "4.1.0",
"postcss": "8.4.38",
"tar": "6.2.1",
"terser": "5.30.3",
"typescript": "5.4.5",
"esbuild": "0.20.2",
"terser": "5.31.1",
"typescript": "5.5.3",
"esbuild": "0.22.0",
"glob": "10.3.12"
},
"devDependencies": {
"@types/node": "20.12.7",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"@misskey-dev/eslint-plugin": "2.0.2",
"@types/node": "20.14.9",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"cross-env": "7.0.3",
"cypress": "13.7.3",
"eslint": "8.57.0",
"cypress": "13.13.0",
"eslint": "9.6.0",
"globals": "15.7.0",
"ncp": "2.0.0",
"start-server-and-test": "2.0.3"
"start-server-and-test": "2.0.4"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0"

View File

@@ -1,4 +0,0 @@
node_modules
/built
/.eslintrc.js
/@types/**/*

View File

@@ -1,32 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', './test/tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script
id="api-reference"
data-url="/api.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
</body>
</html>

View File

@@ -0,0 +1,46 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
ignores: ['**/node_modules', 'built', '@types/**/*'],
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json', './test/tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'import/order': ['warn', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
pathGroups: [{
pattern: '@/**',
group: 'external',
position: 'after',
}],
}],
'no-restricted-globals': ['error', {
name: '__dirname',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}, {
name: '__filename',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}],
},
},
];

View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class APMultipleKeys1708980134301 {
name = 'APMultipleKeys1708980134301'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_171e64971c780ebd23fae140bb"`);
await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKey" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PrivateKey" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`);
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_10c146e4b39b443ede016f6736d"`);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`);
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_0db6a5fdb992323449edc8ee421"`);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_171e64971c780ebd23fae140bba" PRIMARY KEY ("keyId")`);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`);
await queryRunner.query(`CREATE INDEX "IDX_10c146e4b39b443ede016f6736" ON "user_publickey" ("userId") `);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_10c146e4b39b443ede016f6736"`);
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`);
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_171e64971c780ebd23fae140bba"`);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`);
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_0db6a5fdb992323449edc8ee421"`);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_10c146e4b39b443ede016f6736d" PRIMARY KEY ("userId")`);
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" TYPE "public"."user_profile_followersVisibility_enum_old" USING "followersVisibility"::"text"::"public"."user_profile_followersVisibility_enum_old"`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" SET DEFAULT 'public'`);
await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PrivateKey"`);
await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKey"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `);
}
}

View File

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

View File

@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class APMultipleKeys1709269211718 {
name = 'APMultipleKeys1709269211718'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`);
}
}

View File

@@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AbuseReportNotification1713656541000 {
name = 'AbuseReportNotification1713656541000'
async up(queryRunner) {
await queryRunner.query(`
CREATE TABLE "system_webhook" (
"id" varchar(32) NOT NULL,
"isActive" boolean NOT NULL DEFAULT true,
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
"latestSentAt" timestamp with time zone NULL DEFAULT NULL,
"latestStatus" integer NULL DEFAULT NULL,
"name" varchar(255) NOT NULL,
"on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
"url" varchar(1024) NOT NULL,
"secret" varchar(1024) NOT NULL,
CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
);
CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
CREATE TABLE "abuse_report_notification_recipient" (
"id" varchar(32) NOT NULL,
"isActive" boolean NOT NULL DEFAULT true,
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" varchar(255) NOT NULL,
"method" varchar(64) NOT NULL,
"userId" varchar(32) NULL DEFAULT NULL,
"systemWebhookId" varchar(32) NULL DEFAULT NULL,
CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
);
CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
`);
}
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
DROP INDEX "IDX_abuse_report_notification_recipient_method";
DROP INDEX "IDX_abuse_report_notification_recipient_userId";
DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
DROP TABLE "abuse_report_notification_recipient";
DROP INDEX "IDX_system_webhook_isActive";
DROP INDEX "IDX_system_webhook_on";
DROP TABLE "system_webhook";
`);
}
}

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": "^20.10.0"
"node": "^20.10.0 || ^22.0.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
@@ -65,43 +65,43 @@
"utf-8-validate": "6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.412.0",
"@aws-sdk/lib-storage": "3.412.0",
"@bull-board/api": "5.17.0",
"@bull-board/fastify": "5.17.0",
"@bull-board/ui": "5.17.0",
"@aws-sdk/client-s3": "3.600.0",
"@aws-sdk/lib-storage": "3.600.0",
"@bull-board/api": "5.20.5",
"@bull-board/fastify": "5.20.5",
"@bull-board/ui": "5.20.5",
"@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
"@fastify/cors": "9.0.1",
"@fastify/express": "3.0.0",
"@fastify/http-proxy": "9.5.0",
"@fastify/multipart": "8.2.0",
"@fastify/static": "7.0.3",
"@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.4",
"@fastify/view": "9.1.0",
"@misskey-dev/node-http-message-signatures": "0.0.10",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "^0.1.52",
"@nestjs/common": "10.3.8",
"@nestjs/core": "10.3.8",
"@nestjs/testing": "10.3.8",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "^8.5.0",
"@sentry/profiling-node": "^8.5.0",
"@napi-rs/canvas": "^0.1.53",
"@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.10",
"@sentry/node": "8.13.0",
"@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12",
"@swc/core": "1.4.17",
"@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
"ajv": "8.13.0",
"ajv": "8.16.0",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.7.8",
"bullmq": "5.8.3",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@@ -112,27 +112,27 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "4.26.2",
"fastify": "4.28.1",
"fastify-raw-body": "4.3.0",
"feed": "4.2.2",
"file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.2.1",
"got": "14.4.1",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.4.1",
"ip-cidr": "3.1.0",
"ip-cidr": "4.0.1",
"ipaddr.js": "2.2.0",
"is-svg": "5.0.0",
"is-svg": "5.0.1",
"js-yaml": "4.1.0",
"jsdom": "24.0.0",
"jsdom": "24.1.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"meilisearch": "0.38.0",
"meilisearch": "0.41.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
@@ -142,24 +142,24 @@
"nanoid": "5.0.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.9.13",
"nodemailer": "6.9.14",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.2.3",
"otpauth": "9.3.1",
"parse5": "7.1.2",
"pg": "8.11.5",
"pg": "8.12.0",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
"pug": "3.0.3",
"punycode": "2.3.1",
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.20.10",
"re2": "1.21.3",
"redis-lock": "0.1.4",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
@@ -167,27 +167,26 @@
"rxjs": "7.8.1",
"sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0",
"sharp": "0.33.3",
"sharp": "0.33.4",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.22.7",
"systeminformation": "5.22.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",
"tsc-alias": "1.8.8",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.4.5",
"typescript": "5.5.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.17.0",
"ws": "8.17.1",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0",
"@nestjs/platform-express": "10.3.8",
"@nestjs/platform-express": "10.3.10",
"@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36",
"@types/accepts": "1.3.7",
@@ -197,22 +196,21 @@
"@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3",
"@types/http-link-header": "1.0.5",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.12.7",
"@types/node-fetch": "3.0.3",
"@types/node": "20.14.9",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4",
"@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.5",
"@types/pg": "8.11.6",
"@types/pug": "2.0.10",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
@@ -228,18 +226,17 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"aws-sdk-client-mock": "3.0.1",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"execa": "8.0.1",
"fkill": "^9.0.0",
"execa": "9.2.0",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.0",
"nodemon": "3.1.4",
"pid-port": "1.0.0",
"simple-oauth2": "5.0.0"
"simple-oauth2": "5.0.1"
}
}

View File

@@ -30,6 +30,7 @@ function execStart() {
async function killProc() {
if (backendProcess) {
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
backendProcess.kill();
await new Promise(resolve => backendProcess.on('exit', resolve));
backendProcess = undefined;
@@ -46,6 +47,7 @@ async function killProc() {
],
{
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
serialization: "json",
})
.on('message', async (message) => {
if (message.type === 'exit') {

View File

@@ -1,82 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module '@peertube/http-signature' {
import type { IncomingMessage, ClientRequest } from 'node:http';
interface ISignature {
keyId: string;
algorithm: string;
headers: string[];
signature: string;
}
interface IOptions {
headers?: string[];
algorithm?: string;
strict?: boolean;
authorizationHeaderName?: string;
}
interface IParseRequestOptions extends IOptions {
clockSkew?: number;
}
interface IParsedSignature {
scheme: string;
params: ISignature;
signingString: string;
algorithm: string;
keyId: string;
}
type RequestSignerConstructorOptions =
IRequestSignerConstructorOptionsFromProperties |
IRequestSignerConstructorOptionsFromFunction;
interface IRequestSignerConstructorOptionsFromProperties {
keyId: string;
key: string | Buffer;
algorithm?: string;
}
interface IRequestSignerConstructorOptionsFromFunction {
sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void;
}
class RequestSigner {
constructor(options: RequestSignerConstructorOptions);
public writeHeader(header: string, value: string): string;
public writeDateHeader(): string;
public writeTarget(method: string, path: string): void;
public sign(cb: (err: any, authz: string) => void): void;
}
interface ISignRequestOptions extends IOptions {
keyId: string;
key: string;
httpVersion?: string;
}
export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
export function sign(request: ClientRequest, options: ISignRequestOptions): boolean;
export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean;
export function createSigner(): RequestSigner;
export function isSigner(obj: any): obj is RequestSigner;
export function sshKeyToPEM(key: string): string;
export function sshKeyFingerprint(key: string): string;
export function pemToRsaSSHKey(pem: string, comment: string): string;
export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean;
}

View File

@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
const logger = new Logger('core', 'cyan');
const nestLogger = logger.createSubLogger('nest', 'green', false);
const nestLogger = logger.createSubLogger('nest', 'green');
export class NestLogger implements LoggerService {
/**

View File

@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
const clusterLogger = logger.createSubLogger('cluster', 'orange');
const ev = new Xev();
//#region Events

View File

@@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');

View File

@@ -4,13 +4,36 @@
*/
import cluster from 'node:cluster';
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { envOption } from '@/env.js';
import { loadConfig } from '@/config.js';
import { jobQueue, server } from './common.js';
/**
* Init worker process
*/
export async function workerMain() {
const config = loadConfig();
if (config.sentryForBackend) {
Sentry.init({
integrations: [
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,
maxBreadcrumbs: 0,
...config.sentryForBackend.options,
});
}
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {

View File

@@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
* 設定ファイルの型
*/
type Source = {
url: string;
url?: string;
port?: number;
socket?: string;
chmodSocket?: string;
@@ -31,9 +31,9 @@ type Source = {
db: {
host: string;
port: number;
db: string;
user: string;
pass: string;
db?: string;
user?: string;
pass?: string;
disableCache?: boolean;
extra?: { [x: string]: string };
};
@@ -202,13 +202,17 @@ export function loadConfig(): Config {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const url = tryCreateUrl(config.url);
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
const version = meta.version;
const host = url.host;
const hostname = url.hostname;
const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws');
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null;
@@ -231,7 +235,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`,
db: config.db,
db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch,
@@ -259,7 +263,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet,
signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ?

View File

@@ -3,11 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// dummy
export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
export const REMOTE_USER_CACHE_TTL = 1000 * 60 * 60 * 3; // 3hours
export const REMOTE_USER_MOVE_COOLDOWN = 1000 * 60 * 60 * 24 * 14; // 14days
export const REMOTE_SERVER_CACHE_TTL = 1000 * 60 * 60 * 3; // 3hours
//#region hard limits
// If you change DB_* values, you must also change the DB schema.

View File

@@ -0,0 +1,405 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
import { Brackets, In, IsNull, Not } from 'typeorm';
import * as Redis from 'ioredis';
import sanitizeHtml from 'sanitize-html';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import type {
AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient,
MiAbuseUserReport,
MiUser,
} from '@/models/_.js';
import { EmailService } from '@/core/EmailService.js';
import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { IdService } from './IdService.js';
@Injectable()
export class AbuseReportNotificationService implements OnApplicationShutdown {
constructor(
@Inject(DI.abuseReportNotificationRecipientRepository)
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
private idService: IdService,
private roleService: RoleService,
private systemWebhookService: SystemWebhookService,
private emailService: EmailService,
private metaService: MetaService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.redisForSub.on('message', this.onMessage);
}
/**
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
* 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
*
* @see RoleService.getModeratorIds
* @see GlobalEventService.publishAdminStream
*/
@bindThis
public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
if (abuseReports.length <= 0) {
return;
}
const moderatorIds = await this.roleService.getModeratorIds(true, true);
for (const moderatorId of moderatorIds) {
for (const abuseReport of abuseReports) {
this.globalEventService.publishAdminStream(
moderatorId,
'newAbuseUserReport',
{
id: abuseReport.id,
targetUserId: abuseReport.targetUserId,
reporterId: abuseReport.reporterId,
comment: abuseReport.comment,
},
);
}
}
}
/**
* Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
* メールアドレスの送信先は以下の通り.
* - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
* - metaテーブルに設定されているメールアドレス
*
* @see EmailService.sendEmail
*/
@bindThis
public async notifyMail(abuseReports: MiAbuseUserReport[]) {
if (abuseReports.length <= 0) {
return;
}
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
.filter(it => it.isActive && it.userProfile?.emailVerified)
.map(it => it.userProfile?.email)
.filter(x => x != null),
);
// 送信先の鮮度を保つため、毎回取得する
const meta = await this.metaService.fetch(true);
recipientEMailAddresses.push(
...(meta.email ? [meta.email] : []),
);
if (recipientEMailAddresses.length <= 0) {
return;
}
for (const mailAddress of recipientEMailAddresses) {
await Promise.all(
abuseReports.map(it => {
// TODO: 送信処理はJobQueue化したい
return this.emailService.sendEmail(
mailAddress,
'New Abuse Report',
sanitizeHtml(it.comment),
sanitizeHtml(it.comment),
);
}),
);
}
}
/**
* SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
*
* @see SystemWebhookService.enqueueSystemWebhook
*/
@bindThis
public async notifySystemWebhook(
abuseReports: MiAbuseUserReport[],
type: 'abuseReport' | 'abuseReportResolved',
) {
if (abuseReports.length <= 0) {
return;
}
const recipientWebhookIds = await this.fetchWebhookRecipients()
.then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
.map(it => it.systemWebhookId)
.filter(x => x != null));
for (const webhookId of recipientWebhookIds) {
await Promise.all(
abuseReports.map(it => {
return this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,
it,
);
}),
);
}
}
/**
* 通報の通知先一覧を取得する.
*
* @param {Object} [params] クエリの取得条件
* @param {Object} [params.method] 取得する通知先の通知方法
* @param {Object} [opts] 動作時の詳細なオプション
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
* @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
* @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
* @see removeUnauthorizedRecipientUsers
*/
@bindThis
public async fetchRecipients(
params?: {
ids?: MiAbuseReportNotificationRecipient['id'][],
method?: RecipientMethod[],
},
opts?: {
removeUnauthorized?: boolean,
joinUser?: boolean,
joinSystemWebhook?: boolean,
},
): Promise<MiAbuseReportNotificationRecipient[]> {
const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
if (opts?.joinUser) {
query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
}
if (opts?.joinSystemWebhook) {
query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
}
if (params?.ids) {
query.andWhere({ id: In(params.ids) });
}
if (params?.method) {
query.andWhere(new Brackets(qb => {
if (params.method?.includes('email')) {
qb.orWhere({ method: 'email', userId: Not(IsNull()) });
}
if (params.method?.includes('webhook')) {
qb.orWhere({ method: 'webhook', userId: IsNull() });
}
}));
}
const recipients = await query.getMany();
if (recipients.length <= 0) {
return [];
}
// アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
return (opts?.removeUnauthorized ?? true)
? await this.removeUnauthorizedRecipientUsers(recipients)
: recipients;
}
/**
* EMailの通知先一覧を取得する.
* リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
*
* @param {Object} [opts]
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
* @see removeUnauthorizedRecipientUsers
*/
@bindThis
public async fetchEMailRecipients(opts?: {
removeUnauthorized?: boolean
}): Promise<MiAbuseReportNotificationRecipient[]> {
return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
}
/**
* Webhookの通知先一覧を取得する.
* リレーション先の{@link MiSystemWebhook}も同時に取得する.
*/
@bindThis
public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
}
/**
* 通知先を作成する.
*/
@bindThis
public async createRecipient(
params: {
isActive: MiAbuseReportNotificationRecipient['isActive'];
name: MiAbuseReportNotificationRecipient['name'];
method: MiAbuseReportNotificationRecipient['method'];
userId: MiAbuseReportNotificationRecipient['userId'];
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
},
updater: MiUser,
): Promise<MiAbuseReportNotificationRecipient> {
const id = this.idService.gen();
await this.abuseReportNotificationRecipientRepository.insert({
...params,
id,
});
const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
this.moderationLogService
.log(updater, 'createAbuseReportNotificationRecipient', {
recipientId: id,
recipient: created,
})
.then();
return created;
}
/**
* 通知先を更新する.
*/
@bindThis
public async updateRecipient(
params: {
id: MiAbuseReportNotificationRecipient['id'];
isActive: MiAbuseReportNotificationRecipient['isActive'];
name: MiAbuseReportNotificationRecipient['name'];
method: MiAbuseReportNotificationRecipient['method'];
userId: MiAbuseReportNotificationRecipient['userId'];
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
},
updater: MiUser,
): Promise<MiAbuseReportNotificationRecipient> {
const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
await this.abuseReportNotificationRecipientRepository.update(params.id, {
isActive: params.isActive,
updatedAt: new Date(),
name: params.name,
method: params.method,
userId: params.userId,
systemWebhookId: params.systemWebhookId,
});
const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
this.moderationLogService
.log(updater, 'updateAbuseReportNotificationRecipient', {
recipientId: params.id,
before: beforeEntity,
after: afterEntity,
})
.then();
return afterEntity;
}
/**
* 通知先を削除する.
*/
@bindThis
public async deleteRecipient(
id: MiAbuseReportNotificationRecipient['id'],
updater: MiUser,
) {
const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
await this.abuseReportNotificationRecipientRepository.delete(id);
this.moderationLogService
.log(updater, 'deleteAbuseReportNotificationRecipient', {
recipientId: id,
recipient: entity,
})
.then();
}
/**
* モデレータ権限を持たない(*1)通知先ユーザを削除する.
*
* *1: 以下の両方を満たすものの事を言う
* - 通知先にユーザIDが設定されている
* - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
*
* @param recipients 通知先一覧の配列
* @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
*/
@bindThis
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
const userRecipients = recipients.filter(it => it.userId !== null);
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
if (recipientUserIds.size <= 0) {
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
return recipients;
}
// モデレータ権限の有無で通知先設定を振り分ける
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
for (const recipient of userRecipients) {
// eslint-disable-next-line
if (authorizedUserIds.includes(recipient.userId!)) {
authorizedUserRecipients.push(recipient);
} else {
unauthorizedUserRecipients.push(recipient);
}
}
// モデレータ権限を持たない通知先をDBから削除する
if (unauthorizedUserRecipients.length > 0) {
await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
}
const nonUserRecipients = recipients.filter(it => it.userId === null);
return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel !== 'internal') {
return;
}
const { type } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'roleUpdated':
case 'roleDeleted':
case 'userRoleUnassigned': {
// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
process.nextTick(async () => {
const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
userId: Not(IsNull()),
});
await this.removeUnauthorizedRecipientUsers(recipients);
});
break;
}
default: {
break;
}
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { QueueService } from '@/core/QueueService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { IdService } from './IdService.js';
@Injectable()
export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) {
}
/**
* ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
* - 管理者用Redisイベント
* - EMailモデレータ権限所有者ユーザmetaテーブルに設定されているメールアドレス
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async report(params: {
targetUserId: MiAbuseUserReport['targetUserId'],
targetUserHost: MiAbuseUserReport['targetUserHost'],
reporterId: MiAbuseUserReport['reporterId'],
reporterHost: MiAbuseUserReport['reporterHost'],
comment: string,
}[]) {
const entities = params.map(param => {
return {
id: this.idService.gen(),
targetUserId: param.targetUserId,
targetUserHost: param.targetUserHost,
reporterId: param.reporterId,
reporterHost: param.reporterHost,
comment: param.comment,
};
});
const reports = Array.of<MiAbuseUserReport>();
for (const entity of entities) {
const report = await this.abuseUserReportsRepository.insertOne(entity);
reports.push(report);
}
return Promise.all([
this.abuseReportNotificationService.notifyAdminStream(reports),
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
this.abuseReportNotificationService.notifyMail(reports),
]);
}
/**
* 通報を解決し、その内容を下記の手段で管理者各位に通知する.
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
* @param operator 通報を処理したユーザ
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
forward: boolean;
}[],
operator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
id: In(params.map(it => it.reportId)),
});
for (const report of reports) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ps = paramsMap.get(report.id)!;
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: operator.id,
forwarded: ps.forward && report.targetUserHost !== null,
});
if (ps.forward && report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
// eslint-disable-next-line
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
const contextAssignedFlag = this.apRendererService.addContext(flag);
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
}
this.moderationLogService
.log(operator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost !== null,
})
.then();
}
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
}

View File

@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
@@ -12,30 +13,44 @@ import { RelayService } from '@/core/RelayService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
@Injectable()
export class AccountUpdateService {
export class AccountUpdateService implements OnModuleInit {
private apDeliverManagerService: ApDeliverManagerService;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private userEntityService: UserEntityService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private relayService: RelayService,
) {
}
async onModuleInit() {
this.apDeliverManagerService = this.moduleRef.get(ApDeliverManagerService.name);
}
@bindThis
public async publishToFollowers(userId: MiUser['id']) {
/**
* Deliver account update to followers
* @param userId user id
* @param deliverKey optional. Private key to sign the deliver.
*/
public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKeyWithPem) {
const user = await this.usersRepository.findOneBy({ id: userId });
if (user == null) throw new Error('user not found');
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content);
await Promise.allSettled([
this.apDeliverManagerService.deliverToFollowers(user, content, deliverKey),
this.relayService.deliverToRelays(user, content, deliverKey),
]);
}
}
}

View File

@@ -41,7 +41,7 @@ export class ClipService {
const currentCount = await this.clipsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ClipService.TooManyClipsError();
}
@@ -102,7 +102,7 @@ export class ClipService {
const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id,
});
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
throw new ClipService.TooManyClipNotesError();
}

View File

@@ -5,6 +5,14 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -56,7 +64,7 @@ import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
import { WebhookService } from './WebhookService.js';
import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
@@ -144,6 +152,8 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -193,10 +203,12 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -225,6 +237,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -258,6 +271,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -285,6 +299,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
AbuseReportService,
AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -334,10 +350,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
WebhookService,
UserWebhookService,
SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -366,6 +384,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -399,6 +418,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -422,6 +442,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AbuseReportService,
$AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -471,10 +493,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
$WebhookService,
$UserWebhookService,
$SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -503,6 +527,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
$AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -536,6 +561,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
$SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -560,6 +586,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
AbuseReportService,
AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -609,10 +637,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
WebhookService,
UserWebhookService,
SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -640,6 +670,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -673,6 +704,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -696,6 +728,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AbuseReportService,
$AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -745,10 +779,12 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
$WebhookService,
$UserWebhookService,
$SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -776,6 +812,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
$AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -809,6 +846,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
$SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,

View File

@@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import { IsNull, DataSource } from 'typeorm';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
@@ -38,7 +38,7 @@ export class CreateSystemUserService {
// Generate secret
const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair();
const keyPair = await genRSAAndEd25519KeyPair();
let account!: MiUser;
@@ -64,9 +64,8 @@ export class CreateSystemUserService {
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
await transactionalEntityManager.insert(MiUserKeypair, {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id,
...keyPair,
});
await transactionalEntityManager.insert(MiUserProfile, {

View File

@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -32,6 +33,7 @@ export class EmailService {
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}

View File

@@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
@bindThis
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
let noteIds: string[];
let shouldFallbackToDb = false;
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
@@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
const redisResultIds = Array.from(new Set(redisResult.flat(1)));
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
redisResultIds.sort(idCompare);
noteIds = redisResultIds.slice(0, ps.limit);
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
let noteIds = redisResultIds.slice(0, ps.limit);
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true);

View File

@@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
};
},
});

View File

@@ -15,6 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { REMOTE_SERVER_CACHE_TTL } from '@/const.js';
import type { DOMWindow } from 'jsdom';
type NodeInfo = {
@@ -24,6 +25,7 @@ type NodeInfo = {
version?: unknown;
};
metadata?: {
httpMessageSignaturesImplementationLevel?: unknown,
name?: unknown;
nodeName?: unknown;
nodeDescription?: unknown;
@@ -39,6 +41,7 @@ type NodeInfo = {
@Injectable()
export class FetchInstanceMetadataService {
private logger: Logger;
private httpColon = 'https://';
constructor(
private httpRequestService: HttpRequestService,
@@ -48,6 +51,7 @@ export class FetchInstanceMetadataService {
private redisClient: Redis.Redis,
) {
this.logger = this.loggerService.getLogger('metadata', 'cyan');
this.httpColon = process.env.MISSKEY_USE_HTTP?.toLowerCase() === 'true' ? 'http://' : 'https://';
}
@bindThis
@@ -59,7 +63,7 @@ export class FetchInstanceMetadataService {
return await this.redisClient.set(
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
'GET' // 古い値を返すなかったらnull
'GET', // 古い値を返すなかったらnull
);
}
@@ -73,23 +77,24 @@ export class FetchInstanceMetadataService {
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
const host = instance.host;
// finallyでunlockされてしまうのでtry内でロックチェックをしない
// returnであってもfinallyは実行される
if (!force && await this.tryLock(host) === '1') {
// 1が返ってきていたらロックされているという意味なので、何もしない
return;
if (!force) {
// キャッシュ有効チェックはロック取得前に行う
const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now();
if (_instance && _instance.infoUpdatedAt != null && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) {
this.logger.debug(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`);
return;
}
// finallyでunlockされてしまうのでtry内でロックチェックをしない
// returnであってもfinallyは実行される
if (await this.tryLock(host) === '1') {
// 1が返ってきていたら他にロックされているという意味なので、何もしない
return;
}
}
try {
if (!force) {
const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
// unlock at the finally caluse
return;
}
}
this.logger.info(`Fetching metadata of ${instance.host} ...`);
const [info, dom, manifest] = await Promise.all([
@@ -118,6 +123,14 @@ export class FetchInstanceMetadataService {
updates.openRegistrations = info.openRegistrations;
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel && (
info.metadata.httpMessageSignaturesImplementationLevel === '01' ||
info.metadata.httpMessageSignaturesImplementationLevel === '11'
)) {
updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel;
} else {
updates.httpMessageSignaturesImplementationLevel = '00';
}
}
if (name) updates.name = name;
@@ -129,6 +142,12 @@ export class FetchInstanceMetadataService {
await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
this.logger.debug('Updated metadata:', {
info: !!info,
dom: !!dom,
manifest: !!manifest,
updates,
});
} catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally {
@@ -141,7 +160,7 @@ export class FetchInstanceMetadataService {
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try {
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
const wellknown = await this.httpRequestService.getJson(this.httpColon + instance.host + '/.well-known/nodeinfo')
.catch(err => {
if (err.statusCode === 404) {
throw new Error('No nodeinfo provided');
@@ -184,7 +203,7 @@ export class FetchInstanceMetadataService {
private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> {
this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host;
const url = this.httpColon + instance.host;
const html = await this.httpRequestService.getHtml(url);
@@ -196,7 +215,7 @@ export class FetchInstanceMetadataService {
@bindThis
private async fetchManifest(instance: MiInstance): Promise<Record<string, unknown> | null> {
const url = 'https://' + instance.host;
const url = this.httpColon + instance.host;
const manifestUrl = url + '/manifest.json';
@@ -207,7 +226,7 @@ export class FetchInstanceMetadataService {
@bindThis
private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> {
const url = 'https://' + instance.host;
const url = this.httpColon + instance.host;
if (doc) {
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
@@ -234,12 +253,12 @@ export class FetchInstanceMetadataService {
@bindThis
private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
const url = 'https://' + instance.host;
const url = this.httpColon + instance.host;
return (new URL(manifest.icons[0].src, url)).href;
}
if (doc) {
const url = 'https://' + instance.host;
const url = this.httpColon + instance.host;
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const links = Array.from(doc.getElementsByTagName('link')).reverse();

View File

@@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -227,6 +228,9 @@ export interface InternalEventTypes {
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
systemWebhookCreated: MiSystemWebhook;
systemWebhookDeleted: MiSystemWebhook;
systemWebhookUpdated: MiSystemWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
@@ -241,6 +245,7 @@ export interface InternalEventTypes {
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
userKeypairUpdated: { userId: MiUser['id']; };
}
// name/messages(spec) pairs dictionary

View File

@@ -70,7 +70,7 @@ export class HttpRequestService {
localAddress: config.outgoingAddress,
});
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 16);
this.httpAgent = config.proxy
? new HttpProxyAgent({

View File

@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain, color, store);
public getLogger(domain: string, color?: KEYWORD | undefined) {
return new Logger(domain, color);
}
}

View File

@@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'mfm-js';
const treeAdapter = TreeAdapter.defaultTreeAdapter;
const treeAdapter = parse5.defaultTreeAdapter;
type Node = DefaultTreeAdapterMap['node'];
type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@@ -46,7 +48,7 @@ export class MfmService {
return text.trim();
function getText(node: TreeAdapter.Node): string {
function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n';
@@ -58,7 +60,7 @@ export class MfmService {
return '';
}
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) {
for (const n of childNodes) {
analyze(n);
@@ -66,14 +68,16 @@ export class MfmService {
}
}
function analyze(node: TreeAdapter.Node) {
function analyze(node: Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
return;
}
// Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return;
if (!treeAdapter.isElementNode(node)) {
return;
}
switch (node.nodeName) {
case 'br': {
@@ -81,8 +85,7 @@ export class MfmService {
break;
}
case 'a':
{
case 'a': {
const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
@@ -90,7 +93,7 @@ export class MfmService {
// ハッシュタグ
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt;
// メンション
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@');
@@ -102,7 +105,7 @@ export class MfmService {
} else if (part.length === 3) {
text += txt;
}
// その他
// その他
} else {
const generateLink = () => {
if (!href && !txt) {
@@ -130,8 +133,7 @@ export class MfmService {
break;
}
case 'h1':
{
case 'h1': {
text += '【';
appendChildren(node.childNodes);
text += '】\n';
@@ -139,16 +141,14 @@ export class MfmService {
}
case 'b':
case 'strong':
{
case 'strong': {
text += '**';
appendChildren(node.childNodes);
text += '**';
break;
}
case 'small':
{
case 'small': {
text += '<small>';
appendChildren(node.childNodes);
text += '</small>';
@@ -156,8 +156,7 @@ export class MfmService {
}
case 's':
case 'del':
{
case 'del': {
text += '~~';
appendChildren(node.childNodes);
text += '~~';
@@ -165,8 +164,7 @@ export class MfmService {
}
case 'i':
case 'em':
{
case 'em': {
text += '<i>';
appendChildren(node.childNodes);
text += '</i>';
@@ -207,8 +205,7 @@ export class MfmService {
case 'h3':
case 'h4':
case 'h5':
case 'h6':
{
case 'h6': {
text += '\n\n';
appendChildren(node.childNodes);
break;
@@ -221,8 +218,7 @@ export class MfmService {
case 'article':
case 'li':
case 'dt':
case 'dd':
{
case 'dd': {
text += '\n';
appendChildren(node.childNodes);
break;

View File

@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js';
import { QueueService } from '@/core/QueueService.js';
@@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -205,7 +204,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
private webhookService: WebhookService,
private webhookService: UserWebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -606,7 +605,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'note', {
this.queueService.userWebhookDeliver(webhook, 'note', {
note: noteObj,
});
}
@@ -633,7 +632,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'reply', {
this.queueService.userWebhookDeliver(webhook, 'reply', {
note: noteObj,
});
}
@@ -656,7 +655,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'renote', {
this.queueService.userWebhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
@@ -788,7 +787,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'mention', {
this.queueService.userWebhookDeliver(webhook, 'mention', {
note: detailPackedNote,
});
}
@@ -839,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
))).filter(isNotNull);
))).filter(x => x != null);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>

View File

@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
import {
DeliverJobData,
EndedPollNotificationJobData,
InboxJobData,
RelationshipJobData,
UserWebhookDeliverJobData,
SystemWebhookDeliverJobData,
} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
const $system: Provider = {
provide: 'queue:system',
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
inject: [DI.config],
};
const $webhookDeliver: Provider = {
provide: 'queue:webhookDeliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
const $userWebhookDeliver: Provider = {
provide: 'queue:userWebhookDeliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
inject: [DI.config],
};
const $systemWebhookDeliver: Provider = {
provide: 'queue:systemWebhookDeliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
$webhookDeliver,
$userWebhookDeliver,
$systemWebhookDeliver,
],
exports: [
$system,
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
$webhookDeliver,
$userWebhookDeliver,
$systemWebhookDeliver,
],
})
export class QueueModule implements OnApplicationShutdown {
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {}
public async dispose(): Promise<void> {
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
this.webhookDeliverQueue.close(),
this.userWebhookDeliverQueue.close(),
this.systemWebhookDeliverQueue.close(),
]);
}

View File

@@ -8,15 +8,32 @@ import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
import type {
DbJobData,
DeliverJobData,
RelationshipJobData,
SystemWebhookDeliverJobData,
ThinUser,
UserWebhookDeliverJobData,
} from '../queue/types.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
} from './QueueModule.js';
import { genRFC3230DigestHeader, type PrivateKeyWithPem, type ParsedSignature } from '@misskey-dev/node-http-message-signatures';
import type * as Bull from 'bullmq';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@@ -31,7 +48,8 @@ export class QueueService {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
@@ -71,21 +89,21 @@ export class QueueService {
}
@bindThis
public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKeyWithPem) {
if (content == null) return null;
if (to == null) return null;
const contentBody = JSON.stringify(content);
const digest = ApRequestCreator.createDigest(contentBody);
const data: DeliverJobData = {
user: {
id: user.id,
},
content: contentBody,
digest,
digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'),
to,
isSharedInbox,
privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem },
};
return this.deliverQueue.add(to, data, {
@@ -103,13 +121,13 @@ export class QueueService {
* @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
* @param content IActivity | null
* @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
* @param forceMainKey boolean | undefined, force to use main (rsa) key
* @returns void
*/
@bindThis
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>, privateKey?: PrivateKeyWithPem) {
if (content == null) return null;
const contentBody = JSON.stringify(content);
const digest = ApRequestCreator.createDigest(contentBody);
const opts = {
attempts: this.config.deliverJobMaxAttempts ?? 12,
@@ -125,9 +143,9 @@ export class QueueService {
data: {
user,
content: contentBody,
digest,
to: d[0],
isSharedInbox: d[1],
privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem },
} as DeliverJobData,
opts,
})));
@@ -136,7 +154,7 @@ export class QueueService {
}
@bindThis
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
public inbox(activity: IActivity, signature: ParsedSignature | null) {
const data = {
activity: activity,
signature,
@@ -431,9 +449,13 @@ export class QueueService {
});
}
/**
* @see UserWebhookDeliverJobData
* @see WebhookDeliverProcessorService
*/
@bindThis
public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
const data = {
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
const data: UserWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
@@ -444,7 +466,33 @@ export class QueueService {
eventId: randomUUID(),
};
return this.webhookDeliverQueue.add(webhook.id, data, {
return this.userWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',
},
removeOnComplete: true,
removeOnFail: true,
});
}
/**
* @see SystemWebhookDeliverJobData
* @see WebhookDeliverProcessorService
*/
@bindThis
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
const data: SystemWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
to: webhook.url,
secret: webhook.secret,
createdAt: Date.now(),
eventId: randomUUID(),
};
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',

View File

@@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@@ -117,11 +118,16 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
// Check if note is Renote
if (isRenote(note) && !isQuote(note)) {
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
}
let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764';
} else if (_reaction) {
} else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);

View File

@@ -16,6 +16,8 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { DI } from '@/di-symbols.js';
import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { UserKeypairService } from './UserKeypairService.js';
import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
const ACTOR_USERNAME = 'relay.actor' as const;
@@ -34,6 +36,7 @@ export class RelayService {
private queueService: QueueService,
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
private userKeypairService: UserKeypairService,
) {
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
}
@@ -111,7 +114,7 @@ export class RelayService {
}
@bindThis
public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise<void> {
public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKeyWithPem): Promise<void> {
if (activity == null) return;
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
@@ -121,11 +124,9 @@ export class RelayService {
const copy = deepClone(activity);
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
privateKey = privateKey ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id);
const signed = await this.apRendererService.attachLdSignature(copy, privateKey);
const signed = await this.apRendererService.attachLdSignature(copy, user);
for (const relay of relays) {
this.queueService.deliver(user, signed, relay.inbox, false);
}
this.queueService.deliverMany(user, signed, new Map(relays.map(({ inbox }) => [inbox, false])), privateKey);
}
}

View File

@@ -47,6 +47,7 @@ export type RolePolicies = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
@@ -410,14 +413,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)),
}) : [];
const moderatorRoles = includeAdmins
? roles.filter(r => r.isModerator || r.isAdministrator)
: roles.filter(r => r.isModerator);
// TODO: isRootなアカウントも含める
return assigns.map(a => a.userId);
const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: [];
const now = Date.now();
const result = [
// Setを経由して重複を除去ユーザIDは重複する可能性があるので
...new Set(
assigns
.filter(it =>
(excludeExpire)
? (it.expiresAt == null || it.expiresAt.getTime() > now)
: true,
)
.map(a => a.userId),
),
];
return result.sort((x, y) => x.localeCompare(y));
}
@bindThis

View File

@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { generateKeyPair } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import { DataSource, IsNull } from 'typeorm';
@@ -21,6 +20,7 @@ import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js';
@Injectable()
export class SignupService {
@@ -93,22 +93,7 @@ export class SignupService {
}
}
const keyPair = await new Promise<string[]>((res, rej) =>
generateKeyPair('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: undefined,
passphrase: undefined,
},
}, (err, publicKey, privateKey) =>
err ? rej(err) : res([publicKey, privateKey]),
));
const keyPair = await genRSAAndEd25519KeyPair();
let account!: MiUser;
@@ -131,9 +116,8 @@ export class SignupService {
}));
await transactionalEntityManager.save(new MiUserKeypair({
publicKey: keyPair[0],
privateKey: keyPair[1],
userId: account.id,
...keyPair,
}));
await transactionalEntityManager.save(new MiUserProfile({

View File

@@ -0,0 +1,233 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { IdService } from '@/core/IdService.js';
import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class SystemWebhookService implements OnApplicationShutdown {
private logger: Logger;
private activeSystemWebhooksFetched = false;
private activeSystemWebhooks: MiSystemWebhook[] = [];
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.systemWebhooksRepository)
private systemWebhooksRepository: SystemWebhooksRepository,
private idService: IdService,
private queueService: QueueService,
private moderationLogService: ModerationLogService,
private loggerService: LoggerService,
private globalEventService: GlobalEventService,
) {
this.redisForSub.on('message', this.onMessage);
this.logger = this.loggerService.getLogger('webhook');
}
@bindThis
public async fetchActiveSystemWebhooks() {
if (!this.activeSystemWebhooksFetched) {
this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
isActive: true,
});
this.activeSystemWebhooksFetched = true;
}
return this.activeSystemWebhooks;
}
/**
* SystemWebhook の一覧を取得する.
*/
@bindThis
public async fetchSystemWebhooks(params?: {
ids?: MiSystemWebhook['id'][];
isActive?: MiSystemWebhook['isActive'];
on?: MiSystemWebhook['on'];
}): Promise<MiSystemWebhook[]> {
const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
if (params) {
if (params.ids && params.ids.length > 0) {
query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
}
if (params.isActive !== undefined) {
query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
}
if (params.on && params.on.length > 0) {
query.andWhere(':on <@ systemWebhook.on', { on: params.on });
}
}
return query.getMany();
}
/**
* SystemWebhook を作成する.
*/
@bindThis
public async createSystemWebhook(
params: {
isActive: MiSystemWebhook['isActive'];
name: MiSystemWebhook['name'];
on: MiSystemWebhook['on'];
url: MiSystemWebhook['url'];
secret: MiSystemWebhook['secret'];
},
updater: MiUser,
): Promise<MiSystemWebhook> {
const id = this.idService.gen();
await this.systemWebhooksRepository.insert({
...params,
id,
});
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
this.moderationLogService
.log(updater, 'createSystemWebhook', {
systemWebhookId: webhook.id,
webhook: webhook,
})
.then();
return webhook;
}
/**
* SystemWebhook を更新する.
*/
@bindThis
public async updateSystemWebhook(
params: {
id: MiSystemWebhook['id'];
isActive: MiSystemWebhook['isActive'];
name: MiSystemWebhook['name'];
on: MiSystemWebhook['on'];
url: MiSystemWebhook['url'];
secret: MiSystemWebhook['secret'];
},
updater: MiUser,
): Promise<MiSystemWebhook> {
const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
await this.systemWebhooksRepository.update(beforeEntity.id, {
updatedAt: new Date(),
isActive: params.isActive,
name: params.name,
on: params.on,
url: params.url,
secret: params.secret,
});
const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
this.moderationLogService
.log(updater, 'updateSystemWebhook', {
systemWebhookId: beforeEntity.id,
before: beforeEntity,
after: afterEntity,
})
.then();
return afterEntity;
}
/**
* SystemWebhook を削除する.
*/
@bindThis
public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
await this.systemWebhooksRepository.delete(id);
this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
this.moderationLogService
.log(updater, 'deleteSystemWebhook', {
systemWebhookId: webhook.id,
webhook,
})
.then();
}
/**
* SystemWebhook をWebhook配送キューに追加する
* @see QueueService.systemWebhookDeliver
*/
@bindThis
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
const webhookEntity = typeof webhook === 'string'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
: webhook;
if (!webhookEntity || !webhookEntity.isActive) {
this.logger.info(`Webhook is not active or not found : ${webhook}`);
return;
}
if (!webhookEntity.on.includes(type)) {
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
return;
}
return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel !== 'internal') {
return;
}
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'systemWebhookCreated': {
if (body.isActive) {
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
}
break;
}
case 'systemWebhookUpdated': {
if (body.isActive) {
const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
} else {
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
}
} else {
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
}
break;
}
case 'systemWebhookDeleted': {
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
break;
}
default:
break;
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private webhookService: WebhookService,
private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}

View File

@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
@@ -331,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'follow', {
this.queueService.userWebhookDeliver(webhook, 'follow', {
user: packed,
});
}
@@ -345,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'followed', {
this.queueService.userWebhookDeliver(webhook, 'followed', {
user: packed,
});
}
@@ -398,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
@@ -740,7 +740,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packedFollowee,
});
}

View File

@@ -5,41 +5,184 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
import { genEd25519KeyPair, importPrivateKey, PrivateKey, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
import type { MiUser } from '@/models/User.js';
import type { UserKeypairsRepository } from '@/models/_.js';
import { RedisKVCache } from '@/misc/cache.js';
import { RedisKVCache, MemoryKVCache } from '@/misc/cache.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { webcrypto } from 'node:crypto';
@Injectable()
export class UserKeypairService implements OnApplicationShutdown {
private cache: RedisKVCache<MiUserKeypair>;
private keypairEntityCache: RedisKVCache<MiUserKeypair>;
private privateKeyObjectCache: MemoryKVCache<webcrypto.CryptoKey>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
private globalEventService: GlobalEventService,
private userEntityService: UserEntityService,
) {
this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
this.keypairEntityCache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity,
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
this.privateKeyObjectCache = new MemoryKVCache<webcrypto.CryptoKey>(1000 * 60 * 60 * 1);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
public async getUserKeypair(userId: MiUser['id']): Promise<MiUserKeypair> {
return await this.cache.fetch(userId);
return await this.keypairEntityCache.fetch(userId);
}
/**
* Get private key [Only PrivateKeyWithPem for queue data etc.]
* @param userIdOrHint user id or MiUserKeypair
* @param preferType
* If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists.
* Otherwise, main keypair will be returned.
* @returns
*/
@bindThis
public async getLocalUserPrivateKeyPem(
userIdOrHint: MiUser['id'] | MiUserKeypair,
preferType?: string,
): Promise<PrivateKeyWithPem> {
const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint;
if (
preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase()) &&
keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null
) {
return {
keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`,
privateKeyPem: keypair.ed25519PrivateKey,
};
}
return {
keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`,
privateKeyPem: keypair.privateKey,
};
}
/**
* Get private key [Only PrivateKey for ap request]
* Using cache due to performance reasons of `crypto.subtle.importKey`
* @param userIdOrHint user id, MiUserKeypair, or PrivateKeyWithPem
* @param preferType
* If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists.
* Otherwise, main keypair will be returned. (ignored if userIdOrHint is PrivateKeyWithPem)
* @returns
*/
@bindThis
public async getLocalUserPrivateKey(
userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem,
preferType?: string,
): Promise<PrivateKey> {
if (typeof userIdOrHint === 'object' && 'privateKeyPem' in userIdOrHint) {
// userIdOrHint is PrivateKeyWithPem
return {
keyId: userIdOrHint.keyId,
privateKey: await this.privateKeyObjectCache.fetch(userIdOrHint.keyId, async () => {
return await importPrivateKey(userIdOrHint.privateKeyPem);
}),
};
}
const userId = typeof userIdOrHint === 'string' ? userIdOrHint : userIdOrHint.userId;
const getKeypair = () => typeof userIdOrHint === 'string' ? this.getUserKeypair(userId) : userIdOrHint;
if (preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase())) {
const keyId = `${this.userEntityService.genLocalUserUri(userId)}#ed25519-key`;
const fetched = await this.privateKeyObjectCache.fetchMaybe(keyId, async () => {
const keypair = await getKeypair();
if (keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) {
return await importPrivateKey(keypair.ed25519PrivateKey);
}
return;
});
if (fetched) {
return {
keyId,
privateKey: fetched,
};
}
}
const keyId = `${this.userEntityService.genLocalUserUri(userId)}#main-key`;
return {
keyId,
privateKey: await this.privateKeyObjectCache.fetch(keyId, async () => {
const keypair = await getKeypair();
return await importPrivateKey(keypair.privateKey);
}),
};
}
@bindThis
public async refresh(userId: MiUser['id']): Promise<void> {
return await this.keypairEntityCache.refresh(userId);
}
/**
* If DB has ed25519 keypair, refresh cache and return it.
* If not, create, save and return ed25519 keypair.
* @param userId user id
* @returns MiUserKeypair if keypair is created, void if keypair is already exists
*/
@bindThis
public async refreshAndPrepareEd25519KeyPair(userId: MiUser['id']): Promise<MiUserKeypair | void> {
await this.refresh(userId);
const keypair = await this.keypairEntityCache.fetch(userId);
if (keypair.ed25519PublicKey != null) {
return;
}
const ed25519 = await genEd25519KeyPair();
await this.userKeypairsRepository.update({ userId }, {
ed25519PublicKey: ed25519.publicKey,
ed25519PrivateKey: ed25519.privateKey,
});
this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId });
const result = {
...keypair,
ed25519PublicKey: ed25519.publicKey,
ed25519PrivateKey: ed25519.privateKey,
};
this.keypairEntityCache.set(userId, result);
return result;
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'userKeypairUpdated': {
this.refresh(body.userId);
break;
}
}
}
}
@bindThis
public dispose(): void {
this.cache.dispose();
this.keypairEntityCache.dispose();
}
@bindThis

View File

@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id,
});
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError();
}

View File

@@ -0,0 +1,205 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import type { Config } from '@/config.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js';
function defaultActiveThreshold() {
return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
}
@Injectable()
export class UserSearchService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
) {
}
/**
* ユーザ名とホスト名によるユーザ検索を行う.
*
* - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
* 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
* 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
* 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
* 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
* - ログインしていない場合は、以下の順序で検索が行われる.
* 1. 一定期間以内に更新されたユーザ
* 2. 一定期間以内に更新されていないユーザ
* - それぞれの検索結果はユーザ名の昇順でソートされる.
* - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
* 1で既にヒットしていた場合、2, 3, 4でヒットしても無視される
* - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
* - ユーザ名の検索は大文字小文字を区別しない.
* - ホスト名の検索は大文字小文字を区別しない.
* - 検索結果は最大で {@link opts.limit} 件までとなる.
*
* ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
*
* @param params 検索条件.
* @param opts 関数の動作を制御するオプション.
* @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
* @see {@link UserSearchService#buildSearchUserQueries}
* @see {@link UserSearchService#buildSearchUserNoLoginQueries}
*/
@bindThis
public async search(
params: {
username?: string | null,
host?: string | null,
activeThreshold?: Date,
},
opts?: {
limit?: number,
detail?: boolean,
},
me?: MiUser | null,
): Promise<Packed<'User'>[]> {
const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
let resultSet = new Set<MiUser['id']>();
const limit = opts?.limit ?? 10;
for (const query of queries) {
const ids = await query
.select('user.id')
.limit(limit - resultSet.size)
.orderBy('user.usernameLower', 'ASC')
.getRawMany<{ user_id: MiUser['id'] }>()
.then(res => res.map(x => x.user_id));
resultSet = new Set([...resultSet, ...ids]);
if (resultSet.size >= limit) {
break;
}
}
return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
[...resultSet].slice(0, limit),
me,
{ schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
);
}
/**
* ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
* @param me
* @param params
* @private
*/
@bindThis
private buildSearchUserQueries(
me: MiUser,
params: {
username?: string | null,
host?: string | null,
activeThreshold?: Date,
},
) {
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
}));
inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
// 自分自身がヒットするとしたらここ
const activeUserQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
.andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
activeUserQuery.setParameters(followingUserQuery.getParameters());
const inactiveUserQuery = this.generateUserQueryBuilder(params)
.andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
inactiveUserQuery.setParameters(followingUserQuery.getParameters());
return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
}
/**
* ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
* @param params
* @private
*/
@bindThis
private buildSearchUserNoLoginQueries(params: {
username?: string | null,
host?: string | null,
activeThreshold?: Date,
}) {
// デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
const activeUserQuery = this.generateUserQueryBuilder(params)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
}));
const inactiveUserQuery = this.generateUserQueryBuilder(params)
.andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
return [activeUserQuery, inactiveUserQuery];
}
/**
* ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
* @param params
* @private
*/
@bindThis
private generateUserQueryBuilder(params: {
username?: string | null,
host?: string | null,
}): SelectQueryBuilder<MiUser> {
const userQuery = this.usersRepository.createQueryBuilder('user');
if (params.username) {
userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
}
if (params.host) {
if (params.host === this.config.hostname || params.host === '.') {
userQuery.andWhere('user.host IS NULL');
} else {
userQuery.andWhere('user.host LIKE :host', {
host: sqlLikeEscape(params.host.toLowerCase()) + '%',
});
}
}
userQuery.andWhere('user.isSuspended = FALSE');
return userQuery;
}
}

View File

@@ -3,27 +3,23 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository } from '@/models/_.js';
import { Injectable } from '@nestjs/common';
import type { MiUser } from '@/models/User.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { UserKeypairService } from './UserKeypairService.js';
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
@Injectable()
export class UserSuspendService {
constructor(
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private userKeypairService: UserKeypairService,
private apDeliverManagerService: ApDeliverManagerService,
) {
}
@@ -32,28 +28,12 @@ export class UserSuspendService {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = [];
const followings = await this.followingsRepository.find({
where: [
{ followerSharedInbox: Not(IsNull()) },
{ followeeSharedInbox: Not(IsNull()) },
],
select: ['followerSharedInbox', 'followeeSharedInbox'],
});
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
}
for (const inbox of queue) {
this.queueService.deliver(user, content, inbox, true);
}
const manager = this.apDeliverManagerService.createDeliverManager(user, content);
manager.addAllKnowingSharedInboxRecipe();
// process deliver時にはキーペアが消去されているはずなので、ここで挿入する
const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main');
manager.execute({ privateKey });
}
}
@@ -62,28 +42,12 @@ export class UserSuspendService {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにUndo Delete配信
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
const queue: string[] = [];
const followings = await this.followingsRepository.find({
where: [
{ followerSharedInbox: Not(IsNull()) },
{ followeeSharedInbox: Not(IsNull()) },
],
select: ['followerSharedInbox', 'followeeSharedInbox'],
});
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
}
for (const inbox of queue) {
this.queueService.deliver(user as any, content, inbox, true);
}
const manager = this.apDeliverManagerService.createDeliverManager(user, content);
manager.addAllKnowingSharedInboxRecipe();
// process deliver時にはキーペアが消去されているはずなので、ここで挿入する
const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main');
manager.execute({ privateKey });
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { WebhooksRepository } from '@/models/_.js';
import type { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserWebhookService implements OnApplicationShutdown {
private activeWebhooksFetched = false;
private activeWebhooks: MiWebhook[] = [];
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
) {
this.redisForSub.on('message', this.onMessage);
}
@bindThis
public async getActiveWebhooks() {
if (!this.activeWebhooksFetched) {
this.activeWebhooks = await this.webhooksRepository.findBy({
active: true,
});
this.activeWebhooksFetched = true;
}
return this.activeWebhooks;
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel !== 'internal') {
return;
}
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'webhookCreated': {
if (body.active) {
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
break;
}
case 'webhookUpdated': {
if (body.active) {
const i = this.activeWebhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
};
} else {
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
} else {
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
}
break;
}
case 'webhookDeleted': {
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
break;
}
default:
break;
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -46,7 +46,7 @@ export class WebfingerService {
const m = query.match(mRegex);
if (m) {
const hostname = m[2];
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
const useHttp = process.env.MISSKEY_USE_HTTP && process.env.MISSKEY_USE_HTTP.toLowerCase() === 'true';
return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`;
}

View File

@@ -1,97 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { WebhooksRepository } from '@/models/_.js';
import type { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class WebhookService implements OnApplicationShutdown {
private webhooksFetched = false;
private webhooks: MiWebhook[] = [];
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
) {
//this.onMessage = this.onMessage.bind(this);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
public async getActiveWebhooks() {
if (!this.webhooksFetched) {
this.webhooks = await this.webhooksRepository.findBy({
active: true,
});
this.webhooksFetched = true;
}
return this.webhooks;
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'webhookCreated':
if (body.active) {
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
break;
case 'webhookUpdated':
if (body.active) {
const i = this.webhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
};
} else {
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body,
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
user: null, // joinなカラムは通常取ってこないので
});
}
} else {
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
}
break;
case 'webhookDeleted':
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
break;
default:
break;
}
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js';
@@ -41,7 +40,7 @@ export class ApAudienceService {
const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter(isNotNull);
)).filter(x => x != null);
if (toGroups.public.length > 0) {
return {

View File

@@ -5,7 +5,7 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
import type { MiUser, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import { MemoryKVCache } from '@/misc/cache.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
@@ -13,9 +13,12 @@ import { CacheService } from '@/core/CacheService.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import Logger from '@/logger.js';
import { getApId } from './type.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApLoggerService } from './ApLoggerService.js';
import type { IObject } from './type.js';
import { UtilityService } from '../UtilityService.js';
export type UriParseResult = {
/** wether the URI was generated by us */
@@ -35,8 +38,8 @@ export type UriParseResult = {
@Injectable()
export class ApDbResolverService implements OnApplicationShutdown {
private publicKeyCache: MemoryKVCache<MiUserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey[] | null>;
private logger: Logger;
constructor(
@Inject(DI.config)
@@ -53,9 +56,17 @@ export class ApDbResolverService implements OnApplicationShutdown {
private cacheService: CacheService,
private apPersonService: ApPersonService,
private apLoggerService: ApLoggerService,
private utilityService: UtilityService,
) {
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey[] | null>(Infinity);
this.logger = this.apLoggerService.logger.createSubLogger('db-resolver');
}
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;
}
@bindThis
@@ -116,62 +127,141 @@ export class ApDbResolverService implements OnApplicationShutdown {
}
}
/**
* AP KeyId => Misskey User and Key
*/
@bindThis
public async getAuthUserFromKeyId(keyId: string): Promise<{
user: MiRemoteUser;
key: MiUserPublickey;
} | null> {
const key = await this.publicKeyCache.fetch(keyId, async () => {
const key = await this.userPublickeysRepository.findOneBy({
keyId,
});
if (key == null) return null;
return key;
}, key => key != null);
if (key == null) return null;
const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null;
if (user == null) return null;
if (user.isDeleted) return null;
return {
user,
key,
};
private async refreshAndFindKey(userId: MiUser['id'], keyId: string): Promise<MiUserPublickey | null> {
this.refreshCacheByUserId(userId);
const keys = await this.getPublicKeyByUserId(userId);
if (keys == null || !Array.isArray(keys) || keys.length === 0) {
this.logger.warn(`No key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`);
return null;
}
const exactKey = keys.find(x => x.keyId === keyId);
if (exactKey) return exactKey;
this.logger.warn(`No exact key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`);
return null;
}
/**
* AP Actor id => Misskey User and Key
* @param uri AP Actor id
* @param keyId Key id to find. If not specified, main key will be selected.
* @returns
* 1. `null` if the user and key host do not match
* 2. `{ user: null, key: null }` if the user is not found
* 3. `{ user: MiRemoteUser, key: null }` if key is not found
* 4. `{ user: MiRemoteUser, key: MiUserPublickey }` if both are found
*/
@bindThis
public async getAuthUserFromApId(uri: string): Promise<{
public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{
user: MiRemoteUser;
key: MiUserPublickey | null;
} | null> {
const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser;
if (user.isDeleted) return null;
} | {
user: null;
key: null;
} |
null> {
if (keyId) {
if (this.punyHost(uri) !== this.punyHost(keyId)) {
/**
* keyIdはURL形式かつkeyIdのホストはuriのホストと一致するはず
* ApPersonService.validateActorに由来
*
* ただ、Mastodonはリプライ関連で他人のトゥートをHTTP Signature署名して送ってくることがある
* そのような署名は有効性に疑問があるので無視することにする
* ここではuriとkeyIdのホストが一致しない場合は無視する
* ハッシュをなくしたkeyIdとuriの同一性を比べてみてもいいが、`uri#*-key`というkeyIdを設定するのが
* 決まりごとというわけでもないため幅を持たせることにする
*
*
* The keyId should be in URL format and its host should match the host of the uri
* (derived from ApPersonService.validateActor)
*
* However, Mastodon sometimes sends toots from other users with HTTP Signature signing for reply-related purposes
* Such signatures are of questionable validity, so we choose to ignore them
* Here, we ignore cases where the hosts of uri and keyId do not match
* We could also compare the equality of keyId without the hash and uri, but since setting a keyId like `uri#*-key`
* is not a strict rule, we decide to allow for some flexibility
*/
this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`);
return null;
}
}
const key = await this.publicKeyByUserIdCache.fetch(
user.id,
() => this.userPublickeysRepository.findOneBy({ userId: user.id }),
const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser;
if (user.isDeleted) return { user: null, key: null };
const keys = await this.getPublicKeyByUserId(user.id);
if (keys == null || !Array.isArray(keys) || keys.length === 0) {
this.logger.warn(`No key found uri=${uri} userId=${user.id} keys=${JSON.stringify(keys)}`);
return { user, key: null };
}
if (!keyId) {
// Choose the main-like
const mainKey = keys.find(x => {
try {
const url = new URL(x.keyId);
const path = url.pathname.split('/').pop()?.toLowerCase();
if (url.hash) {
if (url.hash.toLowerCase().includes('main')) {
return true;
}
} else if (path?.includes('main') || path === 'publickey') {
return true;
}
} catch { /* noop */ }
return false;
});
return { user, key: mainKey ?? keys[0] };
}
const exactKey = keys.find(x => x.keyId === keyId);
if (exactKey) return { user, key: exactKey };
/**
* keyIdで見つからない場合、まずはキャッシュを更新して再取得
* If not found with keyId, update cache and reacquire
*/
const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id);
if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) {
const exactKey = await this.refreshAndFindKey(user.id, keyId);
if (exactKey) return { user, key: exactKey };
}
/**
* lastFetchedAtでの更新制限を弱めて再取得
* Reacquisition with weakened update limit at lastFetchedAt
*/
if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) {
this.logger.info(`Fetching user to find public key uri=${uri} userId=${user.id} keyId=${keyId}`);
const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0);
if (renewed == null || renewed.isDeleted) return null;
return { user, key: await this.refreshAndFindKey(user.id, keyId) };
}
this.logger.warn(`No key found uri=${uri} userId=${user.id} keyId=${keyId}`);
return { user, key: null };
}
@bindThis
public async getPublicKeyByUserId(userId: MiUser['id']): Promise<MiUserPublickey[] | null> {
return await this.publicKeyByUserIdCache.fetch(
userId,
() => this.userPublickeysRepository.find({ where: { userId } }),
v => v != null,
);
}
return {
user,
key,
};
@bindThis
public refreshCacheByUserId(userId: MiUser['id']): void {
this.publicKeyByUserIdCache.delete(userId);
}
@bindThis
public dispose(): void {
this.publicKeyCache.dispose();
this.publicKeyByUserIdCache.dispose();
}

View File

@@ -9,10 +9,14 @@ import { DI } from '@/di-symbols.js';
import type { FollowingsRepository } from '@/models/_.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { QueueService } from '@/core/QueueService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import type { IActivity } from '@/core/activitypub/type.js';
import { ThinUser } from '@/queue/types.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import type Logger from '@/logger.js';
import { UserKeypairService } from '../UserKeypairService.js';
import { ApLoggerService } from './ApLoggerService.js';
import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
interface IRecipe {
type: string;
@@ -27,12 +31,19 @@ interface IDirectRecipe extends IRecipe {
to: MiRemoteUser;
}
interface IAllKnowingSharedInboxRecipe extends IRecipe {
type: 'AllKnowingSharedInbox';
}
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
recipe.type === 'Followers';
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
recipe.type === 'Direct';
const isAllKnowingSharedInbox = (recipe: IRecipe): recipe is IAllKnowingSharedInboxRecipe =>
recipe.type === 'AllKnowingSharedInbox';
class DeliverManager {
private actor: ThinUser;
private activity: IActivity | null;
@@ -40,16 +51,18 @@ class DeliverManager {
/**
* Constructor
* @param userEntityService
* @param userKeypairService
* @param followingsRepository
* @param queueService
* @param actor Actor
* @param activity Activity to deliver
*/
constructor(
private userEntityService: UserEntityService,
private userKeypairService: UserKeypairService,
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
private accountUpdateService: AccountUpdateService,
private logger: Logger,
actor: { id: MiUser['id']; host: null; },
activity: IActivity | null,
@@ -91,6 +104,18 @@ class DeliverManager {
this.addRecipe(recipe);
}
/**
* Add recipe for all-knowing shared inbox deliver
*/
@bindThis
public addAllKnowingSharedInboxRecipe(): void {
const deliver: IAllKnowingSharedInboxRecipe = {
type: 'AllKnowingSharedInbox',
};
this.addRecipe(deliver);
}
/**
* Add recipe
* @param recipe Recipe
@@ -104,11 +129,44 @@ class DeliverManager {
* Execute delivers
*/
@bindThis
public async execute(): Promise<void> {
public async execute(opts?: { privateKey?: PrivateKeyWithPem }): Promise<void> {
//#region MIGRATION
if (!opts?.privateKey) {
/**
* ed25519の署名がなければ追加する
*/
const created = await this.userKeypairService.refreshAndPrepareEd25519KeyPair(this.actor.id);
if (created) {
// createdが存在するということは新規作成されたということなので、フォロワーに配信する
this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`);
// リモートに配信
const keyPair = await this.userKeypairService.getLocalUserPrivateKeyPem(created, 'main');
await this.accountUpdateService.publishToFollowers(this.actor.id, keyPair);
}
}
//#endregion
//#region collect inboxes by recipes
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
if (this.recipes.some(r => isAllKnowingSharedInbox(r))) {
// all-knowing shared inbox
const followings = await this.followingsRepository.find({
where: [
{ followerSharedInbox: Not(IsNull()) },
{ followeeSharedInbox: Not(IsNull()) },
],
select: ['followerSharedInbox', 'followeeSharedInbox'],
});
for (const following of followings) {
if (following.followeeSharedInbox) inboxes.set(following.followeeSharedInbox, true);
if (following.followerSharedInbox) inboxes.set(following.followerSharedInbox, true);
}
}
// build inbox list
// Process follower recipes first to avoid duplication when processing direct recipes later.
if (this.recipes.some(r => isFollowers(r))) {
@@ -142,39 +200,49 @@ class DeliverManager {
inboxes.set(recipe.to.inbox, false);
}
//#endregion
// deliver
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
await this.queueService.deliverMany(this.actor, this.activity, inboxes, opts?.privateKey);
this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`);
}
}
@Injectable()
export class ApDeliverManagerService {
private logger: Logger;
constructor(
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private userKeypairService: UserKeypairService,
private queueService: QueueService,
private accountUpdateService: AccountUpdateService,
private apLoggerService: ApLoggerService,
) {
this.logger = this.apLoggerService.logger.createSubLogger('deliver-manager');
}
/**
* Deliver activity to followers
* @param actor
* @param activity Activity
* @param forceMainKey Force to use main (rsa) key
*/
@bindThis
public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKeyWithPem): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.userKeypairService,
this.followingsRepository,
this.queueService,
this.accountUpdateService,
this.logger,
actor,
activity,
);
manager.addFollowersRecipe();
await manager.execute();
await manager.execute({ privateKey });
}
/**
@@ -186,9 +254,11 @@ export class ApDeliverManagerService {
@bindThis
public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise<void> {
const manager = new DeliverManager(
this.userEntityService,
this.userKeypairService,
this.followingsRepository,
this.queueService,
this.accountUpdateService,
this.logger,
actor,
activity,
);
@@ -199,10 +269,11 @@ export class ApDeliverManagerService {
@bindThis
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager(
this.userEntityService,
this.userKeypairService,
this.followingsRepository,
this.queueService,
this.accountUpdateService,
this.logger,
actor,
activity,
);

View File

@@ -27,8 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -57,9 +57,6 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -68,6 +65,7 @@ export class ApInboxService {
private utilityService: UtilityService,
private idService: IdService,
private metaService: MetaService,
private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
private reactionService: ReactionService,
@@ -116,15 +114,8 @@ export class ApInboxService {
result = await this.performOneActivity(actor, activity);
}
// ついでにリモートユーザーの情報が古かったら更新しておく
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
this.apPersonService.updatePerson(actor.uri);
});
}
}
return result;
// ついでにリモートユーザーの情報が古かったら更新しておく?
// → No, この関数が呼び出される前に署名検証で更新されているはず
}
@bindThis
@@ -539,20 +530,19 @@ export class ApInboxService {
const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
.filter(isNotNull);
.filter(x => x != null);
const users = await this.usersRepository.findBy({
id: In(userIds),
});
if (users.length < 1) return 'skip';
await this.abuseUserReportsRepository.insert({
id: this.idService.gen(),
await this.abuseReportService.report([{
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
});
}]);
return 'ok';
}

View File

@@ -25,7 +25,7 @@ export class ApMfmService {
}
@bindThis
public getNoteHtml(note: MiNote, apAppend?: string) {
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '') + (apAppend ?? '');

View File

@@ -22,16 +22,15 @@ import { UserKeypairService } from '@/core/UserKeypairService.js';
import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
@Injectable()
export class ApRendererService {
@@ -252,15 +251,15 @@ export class ApRendererService {
}
@bindThis
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey {
return {
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
id: `${this.userEntityService.genLocalUserUri(user.id)}${postfix ?? '/publickey'}`,
type: 'Key',
owner: this.userEntityService.genLocalUserUri(user.id),
publicKeyPem: createPublicKey(key.publicKey).export({
publicKeyPem: createPublicKey(publicKey).export({
type: 'spki',
format: 'pem',
}),
}) as string,
};
}
@@ -317,7 +316,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
};
let inReplyTo;
@@ -500,7 +499,10 @@ export class ApRendererService {
tag,
manuallyApprovesFollowers: user.isLocked,
discoverable: user.isExplorable,
publicKey: this.renderKey(user, keypair, '#main-key'),
publicKey: this.renderKey(user, keypair.publicKey, '#main-key'),
additionalPublicKeys: [
...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key')] : []),
],
isCat: user.isCat,
attachment: attachment.length ? attachment : undefined,
};
@@ -623,12 +625,10 @@ export class ApRendererService {
}
@bindThis
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
public async attachLdSignature(activity: any, key: PrivateKeyWithPem): Promise<IActivity> {
const jsonLd = this.jsonLdService.use();
jsonLd.debug = false;
activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
activity = await jsonLd.signRsaSignature2017(activity, key.privateKeyPem, key.keyId);
return activity;
}
@@ -686,7 +686,7 @@ export class ApRendererService {
if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
return emojis;
}

View File

@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { genRFC3230DigestHeader, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
@@ -15,122 +15,61 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import type { PrivateKeyWithPem, PrivateKey } from '@misskey-dev/node-http-message-signatures';
type Request = {
url: string;
method: string;
headers: Record<string, string>;
};
export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record<string, string> }) {
const u = new URL(args.url);
const request = {
url: u.href,
method: 'POST',
headers: {
'Date': new Date().toUTCString(),
'Host': u.host,
'Content-Type': 'application/activity+json',
...args.additionalHeaders,
} as Record<string, string>,
};
type Signed = {
request: Request;
signingString: string;
signature: string;
signatureHeader: string;
};
// TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする
const digestHeader = args.digest ?? await genRFC3230DigestHeader(args.body, 'SHA-256');
request.headers['Digest'] = digestHeader;
type PrivateKey = {
privateKeyPem: string;
keyId: string;
};
const result = await signAsDraftToRequest(
request,
args.key,
['(request-target)', 'date', 'host', 'digest'],
);
export class ApRequestCreator {
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const digestHeader = args.digest ?? this.createDigest(args.body);
return {
request,
...result,
};
}
const request: Request = {
url: u.href,
method: 'POST',
headers: this.#objectAssignWithLcKey({
'Date': new Date().toUTCString(),
'Host': u.host,
'Content-Type': 'application/activity+json',
'Digest': digestHeader,
}, args.additionalHeaders),
};
export async function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record<string, string> }) {
const u = new URL(args.url);
const request = {
url: u.href,
method: 'GET',
headers: {
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
...args.additionalHeaders,
} as Record<string, string>,
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
// TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする
const result = await signAsDraftToRequest(
request,
args.key,
['(request-target)', 'date', 'host', 'accept'],
);
return {
request,
signingString: result.signingString,
signature: result.signature,
signatureHeader: result.signatureHeader,
};
}
static createDigest(body: string) {
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
}
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
const u = new URL(args.url);
const request: Request = {
url: u.href,
method: 'GET',
headers: this.#objectAssignWithLcKey({
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
return {
request,
signingString: result.signingString,
signature: result.signature,
signatureHeader: result.signatureHeader,
};
}
static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
const signingString = this.#genSigningString(request, includeHeaders);
const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
request.headers = this.#objectAssignWithLcKey(request.headers, {
Signature: signatureHeader,
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
delete request.headers['host'];
return {
request,
signingString,
signature,
signatureHeader,
};
}
static #genSigningString(request: Request, includeHeaders: string[]): string {
request.headers = this.#lcObjectKey(request.headers);
const results: string[] = [];
for (const key of includeHeaders.map(x => x.toLowerCase())) {
if (key === '(request-target)') {
results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
} else {
results.push(`${key}: ${request.headers[key]}`);
}
}
return results.join('\n');
}
static #lcObjectKey(src: Record<string, string>): Record<string, string> {
const dst: Record<string, string> = {};
for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
return dst;
}
static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
}
return {
request,
...result,
};
}
@Injectable()
@@ -150,21 +89,28 @@ export class ApRequestService {
}
@bindThis
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise<void> {
const body = typeof object === 'string' ? object : JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedPost({
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
},
const keyFetched = await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level);
const req = await createSignedPost({
level,
key: keyFetched,
url,
body,
digest,
additionalHeaders: {
'User-Agent': this.config.userAgent,
},
digest,
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
delete req.request.headers['Host'];
this.logger.debug('create signed post', {
version: 'draft',
level,
url,
keyId: keyFetched.keyId,
});
await this.httpRequestService.send(url, {
@@ -180,19 +126,27 @@ export class ApRequestService {
* @param url URL to fetch
*/
@bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
},
public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> {
const key = await this.userKeypairService.getLocalUserPrivateKey(user.id, level);
const req = await createSignedGet({
level,
key,
url,
additionalHeaders: {
'User-Agent': this.config.userAgent,
},
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
delete req.request.headers['Host'];
this.logger.debug('create signed get', {
version: 'draft',
level,
url,
keyId: key.keyId,
});
const res = await this.httpRequestService.send(url, {
method: req.request.method,
headers: req.request.headers,

View File

@@ -16,6 +16,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
@@ -41,6 +42,7 @@ export class Resolver {
private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private federatedInstanceService: FederatedInstanceService,
private loggerService: LoggerService,
private recursionLimit = 100,
) {
@@ -103,8 +105,10 @@ export class Resolver {
this.user = await this.instanceActorService.getInstanceActor();
}
const server = await this.federatedInstanceService.fetch(host);
const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject
? await this.apRequestService.signedGet(value, this.user, server.httpMessageSignaturesImplementationLevel) as IObject
: await this.httpRequestService.getActivityJson(value)) as IObject;
if (
@@ -200,6 +204,7 @@ export class ApResolverService {
private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private federatedInstanceService: FederatedInstanceService,
private loggerService: LoggerService,
) {
}
@@ -220,6 +225,7 @@ export class ApResolverService {
this.httpRequestService,
this.apRendererService,
this.apDbResolverService,
this.federatedInstanceService,
this.loggerService,
);
}

View File

@@ -134,6 +134,7 @@ const security_v1 = {
'privateKey': { '@id': 'sec:privateKey', '@type': '@id' },
'privateKeyPem': 'sec:privateKeyPem',
'publicKey': { '@id': 'sec:publicKey', '@type': '@id' },
'additionalPublicKeys': { '@id': 'sec:publicKey', '@type': '@id' },
'publicKeyBase58': 'sec:publicKeyBase58',
'publicKeyPem': 'sec:publicKeyPem',
'publicKeyWif': 'sec:publicKeyWif',

View File

@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js';
@@ -28,7 +27,7 @@ export class ApMentionService {
const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter(isNotNull);
)).filter(x => x != null);
return mentionedUsers;
}

View File

@@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@@ -253,7 +252,7 @@ export class ApNoteService {
}
};
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);

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