1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-27 06:18:46 +09:00

Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2024-09-24 13:38:38 +09:00
commit 22b04f2ab2
199 changed files with 7083 additions and 7198 deletions

View File

@ -103,6 +103,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForReactions:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -106,6 +106,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForReactions:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -172,6 +172,16 @@ redis:
# # You can specify more ioredis options... # # You can specify more ioredis options...
# #username: example-username # #username: example-username
#redisForReactions:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# # You can specify more ioredis options...
# #username: example-username
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -103,6 +103,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForReactions:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -21,7 +21,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

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

View File

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

View File

@ -48,13 +48,15 @@ jobs:
"packages/backend/migration" "packages/backend/migration"
"packages/backend/src" "packages/backend/src"
"packages/backend/test" "packages/backend/test"
"packages/frontend-shared/src" "packages/frontend-shared/@types"
"packages/frontend-shared/js"
"packages/frontend/.storybook" "packages/frontend/.storybook"
"packages/frontend/@types" "packages/frontend/@types"
"packages/frontend/lib" "packages/frontend/lib"
"packages/frontend/public" "packages/frontend/public"
"packages/frontend/src" "packages/frontend/src"
"packages/frontend/test" "packages/frontend/test"
"packages/frontend-embed/@types"
"packages/frontend-embed/src" "packages/frontend-embed/src"
"packages/misskey-bubble-game/src" "packages/misskey-bubble-game/src"
"packages/misskey-reversi/src" "packages/misskey-reversi/src"

View File

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

View File

@ -12,6 +12,8 @@ on:
- packages/frontend-embed/** - packages/frontend-embed/**
- packages/sw/** - packages/sw/**
- packages/cherrypick-js/** - packages/cherrypick-js/**
- packages/misskey-bubble-game/**
- packages/misskey-reversi/**
- packages/shared/eslint.config.js - packages/shared/eslint.config.js
- .github/workflows/lint.yml - .github/workflows/lint.yml
pull_request: pull_request:
@ -22,6 +24,8 @@ on:
- packages/frontend-embed/** - packages/frontend-embed/**
- packages/sw/** - packages/sw/**
- packages/cherrypick-js/** - packages/cherrypick-js/**
- packages/misskey-bubble-game/**
- packages/misskey-reversi/**
- packages/shared/eslint.config.js - packages/shared/eslint.config.js
- .github/workflows/lint.yml - .github/workflows/lint.yml
jobs: jobs:
@ -33,7 +37,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.3 - uses: actions/setup-node@v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -53,6 +57,8 @@ jobs:
- frontend-embed - frontend-embed
- sw - sw
- cherrypick-js - cherrypick-js
- misskey-bubble-game
- misskey-reversi
env: env:
eslint-cache-version: v1 eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
@ -62,7 +68,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.3 - uses: actions/setup-node@v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -92,7 +98,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.3 - uses: actions/setup-node@v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -19,7 +19,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.3 - uses: actions/setup-node@v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

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

View File

@ -70,8 +70,16 @@ jobs:
- id: out-diff - id: out-diff
name: Build diff Comment name: Build diff Comment
run: | run: |
cat <<- EOF > ./output.md HEADER="이 PR에 의한 api.json 차이"
이 PR에 의한 api.json 차이 FOOTER="[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
DIFF_BYTES="$(stat ./api.json.diff -c '%s' | tr -d '\n')"
echo "$HEADER" > ./output.md
if (( "$DIFF_BYTES" <= 1 )); then
echo '차이점이 없습니다.' >> ./output.md
else
cat <<- EOF >> ./output.md
<details> <details>
<summary>차이점은 여기에서 볼 수 있음</summary> <summary>차이점은 여기에서 볼 수 있음</summary>
@ -79,9 +87,10 @@ jobs:
$(cat ./api.json.diff) $(cat ./api.json.diff)
\`\`\` \`\`\`
</details> </details>
[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
EOF EOF
fi
echo "$FOOTER" >> ./output.md
- uses: thollander/actions-comment-pull-request@v2 - uses: thollander/actions-comment-pull-request@v2
with: with:
pr_number: ${{ steps.load-pr-num.outputs.pr-number }} pr_number: ${{ steps.load-pr-num.outputs.pr-number }}

View File

@ -41,7 +41,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -46,7 +46,7 @@ jobs:
- name: Install FFmpeg - name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -93,7 +93,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -31,7 +31,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

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

View File

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

View File

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

View File

@ -2,26 +2,36 @@
### General ### General
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) - Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
### Client ### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように - Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
- Enhance: アイコンデコレーション管理画面にプレビューを追加 - Enhance: アイコンデコレーション管理画面にプレビューを追加
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく - Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
- Enhance: ScratchpadにUIインスペクターを追加 - Enhance: ScratchpadにUIインスペクターを追加
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正 - Fix: 月の違う同じ日はセパレータが表示されないのを修正
- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/265)
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正 - Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正 - Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110)
### Server ### Server
- Feat: Misskey® Reactions Buffering Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 - Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 - Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624)
- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634)
## 2024.8.0 ## 2024.8.0

View File

@ -124,6 +124,14 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForReactions:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

40
locales/index.d.ts vendored
View File

@ -5547,6 +5547,22 @@ export interface Locale extends ILocale {
* *
*/ */
"clipNoteLimitExceeded": string; "clipNoteLimitExceeded": string;
/**
*
*/
"performance": string;
/**
*
*/
"modified": string;
/**
*
*/
"discard": string;
/**
* {n}
*/
"thereAreNChanges": ParameterizedString<"n">;
/** /**
* *
*/ */
@ -6429,6 +6445,10 @@ export interface Locale extends ILocale {
* DBへ追加で問い合わせを行うフォールバック処理を行います * DBへ追加で問い合わせを行うフォールバック処理を行います
*/ */
"fanoutTimelineDbFallbackDescription": string; "fanoutTimelineDbFallbackDescription": string;
/**
* Redisのメモリ使用量は増加します
*/
"reactionsBufferingDescription": string;
/** /**
* URL * URL
*/ */
@ -7622,6 +7642,26 @@ export interface Locale extends ILocale {
* *
*/ */
"avatarDecorationLimit": string; "avatarDecorationLimit": string;
/**
*
*/
"canImportAntennas": string;
/**
*
*/
"canImportBlocking": string;
/**
*
*/
"canImportFollowing": string;
/**
*
*/
"canImportMuting": string;
/**
*
*/
"canImportUserLists": string;
}; };
"_condition": { "_condition": {
/** /**

View File

@ -1381,6 +1381,10 @@ fromX: "{x}から"
genEmbedCode: "埋め込みコードを生成" genEmbedCode: "埋め込みコードを生成"
noteOfThisUser: "このユーザーのノート一覧" noteOfThisUser: "このユーザーのノート一覧"
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
performance: "パフォーマンス"
modified: "変更あり"
discard: "破棄"
thereAreNChanges: "{n}件の変更があります"
showUnreadNotificationsCount: "未読の通知の数を表示する" showUnreadNotificationsCount: "未読の通知の数を表示する"
showCatOnly: "キャット付きのみ" showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可" additionalPermissionsForFlash: "Playへの追加許可"
@ -1635,6 +1639,7 @@ _serverSettings:
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。" fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
inquiryUrl: "問い合わせ先URL" inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
@ -1975,6 +1980,11 @@ _role:
canSearchNotes: "ノート検索の利用" canSearchNotes: "ノート検索の利用"
canUseTranslator: "翻訳機能の利用" canUseTranslator: "翻訳機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数" avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
canImportAntennas: "アンテナのインポートを許可"
canImportBlocking: "ブロックのインポートを許可"
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"

View File

@ -1,7 +1,7 @@
{ {
"name": "cherrypick", "name": "cherrypick",
"version": "4.12.0-beta.1", "version": "4.12.0-beta.1",
"basedMisskeyVersion": "2024.9.0-alpha.0", "basedMisskeyVersion": "2024.9.0-alpha.7",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -60,11 +60,11 @@
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"ignore-walk": "6.0.5", "ignore-walk": "6.0.5",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.40", "postcss": "8.4.47",
"tar": "6.2.1", "tar": "6.2.1",
"terser": "5.31.3", "terser": "5.33.0",
"typescript": "5.5.4", "typescript": "5.6.2",
"esbuild": "0.23.0", "esbuild": "0.23.1",
"glob": "11.0.0" "glob": "11.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -73,11 +73,11 @@
"@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0", "@typescript-eslint/parser": "7.17.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.13.1", "cypress": "13.14.2",
"eslint": "9.8.0", "eslint": "9.8.0",
"globals": "15.8.0", "globals": "15.9.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"start-server-and-test": "2.0.4" "start-server-and-test": "2.0.8"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0" "@tensorflow/tfjs-core": "4.4.0"

View File

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

View File

@ -68,26 +68,26 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.620.0", "@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.620.0", "@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "5.21.1", "@bull-board/api": "5.23.0",
"@bull-board/fastify": "5.21.1", "@bull-board/fastify": "5.23.0",
"@bull-board/ui": "5.21.1", "@bull-board/ui": "5.23.0",
"@discordapp/twemoji": "15.0.3", "@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "4.3.0", "@fastify/accepts": "5.0.0",
"@fastify/cookie": "9.3.1", "@fastify/cookie": "10.0.0",
"@fastify/cors": "9.0.1", "@fastify/cors": "10.0.0",
"@fastify/express": "3.0.0", "@fastify/express": "4.0.0",
"@fastify/http-proxy": "9.5.0", "@fastify/http-proxy": "10.0.0",
"@fastify/multipart": "8.3.0", "@fastify/multipart": "9.0.0",
"@fastify/static": "7.0.4", "@fastify/static": "8.0.0",
"@fastify/view": "9.1.0", "@fastify/view": "10.0.0",
"@google-cloud/logging": "^10.5.0", "@google-cloud/logging": "^10.5.0",
"@google-cloud/translate": "^7.2.1", "@google-cloud/translate": "^7.2.1",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "^0.1.53", "@napi-rs/canvas": "0.1.56",
"@nestjs/common": "10.3.10", "@nestjs/common": "10.4.3",
"@nestjs/core": "10.3.10", "@nestjs/core": "10.4.3",
"@nestjs/testing": "10.3.10", "@nestjs/testing": "10.4.3",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "8.20.0", "@sentry/node": "8.20.0",
"@sentry/profiling-node": "8.20.0", "@sentry/profiling-node": "8.20.0",
@ -105,7 +105,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.10.4", "bullmq": "5.13.2",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -118,27 +118,28 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.28.1", "fastify": "5.0.0",
"fastify-raw-body": "4.3.0", "fastify-raw-body": "5.0.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.3.0", "file-type": "19.5.0",
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "14.4.2", "got": "14.4.2",
"happy-dom": "15.6.1", "happy-dom": "15.7.4",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.4.1", "ioredis": "5.4.1",
"ip-cidr": "4.0.1", "ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.0.1", "is-svg": "5.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "24.1.1", "jsdom": "24.1.1",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.2", "jsonld": "8.3.2",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"meilisearch": "0.41.0", "meilisearch": "0.42.0",
"juice": "11.0.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
@ -146,24 +147,24 @@
"nanoid": "5.0.7", "nanoid": "5.0.7",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.14", "nodemailer": "6.9.15",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.3.1", "otpauth": "9.3.2",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.12.0", "pg": "8.13.0",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.3", "pug": "3.0.3",
"punycode": "2.3.1", "punycode": "2.3.1",
"qrcode": "1.5.3", "qrcode": "1.5.4",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.21.3", "re2": "1.21.4",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rename": "1.0.4", "rename": "1.0.4",
@ -171,18 +172,18 @@
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.33.4", "sharp": "0.33.5",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"systeminformation": "5.22.11", "systeminformation": "5.23.5",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",
"typescript": "5.5.4", "typescript": "5.6.2",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
@ -191,7 +192,7 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.3.10", "@nestjs/platform-express": "10.4.3",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36", "@swc/jest": "0.2.36",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
@ -200,10 +201,10 @@
"@types/body-parser": "1.19.5", "@types/body-parser": "1.19.5",
"@types/color-convert": "2.0.3", "@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.26",
"@types/htmlescape": "1.1.3", "@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7", "@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12", "@types/jest": "29.5.13",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7", "@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15", "@types/jsonld": "1.5.15",
@ -211,18 +212,18 @@
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "20.14.12", "@types/node": "20.14.12",
"@types/nodemailer": "6.4.15", "@types/nodemailer": "6.4.16",
"@types/oauth": "0.9.5", "@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.6", "@types/pg": "8.11.10",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/punycode": "2.1.4", "@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5", "@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6", "@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7", "@types/rename": "1.0.7",
"@types/sanitize-html": "2.11.0", "@types/sanitize-html": "2.13.0",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@types/simple-oauth2": "5.0.7", "@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5", "@types/sinonjs__fake-timers": "8.1.5",
@ -230,17 +231,17 @@
"@types/tmp": "0.2.6", "@types/tmp": "0.2.6",
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.3", "@types/web-push": "3.6.3",
"@types/ws": "8.5.11", "@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0", "@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.0.1", "aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.30.0",
"execa": "9.3.0", "execa": "9.4.0",
"fkill": "9.0.0", "fkill": "9.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.1.4", "nodemon": "3.1.7",
"pid-port": "1.0.0", "pid-port": "1.0.0",
"simple-oauth2": "5.1.0" "simple-oauth2": "5.1.0"
} }

View File

@ -15,6 +15,8 @@ import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js'; import { RepositoryModule } from './models/RepositoryModule.js';
import { allSettled } from './misc/promise-tracker.js'; import { allSettled } from './misc/promise-tracker.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common'; import type { Provider, OnApplicationShutdown } from '@nestjs/common';
import { MiMeta } from '@/models/Meta.js';
import { GlobalEvents } from './core/GlobalEventService.js';
const $config: Provider = { const $config: Provider = {
provide: DI.config, provide: DI.config,
@ -94,6 +96,71 @@ const $redisForTimelines: Provider = {
inject: [DI.config], inject: [DI.config],
}; };
const $redisForReactions: Provider = {
provide: DI.redisForReactions,
useFactory: (config: Config) => {
return new Redis.Redis(config.redisForReactions);
},
inject: [DI.config],
};
const $meta: Provider = {
provide: DI.meta,
useFactory: async (db: DataSource, redisForSub: Redis.Redis) => {
const meta = await db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
id: 'DESC',
},
});
const meta = metas[0];
if (meta) {
return meta;
} else {
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
const saved = await transactionalEntityManager
.upsert(
MiMeta,
{
id: 'x',
},
['id'],
)
.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
return saved;
}
});
async function 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 'metaUpdated': {
for (const key in body) {
(meta as any)[key] = (body as any)[key];
}
meta.proxyAccount = null; // joinなカラムは通常取ってこないので
break;
}
default:
break;
}
}
}
redisForSub.on('message', onMessage);
return meta;
},
inject: [DI.db, DI.redisForSub],
};
const $redisForJobQueue: Provider = { const $redisForJobQueue: Provider = {
provide: DI.redisForJobQueue, provide: DI.redisForJobQueue,
useFactory: (config: Config) => { useFactory: (config: Config) => {
@ -109,8 +176,8 @@ const $redisForJobQueue: Provider = {
@Global() @Global()
@Module({ @Module({
imports: [RepositoryModule], imports: [RepositoryModule],
providers: [$config, $db, $meilisearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForJobQueue], providers: [$config, $db, $meta, $meilisearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, $redisForJobQueue],
exports: [$config, $db, $meilisearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForJobQueue, RepositoryModule], exports: [$config, $db, $meta, $meilisearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, $redisForJobQueue, RepositoryModule],
}) })
export class GlobalModule implements OnApplicationShutdown { export class GlobalModule implements OnApplicationShutdown {
constructor( constructor(
@ -119,6 +186,7 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
@Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
@Inject(DI.redisForJobQueue) private redisForJobQueue: Redis.Redis, @Inject(DI.redisForJobQueue) private redisForJobQueue: Redis.Redis,
) { } ) { }
@ -132,6 +200,7 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisForPub.disconnect(), this.redisForPub.disconnect(),
this.redisForSub.disconnect(), this.redisForSub.disconnect(),
this.redisForTimelines.disconnect(), this.redisForTimelines.disconnect(),
this.redisForReactions.disconnect(),
this.redisForJobQueue.disconnect(), this.redisForJobQueue.disconnect(),
]); ]);
} }

View File

@ -49,6 +49,7 @@ type Source = {
redisForPubsub?: RedisOptionsSource; redisForPubsub?: RedisOptionsSource;
redisForJobQueue?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource; redisForTimelines?: RedisOptionsSource;
redisForReactions?: RedisOptionsSource;
meilisearch?: { meilisearch?: {
host: string; host: string;
port: string; port: string;
@ -188,6 +189,7 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource; redisForTimelines: RedisOptions & RedisOptionsSource;
redisForReactions: RedisOptions & RedisOptionsSource;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined; sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined; sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
perChannelMaxNoteCacheCount: number; perChannelMaxNoteCacheCount: number;
@ -270,6 +272,7 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
sentryForBackend: config.sentryForBackend, sentryForBackend: config.sentryForBackend,
sentryForFrontend: config.sentryForFrontend, sentryForFrontend: config.sentryForFrontend,
id: config.id, id: config.id,

View File

@ -8,6 +8,8 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
//#region hard limits //#region hard limits
// If you change DB_* values, you must also change the DB schema. // If you change DB_* values, you must also change the DB schema.

View File

@ -14,10 +14,10 @@ import type {
AbuseReportNotificationRecipientRepository, AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient, MiAbuseReportNotificationRecipient,
MiAbuseUserReport, MiAbuseUserReport,
MiMeta,
MiUser, MiUser,
} from '@/models/_.js'; } from '@/models/_.js';
import { EmailService } from '@/core/EmailService.js'; import { EmailService } from '@/core/EmailService.js';
import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -27,15 +27,19 @@ import { IdService } from './IdService.js';
@Injectable() @Injectable()
export class AbuseReportNotificationService implements OnApplicationShutdown { export class AbuseReportNotificationService implements OnApplicationShutdown {
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.abuseReportNotificationRecipientRepository) @Inject(DI.abuseReportNotificationRecipientRepository)
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
@Inject(DI.redisForSub) @Inject(DI.redisForSub)
private redisForSub: Redis.Redis, private redisForSub: Redis.Redis,
private idService: IdService, private idService: IdService,
private roleService: RoleService, private roleService: RoleService,
private systemWebhookService: SystemWebhookService, private systemWebhookService: SystemWebhookService,
private emailService: EmailService, private emailService: EmailService,
private metaService: MetaService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.filter(x => x != null), .filter(x => x != null),
); );
// 送信先の鮮度を保つため、毎回取得する
const meta = await this.metaService.fetch(true);
recipientEMailAddresses.push( recipientEMailAddresses.push(
...(meta.email ? [meta.email] : []), ...(this.meta.email ? [this.meta.email] : []),
); );
if (recipientEMailAddresses.length <= 0) { if (recipientEMailAddresses.length <= 0) {

View File

@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { MetaService } from '@/core/MetaService.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
@Injectable() @Injectable()
export class AccountMoveService { export class AccountMoveService {
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -57,7 +59,6 @@ export class AccountMoveService {
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
private metaService: MetaService,
private relayService: RelayService, private relayService: RelayService,
private queueService: QueueService, private queueService: QueueService,
) { ) {
@ -276,7 +277,7 @@ export class AccountMoveService {
if (this.userEntityService.isRemoteUser(oldAccount)) { if (this.userEntityService.isRemoteUser(oldAccount)) {
this.federatedInstanceService.fetch(oldAccount.host).then(async i => { this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false); this.instanceChart.updateFollowers(i.host, false);
} }
}); });

View File

@ -52,6 +52,7 @@ import { PollService } from './PollService.js';
import { PushNotificationService } from './PushNotificationService.js'; import { PushNotificationService } from './PushNotificationService.js';
import { QueryService } from './QueryService.js'; import { QueryService } from './QueryService.js';
import { ReactionService } from './ReactionService.js'; import { ReactionService } from './ReactionService.js';
import { ReactionsBufferingService } from './ReactionsBufferingService.js';
import { RelayService } from './RelayService.js'; import { RelayService } from './RelayService.js';
import { RoleService } from './RoleService.js'; import { RoleService } from './RoleService.js';
import { S3Service } from './S3Service.js'; import { S3Service } from './S3Service.js';
@ -201,6 +202,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService }; const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService }; const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
@ -356,6 +358,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
PushNotificationService, PushNotificationService,
QueryService, QueryService,
ReactionService, ReactionService,
ReactionsBufferingService,
RelayService, RelayService,
RoleService, RoleService,
S3Service, S3Service,
@ -507,6 +510,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$PushNotificationService, $PushNotificationService,
$QueryService, $QueryService,
$ReactionService, $ReactionService,
$ReactionsBufferingService,
$RelayService, $RelayService,
$RoleService, $RoleService,
$S3Service, $S3Service,
@ -659,6 +663,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
PushNotificationService, PushNotificationService,
QueryService, QueryService,
ReactionService, ReactionService,
ReactionsBufferingService,
RelayService, RelayService,
RoleService, RoleService,
S3Service, S3Service,
@ -809,6 +814,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$PushNotificationService, $PushNotificationService,
$QueryService, $QueryService,
$ReactionService, $ReactionService,
$ReactionsBufferingService,
$RelayService, $RelayService,
$RoleService, $RoleService,
$S3Service, $S3Service,

View File

@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
import { MiDriveFile } from '@/models/DriveFile.js'; import { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
@ -99,6 +98,9 @@ export class DriveService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -115,7 +117,6 @@ export class DriveService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private downloadService: DownloadService, private downloadService: DownloadService,
private internalStorageService: InternalStorageService, private internalStorageService: InternalStorageService,
private s3Service: S3Service, private s3Service: S3Service,
@ -150,9 +151,7 @@ export class DriveService {
// thunbnail, webpublic を必要なら生成 // thunbnail, webpublic を必要なら生成
const alts = await this.generateAlts(path, type, !file.uri); const alts = await this.generateAlts(path, type, !file.uri);
const meta = await this.metaService.fetch(); if (this.meta.useObjectStorage) {
if (meta.useObjectStorage) {
//#region ObjectStorage params //#region ObjectStorage params
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
@ -171,13 +170,13 @@ export class DriveService {
ext = ''; ext = '';
} }
const useObjectStorageRemote = isRemote && meta.useObjectStorageRemote; const useObjectStorageRemote = isRemote && this.meta.useObjectStorageRemote;
const objectStorageBaseUrl = useObjectStorageRemote ? meta.objectStorageRemoteBaseUrl : meta.objectStorageBaseUrl; const objectStorageBaseUrl = useObjectStorageRemote ? this.meta.objectStorageRemoteBaseUrl : this.meta.objectStorageBaseUrl;
const objectStorageUseSSL = useObjectStorageRemote ? meta.objectStorageRemoteUseSSL : meta.objectStorageUseSSL; const objectStorageUseSSL = useObjectStorageRemote ? this.meta.objectStorageRemoteUseSSL : this.meta.objectStorageUseSSL;
const objectStorageEndpoint = useObjectStorageRemote ? meta.objectStorageRemoteEndpoint : meta.objectStorageEndpoint; const objectStorageEndpoint = useObjectStorageRemote ? this.meta.objectStorageRemoteEndpoint : this.meta.objectStorageEndpoint;
const objectStoragePort = useObjectStorageRemote ? meta.objectStorageRemotePort : meta.objectStoragePort; const objectStoragePort = useObjectStorageRemote ? this.meta.objectStorageRemotePort : this.meta.objectStoragePort;
const objectStorageBucket = useObjectStorageRemote ? meta.objectStorageRemoteBucket : meta.objectStorageBucket; const objectStorageBucket = useObjectStorageRemote ? this.meta.objectStorageRemoteBucket : this.meta.objectStorageBucket;
const objectStoragePrefix = useObjectStorageRemote ? meta.objectStorageRemotePrefix : meta.objectStoragePrefix; const objectStoragePrefix = useObjectStorageRemote ? this.meta.objectStorageRemotePrefix : this.meta.objectStoragePrefix;
const baseUrl = objectStorageBaseUrl const baseUrl = objectStorageBaseUrl
?? `${ objectStorageUseSSL ? 'https' : 'http' }://${ objectStorageEndpoint }${ objectStoragePort ? `:${objectStoragePort}` : '' }/${ objectStorageBucket }`; ?? `${ objectStorageUseSSL ? 'https' : 'http' }://${ objectStorageEndpoint }${ objectStoragePort ? `:${objectStoragePort}` : '' }/${ objectStorageBucket }`;
@ -385,11 +384,9 @@ export class DriveService {
if (type === 'image/apng') type = 'image/png'; if (type === 'image/apng') type = 'image/png';
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
const meta = await this.metaService.fetch(); const useObjectStorageRemote = isRemote && this.meta.useObjectStorageRemote;
const objectStorageBucket = useObjectStorageRemote ? this.meta.objectStorageRemoteBucket : this.meta.objectStorageBucket;
const useObjectStorageRemote = isRemote && meta.useObjectStorageRemote; const objectStorageSetPublicRead = useObjectStorageRemote ? this.meta.objectStorageRemoteSetPublicRead : this.meta.objectStorageSetPublicRead;
const objectStorageBucket = useObjectStorageRemote ? meta.objectStorageRemoteBucket : meta.objectStorageBucket;
const objectStorageSetPublicRead = useObjectStorageRemote ? meta.objectStorageRemoteSetPublicRead : meta.objectStorageSetPublicRead;
const params = { const params = {
Bucket: objectStorageBucket, Bucket: objectStorageBucket,
@ -407,7 +404,7 @@ export class DriveService {
); );
if (objectStorageSetPublicRead) params.ACL = 'public-read'; if (objectStorageSetPublicRead) params.ACL = 'public-read';
await this.s3Service.upload(meta, params, isRemote) await this.s3Service.upload(this.meta, params, isRemote)
.then( .then(
result => { result => {
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
@ -473,32 +470,31 @@ export class DriveService {
ext = null, ext = null,
}: AddFileArgs): Promise<MiDriveFile> { }: AddFileArgs): Promise<MiDriveFile> {
let skipNsfwCheck = false; let skipNsfwCheck = false;
const instance = await this.metaService.fetch();
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
if (user == null) { if (user == null) {
skipNsfwCheck = true; skipNsfwCheck = true;
} else if (userRoleNSFW) { } else if (userRoleNSFW) {
skipNsfwCheck = true; skipNsfwCheck = true;
} }
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
const info = await this.fileInfoService.getFileInfo(path, { const info = await this.fileInfoService.getFileInfo(path, {
skipSensitiveDetection: skipNsfwCheck, skipSensitiveDetection: skipNsfwCheck,
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
0.5, 0.5,
sensitiveThresholdForPorn: 0.75, sensitiveThresholdForPorn: 0.75,
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
}); });
this.registerLogger.info(`${JSON.stringify(info)}`); this.registerLogger.info(`${JSON.stringify(info)}`);
// 現状 false positive が多すぎて実用に耐えない // 現状 false positive が多すぎて実用に耐えない
//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { //if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) {
// throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
//} //}
@ -598,13 +594,13 @@ export class DriveService {
file.maybeSensitive = info.sensitive; file.maybeSensitive = info.sensitive;
file.maybePorn = info.porn; file.maybePorn = info.porn;
file.isSensitive = user file.isSensitive = user
? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : ? this.userEntityService.isLocalUser(user) && profile?.alwaysMarkNsfw ? true :
sensitive ?? false sensitive ?? false
: false; : false;
if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (info.sensitive && profile?.autoSensitive) file.isSensitive = true;
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true;
if (userRoleNSFW) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true;
if (url !== null) { if (url !== null) {
@ -666,7 +662,7 @@ export class DriveService {
// ローカルユーザーのみ // ローカルユーザーのみ
this.perUserDriveChart.update(file, true); this.perUserDriveChart.update(file, true);
} else { } else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, true); this.instanceChart.updateDrive(file, true);
} }
} }
@ -812,7 +808,7 @@ export class DriveService {
// ローカルユーザーのみ // ローカルユーザーのみ
this.perUserDriveChart.update(file, false); this.perUserDriveChart.update(file, false);
} else { } else {
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateDrive(file, false); this.instanceChart.updateDrive(file, false);
} }
} }
@ -834,16 +830,15 @@ export class DriveService {
@bindThis @bindThis
public async deleteObjectStorageFile(key: string, isRemote: boolean) { public async deleteObjectStorageFile(key: string, isRemote: boolean) {
const meta = await this.metaService.fetch(); const useObjectStorageRemote = isRemote && this.meta.useObjectStorageRemote;
const useObjectStorageRemote = isRemote && meta.useObjectStorageRemote; const objectStorageBucket = useObjectStorageRemote ? this.meta.objectStorageRemoteBucket : this.meta.objectStorageBucket;
const objectStorageBucket = useObjectStorageRemote ? meta.objectStorageRemoteBucket : meta.objectStorageBucket;
try { try {
const param = { const param = {
Bucket: objectStorageBucket, Bucket: objectStorageBucket,
Key: key, Key: key,
} as DeleteObjectCommandInput; } as DeleteObjectCommandInput;
await this.s3Service.delete(meta, param, isRemote); await this.s3Service.delete(this.meta, param, isRemote);
} catch (err: any) { } catch (err: any) {
if (err.name === 'NoSuchKey') { if (err.name === 'NoSuchKey') {
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);

View File

@ -4,18 +4,17 @@
*/ */
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import juice from 'juice';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { validate as validateEmail } from 'deep-email-validator'; import { validate as validateEmail } from 'deep-email-validator';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import type { UserProfilesRepository } from '@/models/_.js'; import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { QueueService } from '@/core/QueueService.js';
@Injectable() @Injectable()
export class EmailService { export class EmailService {
@ -25,49 +24,41 @@ export class EmailService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private metaService: MetaService,
private loggerService: LoggerService, private loggerService: LoggerService,
private utilityService: UtilityService, private utilityService: UtilityService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private queueService: QueueService,
) { ) {
this.logger = this.loggerService.getLogger('email'); this.logger = this.loggerService.getLogger('email');
} }
@bindThis @bindThis
public async sendEmail(to: string, subject: string, html: string, text: string) { public async sendEmail(to: string, subject: string, html: string, text: string) {
const meta = await this.metaService.fetch(true); if (!this.meta.enableEmail) return;
if (!meta.enableEmail) return;
const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`; const emailSettingUrl = `${this.config.url}/settings/email`;
const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== '';
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: meta.smtpHost, host: this.meta.smtpHost,
port: meta.smtpPort, port: this.meta.smtpPort,
secure: meta.smtpSecure, secure: this.meta.smtpSecure,
ignoreTLS: !enableAuth, ignoreTLS: !enableAuth,
proxy: this.config.proxySmtp, proxy: this.config.proxySmtp,
auth: enableAuth ? { auth: enableAuth ? {
user: meta.smtpUser, user: this.meta.smtpUser,
pass: meta.smtpPass, pass: this.meta.smtpPass,
} : undefined, } : undefined,
} as any); } as any);
try { const htmlContent = `<!doctype html>
// TODO: htmlサニタイズ
const info = await transporter.sendMail({
from: meta.email!,
to: to,
subject: subject,
text: text,
html: `<!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -132,7 +123,7 @@ export class EmailService {
<body> <body>
<main> <main>
<header> <header>
<img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/> <img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
</header> </header>
<article> <article>
<h1>${ subject }</h1> <h1>${ subject }</h1>
@ -146,7 +137,18 @@ export class EmailService {
<a href="${ this.config.url }">${ this.config.host }</a> <a href="${ this.config.url }">${ this.config.host }</a>
</nav> </nav>
</body> </body>
</html>`, </html>`;
const inlinedHtml = juice(htmlContent);
try {
// TODO: htmlサニタイズ
const info = await transporter.sendMail({
from: this.meta.email!,
to: to,
subject: subject,
text: text,
html: inlinedHtml,
}); });
this.logger.info(`Message sent: ${info.messageId}`); this.logger.info(`Message sent: ${info.messageId}`);
@ -161,8 +163,6 @@ export class EmailService {
available: boolean; available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
}> { }> {
const meta = await this.metaService.fetch();
const exist = await this.userProfilesRepository.countBy({ const exist = await this.userProfilesRepository.countBy({
emailVerified: true, emailVerified: true,
email: emailAddress, email: emailAddress,
@ -180,11 +180,11 @@ export class EmailService {
reason?: string | null, reason?: string | null,
} = { valid: true, reason: null }; } = { valid: true, reason: null };
if (meta.enableActiveEmailValidation) { if (this.meta.enableActiveEmailValidation) {
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) {
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey);
} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) { } else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey); validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
} else { } else {
validated = await validateEmail({ validated = await validateEmail({
email: emailAddress, email: emailAddress,
@ -214,7 +214,7 @@ export class EmailService {
} }
const emailDomain: string = emailAddress.split('@')[1]; const emailDomain: string = emailAddress.split('@')[1];
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
if (isBanned) { if (isBanned) {
return { return {

View File

@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { MiHashtag } from '@/models/Hashtag.js'; import type { MiHashtag } from '@/models/Hashtag.js';
import type { HashtagsRepository } from '@/models/_.js'; import type { HashtagsRepository, MiMeta } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@Injectable() @Injectable()
export class HashtagService { export class HashtagService {
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
@ -29,7 +31,6 @@ export class HashtagService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private featuredService: FeaturedService, private featuredService: FeaturedService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private utilityService: UtilityService, private utilityService: UtilityService,
) { ) {
} }
@ -160,10 +161,9 @@ export class HashtagService {
@bindThis @bindThis
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> { public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
const instance = await this.metaService.fetch(); const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return; if (hiddenTags.includes(hashtag)) return;
if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return; if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
// YYYYMMDDHHmm (10分間隔) // YYYYMMDDHHmm (10分間隔)
const now = new Date(); const now = new Date();

View File

@ -239,7 +239,7 @@ export class MfmService {
return null; return null;
} }
const { window } = new Window(); const { happyDOM, window } = new Window();
const doc = window.document; const doc = window.document;
@ -457,6 +457,10 @@ export class MfmService {
appendChildren(nodes, body); appendChildren(nodes, body);
return new XMLSerializer().serializeToString(body); const serialized = new XMLSerializer().serializeToString(body);
happyDOM.close().catch(err => {});
return serialized;
} }
} }

View File

@ -8,7 +8,6 @@ import * as mfm from 'cherrypick-mfm-js';
import { In, DataSource, IsNull, LessThan } from 'typeorm'; import { In, DataSource, IsNull, LessThan } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2';
import { extractMentions } from '@/misc/extract-mentions.js'; import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
@ -16,7 +15,7 @@ import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import { MiEvent } from '@/models/Event.js'; import { MiEvent } from '@/models/Event.js';
import type { IEvent } from '@/models/Event.js'; import type { IEvent } from '@/models/Event.js';
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js'; import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js'; import { concat } from '@/misc/prelude/array.js';
@ -25,11 +24,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { IPoll } from '@/models/Poll.js'; import type { IPoll } from '@/models/Poll.js';
import { MiPoll } from '@/models/Poll.js'; import { MiPoll } from '@/models/Poll.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { MiChannel } from '@/models/Channel.js'; import type { MiChannel } from '@/models/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -53,7 +49,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
@ -161,6 +156,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@ -215,7 +213,6 @@ export class NoteCreateService implements OnApplicationShutdown {
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService,
private searchService: SearchService, private searchService: SearchService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
@ -257,10 +254,8 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.visibleUsers = [];
if (data.channel != null) data.localOnly = true; if (data.channel != null) data.localOnly = true;
const meta = await this.metaService.fetch();
if (data.visibility === 'public' && data.channel == null) { if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords; const sensitiveWords = this.meta.sensitiveWords;
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home'; data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
@ -268,17 +263,17 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
const hasProhibitedWords = await this.checkProhibitedWordsContain({ const hasProhibitedWords = this.checkProhibitedWordsContain({
cw: data.cw, cw: data.cw,
text: data.text, text: data.text,
pollChoices: data.poll?.choices, pollChoices: data.poll?.choices,
}, meta.prohibitedWords); }, this.meta.prohibitedWords);
if (hasProhibitedWords) { if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
} }
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
data.visibility = 'home'; data.visibility = 'home';
@ -371,7 +366,7 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
// if the host is media-silenced, custom emojis are not allowed // if the host is media-silenced, custom emojis are not allowed
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
@ -531,10 +526,8 @@ export class NoteCreateService implements OnApplicationShutdown {
host: MiUser['host']; host: MiUser['host'];
isBot: MiUser['isBot']; isBot: MiUser['isBot'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
const meta = await this.metaService.fetch();
this.notesChart.update(note, true); this.notesChart.update(note, true);
if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) { if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
this.perUserNotesChart.update(user, note, true); this.perUserNotesChart.update(user, note, true);
} }
@ -542,7 +535,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => { this.federatedInstanceService.fetch(user.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true); this.instanceChart.updateNote(i.host, note, true);
} }
}); });
@ -878,15 +871,14 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch(); if (!this.meta.enableFanoutTimeline) return;
if (!meta.enableFanoutTimeline) return;
const r = this.redisForTimelines.pipeline(); const r = this.redisForTimelines.pipeline();
if (note.channelId) { if (note.channelId) {
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({ const channelFollowings = await this.channelFollowingsRepository.find({
where: { where: {
@ -896,9 +888,9 @@ export class NoteCreateService implements OnApplicationShutdown {
}); });
for (const channelFollowing of channelFollowings) { for (const channelFollowing of channelFollowings) {
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
} }
} }
} else { } else {
@ -936,9 +928,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!following.withReplies) continue; if (!following.withReplies) continue;
} }
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
} }
} }
@ -955,25 +947,25 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!userListMembership.withReplies) continue; if (!userListMembership.withReplies) continue;
} }
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
} }
} }
// 自分自身のHTL // 自分自身のHTL
if (note.userHost == null) { if (note.userHost == null) {
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
} }
} }
} }
// 自分自身以外への返信 // 自分自身以外への返信
if (isReply(note)) { if (isReply(note)) {
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) { if (note.visibility === 'public' && note.userHost == null) {
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
@ -982,9 +974,9 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
} else { } else {
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
} }
if (note.visibility === 'public' && note.userHost == null) { if (note.visibility === 'public' && note.userHost == null) {
@ -1043,9 +1035,9 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) { public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
if (prohibitedWords == null) { if (prohibitedWords == null) {
prohibitedWords = (await this.metaService.fetch()).prohibitedWords; prohibitedWords = this.meta.prohibitedWords;
} }
if ( if (

View File

@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm';
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -19,9 +19,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
@ -32,6 +30,9 @@ export class NoteDeleteService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -42,13 +43,11 @@ export class NoteDeleteService {
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private searchService: SearchService, private searchService: SearchService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private notesChart: NotesChart, private notesChart: NotesChart,
@ -104,17 +103,15 @@ export class NoteDeleteService {
*/ */
//#endregion //#endregion
const meta = await this.metaService.fetch();
this.notesChart.update(note, false); this.notesChart.update(note, false);
if (meta.enableChartsForRemoteUser || (user.host == null)) { if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserNotesChart.update(user, note, false); this.perUserNotesChart.update(user, note, false);
} }
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => { this.federatedInstanceService.fetch(user.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, false); this.instanceChart.updateNote(i.host, note, false);
} }
}); });

View File

@ -4,26 +4,25 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js'; import type { MiMeta, UsersRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js'; import type { MiLocalUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ProxyAccountService { export class ProxyAccountService {
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private metaService: MetaService,
) { ) {
} }
@bindThis @bindThis
public async fetch(): Promise<MiLocalUser | null> { public async fetch(): Promise<MiLocalUser | null> {
const meta = await this.metaService.fetch(); if (this.meta.proxyAccountId == null) return null;
if (meta.proxyAccountId == null) return null; return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
} }
} }

View File

@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RedisKVCache } from '@/misc/cache.js'; import { RedisKVCache } from '@/misc/cache.js';
@ -57,13 +56,14 @@ export class PushNotificationService implements OnApplicationShutdown {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@Inject(DI.swSubscriptionsRepository) @Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository, private swSubscriptionsRepository: SwSubscriptionsRepository,
private metaService: MetaService,
) { ) {
this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', { this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
lifetime: 1000 * 60 * 60 * 1, // 1h lifetime: 1000 * 60 * 60 * 1, // 1h
@ -76,14 +76,12 @@ export class PushNotificationService implements OnApplicationShutdown {
@bindThis @bindThis
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) { public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
const meta = await this.metaService.fetch(); if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return;
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(this.config.url, push.setVapidDetails(this.config.url,
meta.swPublicKey, this.meta.swPublicKey,
meta.swPrivateKey); this.meta.swPrivateKey);
const subscriptions = await this.subscriptionsCache.fetch(userId); const subscriptions = await this.subscriptionsCache.fetch(userId);

View File

@ -88,6 +88,12 @@ export class QueueService {
repeat: { pattern: '*/5 * * * *' }, repeat: { pattern: '*/5 * * * *' },
removeOnComplete: true, removeOnComplete: true,
}); });
this.systemQueue.add('bakeBufferedReactions', {
}, {
repeat: { pattern: '0 0 * * *' },
removeOnComplete: true,
});
} }
@bindThis @bindThis

View File

@ -4,9 +4,8 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
@ -21,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
@ -30,9 +28,10 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
const FALLBACK = '\u2764'; const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
@ -71,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
@Injectable() @Injectable()
export class ReactionService { export class ReactionService {
constructor( constructor(
@Inject(DI.redis) @Inject(DI.meta)
private redisClient: Redis.Redis, private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -87,12 +86,12 @@ export class ReactionService {
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private metaService: MetaService,
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private roleService: RoleService, private roleService: RoleService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService, private userBlockingService: UserBlockingService,
private reactionsBufferingService: ReactionsBufferingService,
private idService: IdService, private idService: IdService,
private featuredService: FeaturedService, private featuredService: FeaturedService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
@ -105,8 +104,6 @@ export class ReactionService {
@bindThis @bindThis
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
const meta = await this.metaService.fetch();
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@ -152,7 +149,7 @@ export class ReactionService {
} }
// for media silenced host, custom emoji reactions are not allowed // for media silenced host, custom emoji reactions are not allowed
if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
reaction = FALLBACK; reaction = FALLBACK;
} }
} else { } else {
@ -174,7 +171,6 @@ export class ReactionService {
reaction, reaction,
}; };
// Create reaction
try { try {
await this.noteReactionsRepository.insert(record); await this.noteReactionsRepository.insert(record);
} catch (e) { } catch (e) {
@ -198,6 +194,9 @@ export class ReactionService {
} }
// Increment reactions count // Increment reactions count
if (this.meta.enableReactionsBuffering) {
await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
} else {
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
@ -208,6 +207,7 @@ export class ReactionService {
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
}
// 30%の確率、セルフではない、3日以内に投稿されたートの場合ハイライト用ランキング更新 // 30%の確率、セルフではない、3日以内に投稿されたートの場合ハイライト用ランキング更新
if ( if (
@ -227,7 +227,7 @@ export class ReactionService {
} }
} }
if (meta.enableChartsForRemoteUser || (user.host == null)) { if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note); this.perUserReactionsChart.update(user, note);
} }
@ -305,6 +305,9 @@ export class ReactionService {
} }
// Decrement reactions count // Decrement reactions count
if (this.meta.enableReactionsBuffering) {
await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
} else {
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
@ -313,6 +316,7 @@ export class ReactionService {
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
}
this.globalEventService.publishNoteStream(note.id, 'unreacted', { this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction, reaction: this.decodeReaction(exist.reaction).reaction,
@ -333,6 +337,7 @@ export class ReactionService {
//#endregion //#endregion
} }
// TODO: 廃止
/** /**
* *
* 0 * 0

View File

@ -0,0 +1,162 @@
/*
* 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 { DI } from '@/di-symbols.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import type { MiUser, NotesRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
@Injectable()
export class ReactionsBufferingService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redisForReactions)
private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
) {
}
@bindThis
public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> {
const pipeline = this.redisForReactions.pipeline();
pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
for (let i = 0; i < currentPairs.length; i++) {
pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
}
pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
await pipeline.exec();
}
@bindThis
public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> {
const pipeline = this.redisForReactions.pipeline();
pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
// TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
await pipeline.exec();
}
@bindThis
public async get(noteId: MiNote['id']): Promise<{
deltas: Record<string, number>;
pairs: ([MiUser['id'], string])[];
}> {
const pipeline = this.redisForReactions.pipeline();
pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
const results = await pipeline.exec();
const resultDeltas = results![0][1] as Record<string, string>;
const resultPairs = results![1][1] as string[];
const deltas = {} as Record<string, number>;
for (const [name, count] of Object.entries(resultDeltas)) {
deltas[name] = parseInt(count);
}
const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
return {
deltas,
pairs,
};
}
@bindThis
public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], {
deltas: Record<string, number>;
pairs: ([MiUser['id'], string])[];
}>> {
const map = new Map<MiNote['id'], {
deltas: Record<string, number>;
pairs: ([MiUser['id'], string])[];
}>();
const pipeline = this.redisForReactions.pipeline();
for (const noteId of noteIds) {
pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
}
const results = await pipeline.exec();
const opsForEachNotes = 2;
for (let i = 0; i < noteIds.length; i++) {
const noteId = noteIds[i];
const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>;
const resultPairs = results![i * opsForEachNotes + 1][1] as string[];
const deltas = {} as Record<string, number>;
for (const [name, count] of Object.entries(resultDeltas)) {
deltas[name] = parseInt(count);
}
const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
map.set(noteId, {
deltas,
pairs,
});
}
return map;
}
// TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない
@bindThis
public async bake(): Promise<void> {
const bufferedNoteIds = [];
let cursor = '0';
do {
// https://github.com/redis/ioredis#transparent-key-prefixing
const result = await this.redisForReactions.scan(
cursor,
'MATCH',
`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`,
'COUNT',
'1000');
cursor = result[0];
bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, '')));
} while (cursor !== '0');
const bufferedMap = await this.getMany(bufferedNoteIds);
// clear
const pipeline = this.redisForReactions.pipeline();
for (const noteId of bufferedNoteIds) {
pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`);
pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`);
}
await pipeline.exec();
// TODO: SQL一個にまとめたい
for (const [noteId, buffered] of bufferedMap) {
const sql = Object.entries(buffered.deltas)
.map(([reaction, count]) =>
`jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`)
.join(' || ');
this.notesRepository.createQueryBuilder().update()
.set({
reactions: () => sql,
reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')),
})
.where('id = :id', { id: noteId })
.execute();
}
}
}

View File

@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import type { import type {
MiMeta,
MiRole, MiRole,
MiRoleAssignment, MiRoleAssignment,
RoleAssignmentsRepository, RoleAssignmentsRepository,
@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js'; import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -59,6 +59,11 @@ export type RolePolicies = {
userEachUserListsLimit: number; userEachUserListsLimit: number;
rateLimitFactor: number; rateLimitFactor: number;
avatarDecorationLimit: number; avatarDecorationLimit: number;
canImportAntennas: boolean;
canImportBlocking: boolean;
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@ -89,6 +94,11 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50, userEachUserListsLimit: 50,
rateLimitFactor: 1, rateLimitFactor: 1,
avatarDecorationLimit: 1, avatarDecorationLimit: 1,
canImportAntennas: true,
canImportBlocking: true,
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
}; };
@Injectable() @Injectable()
@ -103,8 +113,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@Inject(DI.redis) @Inject(DI.meta)
private redisClient: Redis.Redis, private meta: MiMeta,
@Inject(DI.redisForTimelines) @Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis, private redisForTimelines: Redis.Redis,
@ -121,7 +131,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@Inject(DI.roleAssignmentsRepository) @Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository, private roleAssignmentsRepository: RoleAssignmentsRepository,
private metaService: MetaService,
private cacheService: CacheService, private cacheService: CacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
@ -343,8 +352,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis @bindThis
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> { public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch(); const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
if (userId == null) return basePolicies; if (userId == null) return basePolicies;
@ -392,6 +400,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
}; };
} }

View File

@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { DataSource, IsNull } from 'typeorm'; import { DataSource, IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -20,7 +20,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js'; import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserService } from '@/core/UserService.js'; import { UserService } from '@/core/UserService.js';
@Injectable() @Injectable()
@ -29,6 +28,9 @@ export class SignupService {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -39,7 +41,6 @@ export class SignupService {
private userService: UserService, private userService: UserService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private usersChart: UsersChart, private usersChart: UsersChart,
) { ) {
@ -88,8 +89,7 @@ export class SignupService {
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
if (!opts.ignorePreservedUsernames && !isTheFirstUser) { if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
const instance = await this.metaService.fetch(true); const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) { if (isPreserved) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
} }

View File

@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; 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 InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { ThinUser } from '@/queue/types.js'; import type { ThinUser } from '@/queue/types.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit {
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private metaService: MetaService,
private notificationService: NotificationService, private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: UserWebhookService, private webhookService: UserWebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService, private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) { ) {
@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
followee.isLocked || followee.isLocked ||
(followeeProfile.carefulBot && follower.isBot) || (followeeProfile.carefulBot && follower.isBot) ||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host)) (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
) { ) {
let autoAccept = false; let autoAccept = false;
@ -307,14 +305,14 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => { this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, true); this.instanceChart.updateFollowing(i.host, true);
} }
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => { this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, true); this.instanceChart.updateFollowers(i.host, true);
} }
}); });
@ -439,14 +437,14 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => { this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false); this.instanceChart.updateFollowing(i.host, false);
} }
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => { this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false); this.instanceChart.updateFollowers(i.host, false);
} }
}); });

View File

@ -12,10 +12,9 @@ import {
} from '@simplewebauthn/server'; } from '@simplewebauthn/server';
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository } from '@/models/_.js'; import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { MiUser } from '@/models/_.js'; import { MiUser } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { import type {
@ -23,7 +22,6 @@ import type {
AuthenticatorTransportFuture, AuthenticatorTransportFuture,
CredentialDeviceType, CredentialDeviceType,
PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialDescriptorFuture,
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON, RegistrationResponseJSON,
} from '@simplewebauthn/types'; } from '@simplewebauthn/types';
@ -31,33 +29,33 @@ import type {
@Injectable() @Injectable()
export class WebAuthnService { export class WebAuthnService {
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.userSecurityKeysRepository) @Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository, private userSecurityKeysRepository: UserSecurityKeysRepository,
private metaService: MetaService,
) { ) {
} }
@bindThis @bindThis
public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> { public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
const instance = await this.metaService.fetch();
return { return {
origin: this.config.url, origin: this.config.url,
rpId: this.config.hostname, rpId: this.config.hostname,
rpName: instance.name ?? this.config.host, rpName: this.meta.name ?? this.config.host,
rpIcon: instance.iconUrl ?? undefined, rpIcon: this.meta.iconUrl ?? undefined,
}; };
} }
@bindThis @bindThis
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> { public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
const relyingParty = await this.getRelyingParty(); const relyingParty = this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({ const keys = await this.userSecurityKeysRepository.findBy({
userId: userId, userId: userId,
}); });
@ -104,7 +102,7 @@ export class WebAuthnService {
await this.redisClient.del(`webauthn:challenge:${userId}`); await this.redisClient.del(`webauthn:challenge:${userId}`);
const relyingParty = await this.getRelyingParty(); const relyingParty = this.getRelyingParty();
let verification; let verification;
try { try {
@ -143,7 +141,7 @@ export class WebAuthnService {
@bindThis @bindThis
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> { public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
const relyingParty = await this.getRelyingParty(); const relyingParty = this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({ const keys = await this.userSecurityKeysRepository.findBy({
userId: userId, userId: userId,
}); });
@ -209,7 +207,7 @@ export class WebAuthnService {
} }
} }
const relyingParty = await this.getRelyingParty(); const relyingParty = this.getRelyingParty();
let verification; let verification;
try { try {

View File

@ -18,7 +18,6 @@ import { NoteUpdateService } from '@/core/NoteUpdateService.js';
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
import { AppLockService } from '@/core/AppLockService.js'; import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { MetaService } from '@/core/MetaService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -26,7 +25,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { MessagingService } from '@/core/MessagingService.js'; import { MessagingService } from '@/core/MessagingService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, FollowRequestsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta, MessagingMessagesRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -50,6 +49,9 @@ export class ApInboxService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -69,7 +71,6 @@ export class ApInboxService {
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private abuseReportService: AbuseReportService, private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService, private apAudienceService: ApAudienceService,
@ -322,8 +323,7 @@ export class ApInboxService {
} }
// アナウンス先をブロックしてたら中断 // アナウンス先をブロックしてたら中断
const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
const relays = await this.relayService.getAcceptedRelays(); const relays = await this.relayService.getAcceptedRelays();
const fromRelay = !!actor.inbox && relays.map(r => r.inbox).includes(actor.inbox); const fromRelay = !!actor.inbox && relays.map(r => r.inbox).includes(actor.inbox);

View File

@ -207,7 +207,7 @@ export class ApRequestService {
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) { if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
const html = await res.text(); const html = await res.text();
const window = new Window({ const { window, happyDOM } = new Window({
settings: { settings: {
disableJavaScriptEvaluation: true, disableJavaScriptEvaluation: true,
disableJavaScriptFileLoading: true, disableJavaScriptFileLoading: true,
@ -241,7 +241,7 @@ export class ApRequestService {
} catch (e) { } catch (e) {
// something went wrong parsing the HTML, ignore the whole thing // something went wrong parsing the HTML, ignore the whole thing
} finally { } finally {
window.close(); happyDOM.close().catch(err => {});
} }
} }
//#endregion //#endregion

View File

@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm'; import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -29,6 +28,7 @@ export class Resolver {
constructor( constructor(
private config: Config, private config: Config,
private meta: MiMeta,
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
@ -36,7 +36,6 @@ export class Resolver {
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private metaService: MetaService,
private apRequestService: ApRequestService, private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -94,8 +93,7 @@ export class Resolver {
return await this.resolveLocal(value); return await this.resolveLocal(value);
} }
const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
throw new Error('Instance is blocked'); throw new Error('Instance is blocked');
} }
@ -178,6 +176,9 @@ export class ApResolverService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -195,7 +196,6 @@ export class ApResolverService {
private utilityService: UtilityService, private utilityService: UtilityService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private metaService: MetaService,
private apRequestService: ApRequestService, private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -208,6 +208,7 @@ export class ApResolverService {
public createResolver(): Resolver { public createResolver(): Resolver {
return new Resolver( return new Resolver(
this.config, this.config,
this.meta,
this.usersRepository, this.usersRepository,
this.notesRepository, this.notesRepository,
this.pollsRepository, this.pollsRepository,
@ -215,7 +216,6 @@ export class ApResolverService {
this.followRequestsRepository, this.followRequestsRepository,
this.utilityService, this.utilityService,
this.instanceActorService, this.instanceActorService,
this.metaService,
this.apRequestService, this.apRequestService,
this.httpRequestService, this.httpRequestService,
this.apRendererService, this.apRendererService,

View File

@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { MetaService } from '@/core/MetaService.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
@ -24,10 +23,12 @@ export class ApImageService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private metaService: MetaService,
private apResolverService: ApResolverService, private apResolverService: ApResolverService,
private driveService: DriveService, private driveService: DriveService,
private apLoggerService: ApLoggerService, private apLoggerService: ApLoggerService,
@ -63,12 +64,10 @@ export class ApImageService {
this.logger.info(`Creating the Image: ${image.url}`); this.logger.info(`Creating the Image: ${image.url}`);
const instance = await this.metaService.fetch();
// Cache if remote file cache is on AND either // Cache if remote file cache is on AND either
// 1. remote sensitive file is also on // 1. remote sensitive file is also on
// 2. or the image is not sensitive // 2. or the image is not sensitive
const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
const file = await this.driveService.uploadFromUrl({ const file = await this.driveService.uploadFromUrl({
url: image.url, url: image.url,

View File

@ -6,13 +6,12 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, MessagingMessagesRepository, NotesRepository, PollsRepository } from '@/models/_.js'; import type { PollsRepository, EmojisRepository, MiMeta, MessagingMessagesRepository, NotesRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import { MetaService } from '@/core/MetaService.js';
import { AppLockService } from '@/core/AppLockService.js'; import { AppLockService } from '@/core/AppLockService.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { NoteCreateService } from '@/core/NoteCreateService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js';
@ -49,6 +48,9 @@ export class ApNoteService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.pollsRepository) @Inject(DI.pollsRepository)
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
@ -75,7 +77,6 @@ export class ApNoteService {
private apImageService: ApImageService, private apImageService: ApImageService,
private apQuestionService: ApQuestionService, private apQuestionService: ApQuestionService,
private apEventService: ApEventService, private apEventService: ApEventService,
private metaService: MetaService,
private messagingService: MessagingService, private messagingService: MessagingService,
private appLockService: AppLockService, private appLockService: AppLockService,
private pollService: PollService, private pollService: PollService,
@ -194,7 +195,7 @@ export class ApNoteService {
/** /**
* *
*/ */
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
if (hasProhibitedWords) { if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
} }
@ -452,8 +453,7 @@ export class ApNoteService {
const uri = getApId(value); const uri = getApId(value);
// ブロックしていたら中断 // ブロックしていたら中断
const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
throw new StatusError('blocked host', 451); throw new StatusError('blocked host', 451);
} }

View File

@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService; private driveFileEntityService: DriveFileEntityService;
private idService: IdService; private idService: IdService;
private globalEventService: GlobalEventService; private globalEventService: GlobalEventService;
private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService; private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService; private fetchInstanceMetadataService: FetchInstanceMetadataService;
private cacheService: CacheService; private cacheService: CacheService;
@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@ -114,7 +115,6 @@ export class ApPersonService implements OnModuleInit {
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService'); this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.cacheService = this.moduleRef.get('CacheService'); this.cacheService = this.moduleRef.get('CacheService');
@ -493,10 +493,10 @@ export class ApPersonService implements OnModuleInit {
this.cacheService.uriPersonCache.set(user.uri, user); this.cacheService.uriPersonCache.set(user.uri, user);
// Register host // Register host
this.federatedInstanceService.fetch(host).then(async i => { this.federatedInstanceService.fetch(host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.newUser(i.host); this.instanceChart.newUser(i.host);
} }
}); });

View File

@ -5,10 +5,9 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
import { AppLockService } from '@/core/AppLockService.js'; import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import Chart from '../core.js'; import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js'; import { ChartLoggerService } from '../ChartLoggerService.js';
@ -24,13 +23,15 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private metaService: MetaService,
private appLockService: AppLockService, private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService, private chartLoggerService: ChartLoggerService,
) { ) {
@ -43,8 +44,6 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
} }
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> { protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
const meta = await this.metaService.fetch();
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host') .select('instance.host')
.where('instance.suspensionState != \'none\''); .where('instance.suspensionState != \'none\'');
@ -65,21 +64,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.followingsRepository.createQueryBuilder('following') this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)') .select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL') .where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne() .getRawOne()
.then(x => parseInt(x.count, 10)), .then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following') this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followerHost)') .select('COUNT(DISTINCT following.followerHost)')
.where('following.followerHost IS NOT NULL') .where('following.followerHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne() .getRawOne()
.then(x => parseInt(x.count, 10)), .then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following') this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)') .select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL') .where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
.setParameters(pubsubSubQuery.getParameters()) .setParameters(pubsubSubQuery.getParameters())
@ -88,7 +87,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance') this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)') .select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`) .where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'') .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false') .andWhere('instance.isNotResponding = false')
.getRawOne() .getRawOne()
@ -96,7 +95,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance') this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)') .select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'') .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false') .andWhere('instance.isNotResponding = false')
.getRawOne() .getRawOne()

View File

@ -3,19 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { MiMeta } from '@/models/_.js';
@Injectable() @Injectable()
export class InstanceEntityService { export class InstanceEntityService {
constructor( constructor(
private metaService: MetaService, @Inject(DI.meta)
private meta: MiMeta,
private roleService: RoleService, private roleService: RoleService,
private utilityService: UtilityService, private utilityService: UtilityService,
@ -27,7 +30,6 @@ export class InstanceEntityService {
instance: MiInstance, instance: MiInstance,
me?: { id: MiUser['id']; } | null | undefined, me?: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> { ): Promise<Packed<'FederationInstance'>> {
const meta = await this.metaService.fetch();
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
return { return {
@ -41,7 +43,7 @@ export class InstanceEntityService {
isNotResponding: instance.isNotResponding, isNotResponding: instance.isNotResponding,
isSuspended: instance.suspensionState !== 'none', isSuspended: instance.suspensionState !== 'none',
suspensionState: instance.suspensionState, suspensionState: instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
softwareName: instance.softwareName, softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion, softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations, openRegistrations: instance.openRegistrations,
@ -49,8 +51,8 @@ export class InstanceEntityService {
description: instance.description, description: instance.description,
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail, maintainerEmail: instance.maintainerEmail,
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host), isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl, faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor, themeColor: instance.themeColor,

View File

@ -10,7 +10,6 @@ import type { Packed } from '@/misc/json-schema.js';
import type { MiMeta } from '@/models/Meta.js'; import type { MiMeta } from '@/models/Meta.js';
import type { AdsRepository } from '@/models/_.js'; import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js';
@ -24,11 +23,13 @@ export class MetaEntityService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.adsRepository) @Inject(DI.adsRepository)
private adsRepository: AdsRepository, private adsRepository: AdsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
) { } ) { }
@ -37,7 +38,7 @@ export class MetaEntityService {
let instance = meta; let instance = meta;
if (!instance) { if (!instance) {
instance = await this.metaService.fetch(); instance = this.meta;
} }
const ads = await this.adsRepository.createQueryBuilder('ads') const ads = await this.adsRepository.createQueryBuilder('ads')
@ -141,7 +142,7 @@ export class MetaEntityService {
let instance = meta; let instance = meta;
if (!instance) { if (!instance) {
instance = await this.metaService.fetch(); instance = this.meta;
} }
const packed = await this.pack(instance); const packed = await this.pack(instance);

View File

@ -11,29 +11,45 @@ import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, EventsRepository } from '@/models/_.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, EventsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js'; import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js'; import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
function mergeReactions(src: Record<string, number>, delta: Record<string, number>) {
const reactions = { ...src };
for (const [name, count] of Object.entries(delta)) {
if (reactions[name] != null) {
reactions[name] += count;
} else {
reactions[name] = count;
}
}
return reactions;
}
@Injectable() @Injectable()
export class NoteEntityService implements OnModuleInit { export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private driveFileEntityService: DriveFileEntityService; private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
private reactionService: ReactionService; private reactionService: ReactionService;
private reactionsBufferingService: ReactionsBufferingService;
private idService: IdService; private idService: IdService;
private noteLoader = new DebounceLoader(this.findNoteOrFail); private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -62,6 +78,8 @@ export class NoteEntityService implements OnModuleInit {
//private driveFileEntityService: DriveFileEntityService, //private driveFileEntityService: DriveFileEntityService,
//private customEmojiService: CustomEmojiService, //private customEmojiService: CustomEmojiService,
//private reactionService: ReactionService, //private reactionService: ReactionService,
//private reactionsBufferingService: ReactionsBufferingService,
//private idService: IdService,
) { ) {
} }
@ -70,6 +88,7 @@ export class NoteEntityService implements OnModuleInit {
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.reactionService = this.moduleRef.get('ReactionService'); this.reactionService = this.moduleRef.get('ReactionService');
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
} }
@ -301,6 +320,7 @@ export class NoteEntityService implements OnModuleInit {
skipHide?: boolean; skipHide?: boolean;
withReactionAndUserPairCache?: boolean; withReactionAndUserPairCache?: boolean;
_hint_?: { _hint_?: {
bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
myReactions: Map<MiNote['id'], string | null>; myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>> packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
@ -317,6 +337,20 @@ export class NoteEntityService implements OnModuleInit {
const note = typeof src === 'object' ? src : await this.noteLoader.load(src); const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
const host = note.userHost; const host = note.userHost;
const bufferedReactions = opts._hint_?.bufferedReactions != null
? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] })
: this.meta.enableReactionsBuffering
? await this.reactionsBufferingService.get(note.id)
: { deltas: {}, pairs: [] };
const reactions = mergeReactions(this.reactionService.convertLegacyReactions(note.reactions), bufferedReactions.deltas ?? {});
for (const [name, count] of Object.entries(reactions)) {
if (count <= 0) {
delete reactions[name];
}
}
const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
let text = note.text; let text = note.text;
if (note.name && (note.url ?? note.uri)) { if (note.name && (note.url ?? note.uri)) {
@ -329,7 +363,7 @@ export class NoteEntityService implements OnModuleInit {
: await this.channelsRepository.findOneBy({ id: note.channelId }) : await this.channelsRepository.findOneBy({ id: note.channelId })
: null; : null;
const reactionEmojiNames = Object.keys(note.reactions) const reactionEmojiNames = Object.keys(reactions)
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis([note])); await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis([note]));
@ -353,10 +387,10 @@ export class NoteEntityService implements OnModuleInit {
disableRightClick: note.disableRightClick || undefined, disableRightClick: note.disableRightClick || undefined,
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0), reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
reactions: this.reactionService.convertLegacyReactions(note.reactions), reactions: reactions,
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds, fileIds: note.fileIds,
@ -396,8 +430,12 @@ export class NoteEntityService implements OnModuleInit {
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
event: note.hasEvent ? this.populateEvent(note) : undefined, event: note.hasEvent ? this.populateEvent(note) : undefined,
...(meId && Object.keys(note.reactions).length > 0 ? { ...(meId && Object.keys(reactions).length > 0 ? {
myReaction: this.populateMyReaction(note, meId, options?._hint_), myReaction: this.populateMyReaction({
id: note.id,
reactions: reactions,
reactionAndUserPairCache: reactionAndUserPairCache,
}, meId, options?._hint_),
} : {}), } : {}),
} : {}), } : {}),
}); });
@ -420,6 +458,8 @@ export class NoteEntityService implements OnModuleInit {
) { ) {
if (notes.length === 0) return []; if (notes.length === 0) return [];
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
const meId = me ? me.id : null; const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], string | null>(); const myReactionsMap = new Map<MiNote['id'], string | null>();
if (meId) { if (meId) {
@ -430,23 +470,33 @@ export class NoteEntityService implements OnModuleInit {
for (const note of notes) { for (const note of notes) {
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0); const reactionsCount = Object.values(mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) { if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null); myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) { } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
if (pairInBuffer) {
myReactionsMap.set(note.renote.id, pairInBuffer[1]);
} else {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId)); const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null); myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
}
} else { } else {
idsNeedFetchMyReaction.add(note.renote.id); idsNeedFetchMyReaction.add(note.renote.id);
} }
} else { } else {
if (note.id < oldId) { if (note.id < oldId) {
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); const reactionsCount = Object.values(mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) { if (reactionsCount === 0) {
myReactionsMap.set(note.id, null); myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length) { } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
if (pairInBuffer) {
myReactionsMap.set(note.id, pairInBuffer[1]);
} else {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null); myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
}
} else { } else {
idsNeedFetchMyReaction.add(note.id); idsNeedFetchMyReaction.add(note.id);
} }
@ -481,6 +531,7 @@ export class NoteEntityService implements OnModuleInit {
return await Promise.all(notes.map(n => this.pack(n, me, { return await Promise.all(notes.map(n => this.pack(n, me, {
...options, ...options,
_hint_: { _hint_: {
bufferedReactions,
myReactions: myReactionsMap, myReactions: myReactionsMap,
packedFiles, packedFiles,
packedUsers, packedUsers,

View File

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import si from 'systeminformation'; import si from 'systeminformation';
import Xev from 'xev'; import Xev from 'xev';
import * as osUtils from 'os-utils'; import * as osUtils from 'os-utils';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
const ev = new Xev(); const ev = new Xev();
@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown {
private intervalId: NodeJS.Timeout | null = null; private intervalId: NodeJS.Timeout | null = null;
constructor( constructor(
private metaService: MetaService, @Inject(DI.meta)
private meta: MiMeta,
) { ) {
} }
@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown {
*/ */
@bindThis @bindThis
public async start(): Promise<void> { public async start(): Promise<void> {
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return; if (!this.meta.enableServerMachineStats) return;
const log = [] as any[]; const log = [] as any[];

View File

@ -6,12 +6,14 @@
export const DI = { export const DI = {
config: Symbol('config'), config: Symbol('config'),
db: Symbol('db'), db: Symbol('db'),
meta: Symbol('meta'),
meilisearch: Symbol('meilisearch'), meilisearch: Symbol('meilisearch'),
cloudLogging: Symbol('cloudLogging'), cloudLogging: Symbol('cloudLogging'),
redis: Symbol('redis'), redis: Symbol('redis'),
redisForPub: Symbol('redisForPub'), redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'), redisForSub: Symbol('redisForSub'),
redisForTimelines: Symbol('redisForTimelines'), redisForTimelines: Symbol('redisForTimelines'),
redisForReactions: Symbol('redisForReactions'),
redisForJobQueue: Symbol('redisForJobQueue'), redisForJobQueue: Symbol('redisForJobQueue'),
//#region Repositories //#region Repositories

View File

@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => { export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
const index = request.url.indexOf('?'); const index = request.url.indexOf('?');
if (~index) { if (~index) {
reply.redirect(301, request.url.slice(0, index)); reply.redirect(request.url.slice(0, index), 301);
} }
done(); done();
}; };

View File

@ -697,6 +697,11 @@ export class MiMeta {
}) })
public perUserListTimelineCacheMax: number; public perUserListTimelineCacheMax: number;
@Column('boolean', {
default: false,
})
public enableReactionsBuffering: boolean;
@Column('integer', { @Column('integer', {
default: 0, default: 0,
}) })

View File

@ -272,6 +272,26 @@ export const packedRolePoliciesSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canImportAntennas: {
type: 'boolean',
optional: false, nullable: false,
},
canImportBlocking: {
type: 'boolean',
optional: false, nullable: false,
},
canImportFollowing: {
type: 'boolean',
optional: false, nullable: false,
},
canImportMuting: {
type: 'boolean',
optional: false, nullable: false,
},
canImportUserLists: {
type: 'boolean',
optional: false, nullable: false,
},
canEditNote: { canEditNote: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -14,6 +14,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
@ -52,6 +53,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ResyncChartsProcessorService, ResyncChartsProcessorService,
CleanChartsProcessorService, CleanChartsProcessorService,
CheckExpiredMutingsProcessorService, CheckExpiredMutingsProcessorService,
BakeBufferedReactionsProcessorService,
CleanProcessorService, CleanProcessorService,
DeleteDriveFilesProcessorService, DeleteDriveFilesProcessorService,
ExportCustomEmojisProcessorService, ExportCustomEmojisProcessorService,

View File

@ -41,6 +41,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueLoggerService } from './QueueLoggerService.js';
@ -124,6 +125,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private cleanChartsProcessorService: CleanChartsProcessorService, private cleanChartsProcessorService: CleanChartsProcessorService,
private aggregateRetentionProcessorService: AggregateRetentionProcessorService, private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private cleanProcessorService: CleanProcessorService, private cleanProcessorService: CleanProcessorService,
) { ) {
this.logger = this.queueLoggerService.logger; this.logger = this.queueLoggerService.logger;
@ -153,6 +155,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'cleanCharts': return this.cleanChartsProcessorService.process(); case 'cleanCharts': return this.cleanChartsProcessorService.process();
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'clean': return this.cleanProcessorService.process(); case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`); default: throw new Error(`unrecognized job type ${job.name} for system`);
} }

View File

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
@Injectable()
export class BakeBufferedReactionsProcessorService {
private logger: Logger;
constructor(
@Inject(DI.meta)
private meta: MiMeta,
private reactionsBufferingService: ReactionsBufferingService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
}
@bindThis
public async process(): Promise<void> {
if (!this.meta.enableReactionsBuffering) {
this.logger.info('Reactions buffering is disabled. Skipping...');
return;
}
this.logger.info('Baking buffered reactions...');
await this.reactionsBufferingService.bake();
this.logger.succ('All buffered reactions baked.');
}
}

View File

@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq'; import * as Bull from 'bullmq';
import { Not } from 'typeorm'; import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { InstancesRepository } from '@/models/_.js'; import type { InstancesRepository, MiMeta } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { MetaService } from '@/core/MetaService.js';
import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
@ -31,10 +30,12 @@ export class DeliverProcessorService {
private latest: string | null; private latest: string | null;
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private metaService: MetaService,
private utilityService: UtilityService, private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService, private fetchInstanceMetadataService: FetchInstanceMetadataService,
@ -53,8 +54,7 @@ export class DeliverProcessorService {
const { host } = new URL(job.data.to); const { host } = new URL(job.data.to);
// ブロックしてたら中断 // ブロックしてたら中断
const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) {
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
return 'skip (blocked)'; return 'skip (blocked)';
} }
@ -88,7 +88,7 @@ export class DeliverProcessorService {
this.apRequestChart.deliverSucc(); this.apRequestChart.deliverSucc();
this.federationChart.deliverd(i.host, true); this.federationChart.deliverd(i.host, true);
if (meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true); this.instanceChart.requestSent(i.host, true);
} }
}); });
@ -120,7 +120,7 @@ export class DeliverProcessorService {
this.apRequestChart.deliverFail(); this.apRequestChart.deliverFail();
this.federationChart.deliverd(i.host, false); this.federationChart.deliverd(i.host, false);
if (meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false); this.instanceChart.requestSent(i.host, false);
} }
}); });

View File

@ -4,11 +4,10 @@
*/ */
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import httpSignature from '@peertube/http-signature'; import httpSignature from '@peertube/http-signature';
import * as Bull from 'bullmq'; import * as Bull from 'bullmq';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { MetaService } from '@/core/MetaService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
@ -28,14 +27,18 @@ import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type { InboxJobData } from '../types.js'; import type { InboxJobData } from '../types.js';
import { MiMeta } from '@/models/Meta.js';
import { DI } from '@/di-symbols.js';
@Injectable() @Injectable()
export class InboxProcessorService { export class InboxProcessorService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
private utilityService: UtilityService, private utilityService: UtilityService,
private metaService: MetaService,
private apInboxService: ApInboxService, private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService, private fetchInstanceMetadataService: FetchInstanceMetadataService,
@ -64,8 +67,7 @@ export class InboxProcessorService {
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
// ブロックしてたら中断 // ブロックしてたら中断
const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
return `Blocked request: ${host}`; return `Blocked request: ${host}`;
} }
@ -166,7 +168,7 @@ export class InboxProcessorService {
// ブロックしてたら中断 // ブロックしてたら中断
const ldHost = this.utilityService.extractDbHost(authUser.user.uri); const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) {
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
} }
} else { } else {
@ -197,7 +199,7 @@ export class InboxProcessorService {
this.apRequestChart.inbox(); this.apRequestChart.inbox();
this.federationChart.inbox(i.host); this.federationChart.inbox(i.host);
if (meta.enableChartsForFederatedInstances) { if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host); this.instanceChart.requestReceived(i.host);
} }
}); });

View File

@ -82,7 +82,7 @@ export class FileServerService {
.catch(err => this.errorHandler(request, reply, err)); .catch(err => this.errorHandler(request, reply, err));
}); });
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`); return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
}); });
done(); done();
}); });
@ -147,12 +147,12 @@ export class FileServerService {
url.searchParams.set('static', '1'); url.searchParams.set('static', '1');
file.cleanup(); file.cleanup();
return await reply.redirect(301, url.toString()); return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) { } else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) { if (externalThumbnail) {
file.cleanup(); file.cleanup();
return await reply.redirect(301, externalThumbnail); return await reply.redirect(externalThumbnail, 301);
} }
image = await this.videoProcessingService.generateVideoThumbnail(file.path); image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@ -167,7 +167,7 @@ export class FileServerService {
url.searchParams.set('url', file.url); url.searchParams.set('url', file.url);
file.cleanup(); file.cleanup();
return await reply.redirect(301, url.toString()); return await reply.redirect(url.toString(), 301);
} }
} }
@ -314,8 +314,8 @@ export class FileServerService {
} }
return await reply.redirect( return await reply.redirect(
301,
url.toString(), url.toString(),
301,
); );
} }

View File

@ -27,6 +27,9 @@ export class HealthServerService {
@Inject(DI.redisForTimelines) @Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis, private redisForTimelines: Redis.Redis,
@Inject(DI.redisForReactions)
private redisForReactions: Redis.Redis,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@ -43,6 +46,7 @@ export class HealthServerService {
this.redisForPub.ping(), this.redisForPub.ping(),
this.redisForSub.ping(), this.redisForSub.ping(),
this.redisForTimelines.ping(), this.redisForTimelines.ping(),
this.redisForReactions.ping(),
this.db.query('SELECT 1'), this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []), ...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503)); ]).then(() => 200, () => 503));

View File

@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js'; import { ApiServerService } from './api/ApiServerService.js';
@ -44,6 +43,9 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -53,7 +55,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
private metaService: MetaService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private apiServerService: ApiServerService, private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService, private openApiServerService: OpenApiServerService,
@ -165,8 +166,8 @@ export class ServerService implements OnApplicationShutdown {
} }
return await reply.redirect( return await reply.redirect(
301,
url.toString(), url.toString(),
301,
); );
}); });
@ -193,7 +194,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Content-Type', 'image/png'); reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400'); reply.header('Cache-Control', 'public, max-age=86400');
if ((await this.metaService.fetch()).enableIdenticonGeneration) { if (this.meta.enableIdenticonGeneration) {
return await genIdenticon(request.params.x); return await genIdenticon(request.params.x);
} else { } else {
return reply.redirect('/static-assets/avatar.png'); return reply.redirect('/static-assets/avatar.png');

View File

@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js'; import type { MiAccessToken } from '@/models/AccessToken.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import type { UserIpsRepository } from '@/models/_.js'; import type { MiMeta, UserIpsRepository } from '@/models/_.js';
import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
@ -41,13 +40,15 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout; private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor( constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.userIpsRepository) @Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository, private userIpsRepository: UserIpsRepository,
private metaService: MetaService,
private authenticateService: AuthenticateService, private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService, private rateLimiterService: RateLimiterService,
private roleService: RoleService, private roleService: RoleService,
@ -65,15 +66,6 @@ export class ApiCallService implements OnApplicationShutdown {
let statusCode = err.httpStatusCode; let statusCode = err.httpStatusCode;
if (err.httpStatusCode === 401) { if (err.httpStatusCode === 401) {
reply.header('WWW-Authenticate', 'Bearer realm="CherryPick"'); reply.header('WWW-Authenticate', 'Bearer realm="CherryPick"');
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="CherryPick", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
} else if (err.kind === 'permission') {
// (ROLE_PERMISSION_DENIEDは関係ない)
if (err.code === 'PERMISSION_DENIED') {
reply.header('WWW-Authenticate', `Bearer realm="CherryPick", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
} else if (err.code === 'RATE_LIMIT_EXCEEDED') { } else if (err.code === 'RATE_LIMIT_EXCEEDED') {
const info: unknown = err.info; const info: unknown = err.info;
const unixEpochInSeconds = Date.now(); const unixEpochInSeconds = Date.now();
@ -84,6 +76,15 @@ export class ApiCallService implements OnApplicationShutdown {
} else { } else {
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`); this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
} }
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="CherryPick", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
} else if (err.kind === 'permission') {
// (ROLE_PERMISSION_DENIEDは関係ない)
if (err.code === 'PERMISSION_DENIED') {
reply.header('WWW-Authenticate', `Bearer realm="CherryPick", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
} else if (!statusCode) { } else if (!statusCode) {
statusCode = 500; statusCode = 500;
} }
@ -266,9 +267,8 @@ export class ApiCallService implements OnApplicationShutdown {
} }
@bindThis @bindThis
private async logIp(request: FastifyRequest, user: MiLocalUser) { private logIp(request: FastifyRequest, user: MiLocalUser) {
const meta = await this.metaService.fetch(); if (!this.meta.enableIpLogging) return;
if (!meta.enableIpLogging) return;
const ip = request.ip; const ip = request.ip;
const ips = this.userIpHistories.get(user.id); const ips = this.userIpHistories.get(user.id);
if (ips == null || !ips.has(ip)) { if (ips == null || !ips.has(ip)) {

View File

@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { CaptchaService } from '@/core/CaptchaService.js'; import { CaptchaService } from '@/core/CaptchaService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { SignupService } from '@/core/SignupService.js'; import { SignupService } from '@/core/SignupService.js';
@ -28,6 +27,9 @@ export class SignupApiService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -45,7 +47,6 @@ export class SignupApiService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private captchaService: CaptchaService, private captchaService: CaptchaService,
private signupService: SignupService, private signupService: SignupService,
private signinService: SigninService, private signinService: SigninService,
@ -72,31 +73,29 @@ export class SignupApiService {
) { ) {
const body = request.body; const body = request.body;
const instance = await this.metaService.fetch(true);
// Verify *Captcha // Verify *Captcha
// ただしテスト時はこの機構は障害となるため無効にする // ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err); throw new FastifyReplyError(400, err);
}); });
} }
if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
throw new FastifyReplyError(400, err); throw new FastifyReplyError(400, err);
}); });
} }
if (instance.enableRecaptcha && instance.recaptchaSecretKey) { if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err); throw new FastifyReplyError(400, err);
}); });
} }
if (instance.enableTurnstile && instance.turnstileSecretKey) { if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => { await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
throw new FastifyReplyError(400, err); throw new FastifyReplyError(400, err);
}); });
} }
@ -108,7 +107,7 @@ export class SignupApiService {
const invitationCode = body['invitationCode']; const invitationCode = body['invitationCode'];
const emailAddress = body['emailAddress']; const emailAddress = body['emailAddress'];
if (instance.emailRequiredForSignup) { if (this.meta.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') { if (emailAddress == null || typeof emailAddress !== 'string') {
reply.code(400); reply.code(400);
return; return;
@ -123,7 +122,7 @@ export class SignupApiService {
let ticket: MiRegistrationTicket | null = null; let ticket: MiRegistrationTicket | null = null;
if (instance.disableRegistration) { if (this.meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') { if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400); reply.code(400);
return; return;
@ -144,7 +143,7 @@ export class SignupApiService {
} }
// メアド認証が有効の場合 // メアド認証が有効の場合
if (instance.emailRequiredForSignup) { if (this.meta.emailRequiredForSignup) {
// メアド認証済みならエラー // メアド認証済みならエラー
if (ticket.usedBy) { if (ticket.usedBy) {
reply.code(400); reply.code(400);
@ -162,7 +161,7 @@ export class SignupApiService {
} }
} }
if (instance.emailRequiredForSignup) { if (this.meta.emailRequiredForSignup) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
} }
@ -172,7 +171,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'USED_USERNAME'); throw new FastifyReplyError(400, 'USED_USERNAME');
} }
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) { if (isPreserved) {
throw new FastifyReplyError(400, 'DENIED_USERNAME'); throw new FastifyReplyError(400, 'DENIED_USERNAME');
} }

View File

@ -429,6 +429,10 @@ export const meta = {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableReactionsBuffering: {
type: 'boolean',
optional: false, nullable: false,
},
notesPerOneAd: { notesPerOneAd: {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
@ -716,6 +720,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
enableReactionsBuffering: instance.enableReactionsBuffering,
notesPerOneAd: instance.notesPerOneAd, notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl, summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled, urlPreviewEnabled: instance.urlPreviewEnabled,

View File

@ -163,6 +163,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' },
enableReactionsBuffering: { type: 'boolean' },
notesPerOneAd: { type: 'integer' }, notesPerOneAd: { type: 'integer' },
silencedHosts: { silencedHosts: {
type: 'array', type: 'array',
@ -705,6 +706,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
} }
if (ps.enableReactionsBuffering !== undefined) {
set.enableReactionsBuffering = ps.enableReactionsBuffering;
}
if (ps.notesPerOneAd !== undefined) { if (ps.notesPerOneAd !== undefined) {
set.notesPerOneAd = ps.notesPerOneAd; set.notesPerOneAd = ps.notesPerOneAd;
} }

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms'; import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js'; import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -20,6 +19,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],
@ -88,10 +89,12 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private utilityService: UtilityService, private utilityService: UtilityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private metaService: MetaService,
private apResolverService: ApResolverService, private apResolverService: ApResolverService,
private apDbResolverService: ApDbResolverService, private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
@ -113,8 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@bindThis @bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断 // ブロックしてたら中断
const fetchedMeta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(this.serverSettings.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
let local = await this.mergePack(me, ...await Promise.all([ let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri), this.apDbResolverService.getUserFromApId(uri),

View File

@ -5,14 +5,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -58,6 +56,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -68,16 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const serverSettings = await this.metaService.fetch();
const channel = await this.channelsRepository.findOneBy({ const channel = await this.channelsRepository.findOneBy({
id: ps.channelId, id: ps.channelId,
}); });
@ -88,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me); if (me) this.activeUsersChart.read(me);
if (!serverSettings.enableFanoutTimeline) { if (!this.serverSettings.enableFanoutTimeline) {
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
} }

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
@ -41,14 +40,10 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private metaService: MetaService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
// Calculate drive usage
const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
const policies = await this.roleService.getUserPolicies(me.id); const policies = await this.roleService.getUserPolicies(me.id);

View File

@ -4,14 +4,15 @@
*/ */
import ms from 'ms'; import ms from 'ms';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
tags: ['drive'], tags: ['drive'],
@ -73,8 +74,10 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private metaService: MetaService,
private driveService: DriveService, private driveService: DriveService,
) { ) {
super(meta, paramDef, async (ps, me, _1, _2, file, cleanup, ip, headers) => { super(meta, paramDef, async (ps, me, _1, _2, file, cleanup, ip, headers) => {
@ -91,8 +94,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
const instance = await this.metaService.fetch();
try { try {
// Create file // Create file
const driveFile = await this.driveService.addFile({ const driveFile = await this.driveService.addFile({
@ -103,8 +104,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId, folderId: ps.folderId,
force: ps.force, force: ps.force,
sensitive: ps.isSensitive, sensitive: ps.isSensitive,
requestIp: instance.enableIpLogging ? ip : null, requestIp: this.serverSettings.enableIpLogging ? ip : null,
requestHeaders: instance.enableIpLogging ? headers : null, requestHeaders: this.serverSettings.enableIpLogging ? headers : null,
}); });
return await this.driveFileEntityService.pack(driveFile, { self: true }); return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) { } catch (err) {

View File

@ -16,6 +16,7 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canImportAntennas',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {

View File

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canImportBlocking',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {

View File

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canImportFollowing',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),

View File

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canImportMuting',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {

View File

@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canImportUserLists',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),

View File

@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms'; import ms from 'ms';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js'; import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmailService } from '@/core/EmailService.js'; import { EmailService } from '@/core/EmailService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -15,7 +15,6 @@ import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js'; import { UserAuthService } from '@/core/UserAuthService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -70,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private metaService: MetaService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private emailService: EmailService, private emailService: EmailService,
private userAuthService: UserAuthService, private userAuthService: UserAuthService,
@ -105,7 +106,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!res.available) { if (!res.available) {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
} else if ((await this.metaService.fetch()).emailRequiredForSignup) { } else if (this.serverSettings.emailRequiredForSignup) {
throw new ApiError(meta.errors.emailRequired); throw new ApiError(meta.errors.emailRequired);
} }

View File

@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';

View File

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -75,6 +74,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -88,7 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService, private cacheService: CacheService,
private queryService: QueryService, private queryService: QueryService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private metaService: MetaService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -102,9 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
const serverSettings = await this.metaService.fetch(); if (!this.serverSettings.enableFanoutTimeline) {
if (!serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({ const timeline = await this.getFromDb({
untilId, untilId,
sinceId, sinceId,
@ -158,7 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
allowPartial: ps.allowPartial, allowPartial: ps.allowPartial,
me, me,
redisTimelines: timelineConfig, redisTimelines: timelineConfig,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback, useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true, alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes, excludePureRenotes: !ps.withRenotes,
withCats: ps.withCats, withCats: ps.withCats,

View File

@ -5,16 +5,14 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js'; import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -67,6 +65,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -74,10 +75,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService, private roleService: RoleService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private idService: IdService, private idService: IdService,
private cacheService: CacheService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -90,9 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
const serverSettings = await this.metaService.fetch(); if (!this.serverSettings.enableFanoutTimeline) {
if (!serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({ const timeline = await this.getFromDb({
untilId, untilId,
sinceId, sinceId,
@ -117,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
limit: ps.limit, limit: ps.limit,
allowPartial: ps.allowPartial, allowPartial: ps.allowPartial,
me, me,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback, useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: redisTimelines:
ps.withFiles ? ['localTimelineWithFiles'] ps.withFiles ? ['localTimelineWithFiles']
: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']

View File

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
export const meta = { export const meta = {
@ -57,6 +56,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -70,15 +72,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const serverSettings = await this.metaService.fetch(); if (!this.serverSettings.enableFanoutTimeline) {
if (!serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({ const timeline = await this.getFromDb({
untilId, untilId,
sinceId, sinceId,
@ -110,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
limit: ps.limit, limit: ps.limit,
allowPartial: ps.allowPartial, allowPartial: ps.allowPartial,
me, me,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback, useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
alwaysIncludeMyNotes: true, alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes, excludePureRenotes: !ps.withRenotes,

View File

@ -5,17 +5,18 @@
import { URLSearchParams } from 'node:url'; import { URLSearchParams } from 'node:url';
import fs from 'node:fs'; import fs from 'node:fs';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { translate } from '@vitalets/google-translate-api'; import { translate } from '@vitalets/google-translate-api';
import { TranslationServiceClient } from '@google-cloud/translate'; import { TranslationServiceClient } from '@google-cloud/translate';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],
@ -68,9 +69,11 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private getterService: GetterService, private getterService: GetterService,
private metaService: MetaService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private roleService: RoleService, private roleService: RoleService,
) { ) {
@ -93,15 +96,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return; return;
} }
const instance = await this.metaService.fetch();
const translatorServices = [ const translatorServices = [
'deepl', 'deepl',
'google_no_api', 'google_no_api',
'ctav3', 'ctav3',
]; ];
if (instance.translatorType == null || !translatorServices.includes(instance.translatorType)) { if (this.serverSettings.translatorType == null || !translatorServices.includes(this.serverSettings.translatorType)) {
throw new ApiError(meta.errors.noTranslateService); throw new ApiError(meta.errors.noTranslateService);
} }
@ -109,12 +110,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
let translationResult; let translationResult;
if (instance.translatorType === 'deepl') { if (this.serverSettings.translatorType === 'deepl') {
if (instance.deeplAuthKey == null) { if (this.serverSettings.deeplAuthKey == null) {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
translationResult = await this.translateDeepL((note.cw ? note.cw + '\n' : '') + note.text, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType); translationResult = await this.translateDeepL((note.cw ? note.cw + '\n' : '') + note.text, targetLang, this.serverSettings.deeplAuthKey, this.serverSettings.deeplIsPro, this.serverSettings.translatorType);
} else if (instance.translatorType === 'google_no_api') { } else if (this.serverSettings.translatorType === 'google_no_api') {
let targetLang = ps.targetLang; let targetLang = ps.targetLang;
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
@ -123,14 +124,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return { return {
sourceLang: raw.src, sourceLang: raw.src,
text: text, text: text,
translator: translatorServices, translator: this.serverSettings.translatorType, // 修正点: 配列ではなく単一の文字列
}; };
} else if (instance.translatorType === 'ctav3') { } else if (this.serverSettings.translatorType === 'ctav3') {
if (instance.ctav3SaKey == null) return Promise.resolve(204); if (this.serverSettings.ctav3SaKey == null) return Promise.resolve(204);
else if (instance.ctav3ProjectId == null) return Promise.resolve(204); else if (this.serverSettings.ctav3ProjectId == null) return Promise.resolve(204);
else if (instance.ctav3Location == null) return Promise.resolve(204); else if (this.serverSettings.ctav3Location == null) return Promise.resolve(204);
translationResult = await this.apiCloudTranslationAdvanced( translationResult = await this.apiCloudTranslationAdvanced(
(note.cw ? note.cw + '\n' : '') + note.text, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType, (note.cw ? note.cw + '\n' : '') + note.text, targetLang, this.serverSettings.ctav3SaKey, this.serverSettings.ctav3ProjectId, this.serverSettings.ctav3Location, this.serverSettings.ctav3Model, this.serverSettings.ctav3Glossary, this.serverSettings.translatorType,
); );
} else { } else {
throw new Error('Unsupported translator type'); throw new Error('Unsupported translator type');

View File

@ -5,16 +5,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -70,6 +68,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -81,11 +82,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private cacheService: CacheService,
private idService: IdService, private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -100,9 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchList); throw new ApiError(meta.errors.noSuchList);
} }
const serverSettings = await this.metaService.fetch(); if (!this.serverSettings.enableFanoutTimeline) {
if (!serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb(list, { const timeline = await this.getFromDb(list, {
untilId, untilId,
sinceId, sinceId,
@ -126,7 +123,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
limit: ps.limit, limit: ps.limit,
allowPartial: ps.allowPartial, allowPartial: ps.allowPartial,
me, me,
useDbFallback: serverSettings.enableFanoutTimelineDbFallback, useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
alwaysIncludeMyNotes: true, alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes, excludePureRenotes: !ps.withRenotes,

View File

@ -5,11 +5,10 @@
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js'; import type { MiMeta, UsersRepository } from '@/models/_.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -38,16 +37,16 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private metaService: MetaService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const meta = await this.metaService.fetch(); const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
usernameLower: acct.username.toLowerCase(), usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(), host: acct.host ?? IsNull(),
}))); })));

View File

@ -5,9 +5,10 @@
import * as os from 'node:os'; import * as os from 'node:os';
import si from 'systeminformation'; import si from 'systeminformation';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js'; import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: false, requireCredential: false,
@ -73,10 +74,11 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private metaService: MetaService, @Inject(DI.meta)
private serverSettings: MiMeta,
) { ) {
super(meta, paramDef, async () => { super(meta, paramDef, async () => {
if (!(await this.metaService.fetch()).enableServerMachineStats) return { if (!this.serverSettings.enableServerMachineStats) return {
machine: '?', machine: '?',
cpu: { cpu: {
model: '?', model: '?',

View File

@ -5,9 +5,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { SwSubscriptionsRepository } from '@/models/_.js'; import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
@ -62,11 +61,13 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.swSubscriptionsRepository) @Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository, private swSubscriptionsRepository: SwSubscriptionsRepository,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -78,12 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
publickey: ps.publickey, publickey: ps.publickey,
}); });
const instance = await this.metaService.fetch(true);
if (exist != null) { if (exist != null) {
return { return {
state: 'already-subscribed' as const, state: 'already-subscribed' as const,
key: instance.swPublicKey, key: this.serverSettings.swPublicKey,
userId: me.id, userId: me.id,
endpoint: exist.endpoint, endpoint: exist.endpoint,
sendReadMessage: exist.sendReadMessage, sendReadMessage: exist.sendReadMessage,
@ -103,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return { return {
state: 'subscribed' as const, state: 'subscribed' as const,
key: instance.swPublicKey, key: this.serverSettings.swPublicKey,
userId: me.id, userId: me.id,
endpoint: ps.endpoint, endpoint: ps.endpoint,
sendReadMessage: ps.sendReadMessage, sendReadMessage: ps.sendReadMessage,

View File

@ -5,11 +5,10 @@
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { localUsernameSchema } from '@/models/User.js'; import { localUsernameSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { MetaService } from '@/core/MetaService.js';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],
@ -39,13 +38,14 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.usedUsernamesRepository) @Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository, private usedUsernamesRepository: UsedUsernamesRepository,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const exist = await this.usersRepository.countBy({ const exist = await this.usersRepository.countBy({
@ -55,8 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() }); const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
const meta = await this.metaService.fetch(); const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
return { return {
available: exist === 0 && exist2 === 0 && !isPreserved, available: exist === 0 && exist2 === 0 && !isPreserved,

View File

@ -5,14 +5,13 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js'; import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
@ -68,6 +67,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -76,15 +78,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService, private cacheService: CacheService,
private idService: IdService, private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isSelf = me && (me.id === ps.userId); const isSelf = me && (me.id === ps.userId);
const serverSettings = await this.metaService.fetch();
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
// early return if me is blocked by requesting user // early return if me is blocked by requesting user
@ -95,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
if (!serverSettings.enableFanoutTimeline) { if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({ const timeline = await this.getFromDb({
untilId, untilId,
sinceId, sinceId,

View File

@ -5,16 +5,17 @@
import { URLSearchParams } from 'node:url'; import { URLSearchParams } from 'node:url';
import fs from 'node:fs'; import fs from 'node:fs';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { translate } from '@vitalets/google-translate-api'; import { translate } from '@vitalets/google-translate-api';
import { TranslationServiceClient } from '@google-cloud/translate'; import { TranslationServiceClient } from '@google-cloud/translate';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],
@ -62,8 +63,10 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private getterService: GetterService, private getterService: GetterService,
private metaService: MetaService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private roleService: RoleService, private roleService: RoleService,
) { ) {
@ -82,15 +85,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return; return;
} }
const instance = await this.metaService.fetch();
const translatorServices = [ const translatorServices = [
'deepl', 'deepl',
'google_no_api', 'google_no_api',
'ctav3', 'ctav3',
]; ];
if (instance.translatorType == null || !translatorServices.includes(instance.translatorType)) { if (this.serverSettings.translatorType == null || !translatorServices.includes(this.serverSettings.translatorType)) {
return Promise.resolve(204); // Promise.resolveで204をラップする return Promise.resolve(204); // Promise.resolveで204をラップする
} }
@ -98,12 +99,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
let translationResult; let translationResult;
if (instance.translatorType === 'deepl') { if (this.serverSettings.translatorType === 'deepl') {
if (instance.deeplAuthKey == null) { if (this.serverSettings.deeplAuthKey == null) {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
translationResult = await this.translateDeepL(target.description, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType); translationResult = await this.translateDeepL(target.description, targetLang, this.serverSettings.deeplAuthKey, this.serverSettings.deeplIsPro, this.serverSettings.translatorType);
} else if (instance.translatorType === 'google_no_api') { } else if (this.serverSettings.translatorType === 'google_no_api') {
let targetLang = ps.targetLang; let targetLang = ps.targetLang;
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
@ -112,14 +113,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return { return {
sourceLang: raw.src, sourceLang: raw.src,
text: text, text: text,
translator: instance.translatorType, // 修正点: 配列ではなく単一の文字列 translator: this.serverSettings.translatorType, // 修正点: 配列ではなく単一の文字列
}; };
} else if (instance.translatorType === 'ctav3') { } else if (this.serverSettings.translatorType === 'ctav3') {
if (instance.ctav3SaKey == null) return Promise.resolve(204); if (this.serverSettings.ctav3SaKey == null) return Promise.resolve(204);
else if (instance.ctav3ProjectId == null) return Promise.resolve(204); else if (this.serverSettings.ctav3ProjectId == null) return Promise.resolve(204);
else if (instance.ctav3Location == null) return Promise.resolve(204); else if (this.serverSettings.ctav3Location == null) return Promise.resolve(204);
translationResult = await this.apiCloudTranslationAdvanced( translationResult = await this.apiCloudTranslationAdvanced(
target.description, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType, target.description, targetLang, this.serverSettings.ctav3SaKey, this.serverSettings.ctav3ProjectId, this.serverSettings.ctav3Location, this.serverSettings.ctav3Model, this.serverSettings.ctav3Glossary, this.serverSettings.translatorType,
); );
} else { } else {
throw new Error('Unsupported translator type'); throw new Error('Unsupported translator type');

View File

@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { createBullBoard } from '@bull-board/api'; import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js';
import { FastifyAdapter } from '@bull-board/fastify'; import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify';
import ms from 'ms'; import ms from 'ms';
import sharp from 'sharp'; import sharp from 'sharp';
import pug from 'pug'; import pug from 'pug';
@ -24,7 +24,6 @@ import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js';
import type { import type {
DbQueue, DbQueue,
DeliverQueue, DeliverQueue,
@ -73,6 +72,9 @@ export class ClientServerService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -109,7 +111,6 @@ export class ClientServerService {
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
private metaService: MetaService,
private urlPreviewService: UrlPreviewService, private urlPreviewService: UrlPreviewService,
private feedService: FeedService, private feedService: FeedService,
private roleService: RoleService, private roleService: RoleService,
@ -129,32 +130,30 @@ export class ClientServerService {
@bindThis @bindThis
private async manifestHandler(reply: FastifyReply) { private async manifestHandler(reply: FastifyReply) {
const instance = await this.metaService.fetch(true);
let manifest = { let manifest = {
// 空文字列の場合右辺を使いたいため // 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'short_name': instance.shortName || instance.name || this.config.host, 'short_name': this.meta.shortName || this.meta.name || this.config.host,
// 空文字列の場合右辺を使いたいため // 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'name': instance.name || this.config.host, 'name': this.meta.name || this.config.host,
'start_url': '/', 'start_url': '/',
'display': 'standalone', 'display': 'standalone',
'background_color': '#95e3e8', 'background_color': '#95e3e8',
// 空文字列の場合右辺を使いたいため // 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'theme_color': instance.themeColor || '#ffa9c3', 'theme_color': this.meta.themeColor || '#ffa9c3',
'icons': [{ 'icons': [{
// 空文字列の場合右辺を使いたいため // 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app192IconUrl || '/static-assets/icons/192.png', 'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
'sizes': '192x192', 'sizes': '192x192',
'type': 'image/png', 'type': 'image/png',
'purpose': 'maskable', 'purpose': 'maskable',
}, { }, {
// 空文字列の場合右辺を使いたいため // 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app512IconUrl || '/static-assets/icons/512.png', 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '512x512', 'sizes': '512x512',
'type': 'image/png', 'type': 'image/png',
'purpose': 'maskable', 'purpose': 'maskable',
@ -178,7 +177,7 @@ export class ClientServerService {
manifest = { manifest = {
...manifest, ...manifest,
...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride), ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride),
}; };
reply.header('Cache-Control', 'max-age=300'); reply.header('Cache-Control', 'max-age=300');
@ -240,7 +239,7 @@ export class ClientServerService {
} }
}); });
const serverAdapter = new FastifyAdapter(); const bullBoardServerAdapter = new BullBoardFastifyAdapter();
createBullBoard({ createBullBoard({
queues: [ queues: [
@ -253,11 +252,11 @@ export class ClientServerService {
this.userWebhookDeliverQueue, this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue, this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)), ].map(q => new BullMQAdapter(q)),
serverAdapter, serverAdapter: bullBoardServerAdapter,
}); });
serverAdapter.setBasePath(bullBoardPath); bullBoardServerAdapter.setBasePath(bullBoardPath);
(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath }); //(fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath });
//#endregion //#endregion
fastify.register(fastifyView, { fastify.register(fastifyView, {
@ -454,9 +453,7 @@ export class ClientServerService {
// OpenSearch XML // OpenSearch XML
fastify.get('/opensearch.xml', async (request, reply) => { fastify.get('/opensearch.xml', async (request, reply) => {
const meta = await this.metaService.fetch(); const name = this.meta.name ?? 'CherryPick';
const name = meta.name ?? 'CherryPick';
let content = ''; let content = '';
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">'; content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
content += `<ShortName>${name}</ShortName>`; content += `<ShortName>${name}</ShortName>`;
@ -473,14 +470,13 @@ export class ClientServerService {
//#endregion //#endregion
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => { const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', { return await reply.view('base', {
img: meta.bannerUrl, img: this.meta.bannerUrl,
url: this.config.url, url: this.config.url,
title: meta.name ?? 'CherryPick', title: this.meta.name ?? 'CherryPick',
desc: meta.description, desc: this.meta.description,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
...data, ...data,
}); });
}; };
@ -558,7 +554,6 @@ export class ClientServerService {
if (user != null) { if (user != null) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const meta = await this.metaService.fetch();
const me = profile.fields const me = profile.fields
? profile.fields ? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/)) .filter(filed => filed.value != null && filed.value.match(/^https?:/))
@ -574,7 +569,7 @@ export class ClientServerService {
user, profile, me, user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub, sub: request.params.sub,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
// リモートユーザーなので // リモートユーザーなので
@ -612,7 +607,6 @@ export class ClientServerService {
if (note) { if (note) {
const _note = await this.noteEntityService.pack(note); const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) { if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai'); reply.header('X-Robots-Tag', 'noimageai');
@ -624,7 +618,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl, avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting // TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note), summary: getNoteSummary(_note),
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -649,7 +643,6 @@ export class ClientServerService {
if (page) { if (page) {
const _page = await this.pageEntityService.pack(page); const _page = await this.pageEntityService.pack(page);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
const meta = await this.metaService.fetch();
if (['public'].includes(page.visibility)) { if (['public'].includes(page.visibility)) {
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
} else { } else {
@ -663,7 +656,7 @@ export class ClientServerService {
page: _page, page: _page,
profile, profile,
avatarUrl: _page.user.avatarUrl, avatarUrl: _page.user.avatarUrl,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -679,7 +672,6 @@ export class ClientServerService {
if (flash) { if (flash) {
const _flash = await this.flashEntityService.pack(flash); const _flash = await this.flashEntityService.pack(flash);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) { if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai'); reply.header('X-Robots-Tag', 'noimageai');
@ -689,7 +681,7 @@ export class ClientServerService {
flash: _flash, flash: _flash,
profile, profile,
avatarUrl: _flash.user.avatarUrl, avatarUrl: _flash.user.avatarUrl,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -705,7 +697,6 @@ export class ClientServerService {
if (clip && clip.isPublic) { if (clip && clip.isPublic) {
const _clip = await this.clipEntityService.pack(clip); const _clip = await this.clipEntityService.pack(clip);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) { if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai'); reply.header('X-Robots-Tag', 'noimageai');
@ -715,7 +706,7 @@ export class ClientServerService {
clip: _clip, clip: _clip,
profile, profile,
avatarUrl: _clip.user.avatarUrl, avatarUrl: _clip.user.avatarUrl,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -729,7 +720,6 @@ export class ClientServerService {
if (post) { if (post) {
const _post = await this.galleryPostEntityService.pack(post); const _post = await this.galleryPostEntityService.pack(post);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) { if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai'); reply.header('X-Robots-Tag', 'noimageai');
@ -739,7 +729,7 @@ export class ClientServerService {
post: _post, post: _post,
profile, profile,
avatarUrl: _post.user.avatarUrl, avatarUrl: _post.user.avatarUrl,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -754,11 +744,10 @@ export class ClientServerService {
if (channel) { if (channel) {
const _channel = await this.channelEntityService.pack(channel); const _channel = await this.channelEntityService.pack(channel);
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', { return await reply.view('channel', {
channel: _channel, channel: _channel,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -773,11 +762,10 @@ export class ClientServerService {
if (game) { if (game) {
const _game = await this.reversiGameEntityService.packDetail(game); const _game = await this.reversiGameEntityService.packDetail(game);
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=3600'); reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', { return await reply.view('reversi-game', {
game: _game, game: _game,
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -798,28 +786,90 @@ export class ClientServerService {
//#endregion //#endregion
//#region embed pages //#region embed pages
fastify.get('/embed/*', async (request, reply) => { fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => {
const meta = await this.metaService.fetch(); reply.removeHeader('X-Frame-Options');
const user = await this.usersRepository.findOneBy({
id: request.params.user,
});
if (user == null) return;
if (user.host != null) return;
const _user = await this.userEntityService.pack(user);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'CherryPick',
...await this.generateCommonPugData(this.meta),
embedCtx: htmlSafeJsonStringify({
user: _user,
}),
});
});
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
const note = await this.notesRepository.findOneBy({
id: request.params.note,
});
if (note == null) return;
if (note.visibility !== 'public') return;
if (note.userHost != null) return;
const _note = await this.noteEntityService.pack(note, null, { detail: true });
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'CherryPick',
...await this.generateCommonPugData(this.meta),
embedCtx: htmlSafeJsonStringify({
note: _note,
}),
});
});
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
const clip = await this.clipsRepository.findOneBy({
id: request.params.clip,
});
if (clip == null) return;
const _clip = await this.clipEntityService.pack(clip);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: this.meta.name ?? 'CherryPick',
...await this.generateCommonPugData(this.meta),
embedCtx: htmlSafeJsonStringify({
clip: _clip,
}),
});
});
fastify.get('/embed/*', async (request, reply) => {
reply.removeHeader('X-Frame-Options'); reply.removeHeader('X-Frame-Options');
reply.header('Cache-Control', 'public, max-age=3600'); reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', { return await reply.view('base-embed', {
title: meta.name ?? 'CherryPick', title: this.meta.name ?? 'CherryPick',
...await this.generateCommonPugData(meta), ...await this.generateCommonPugData(this.meta),
}); });
}); });
fastify.get('/_info_card_', async (request, reply) => { fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
reply.removeHeader('X-Frame-Options'); reply.removeHeader('X-Frame-Options');
return await reply.view('info-card', { return await reply.view('info-card', {
version: this.config.version, version: this.config.version,
basedMisskeyVersion: this.config.basedMisskeyVersion, basedMisskeyVersion: this.config.basedMisskeyVersion,
host: this.config.host, host: this.config.host,
meta: meta, meta: this.meta,
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
}); });

View File

@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly';
import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { query } from '@/misc/prelude/url.js'; import { query } from '@/misc/prelude/url.js';
@ -26,7 +25,9 @@ export class UrlPreviewService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
private metaService: MetaService, @Inject(DI.meta)
private meta: MiMeta,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
@ -62,9 +63,7 @@ export class UrlPreviewService {
return; return;
} }
const meta = await this.metaService.fetch(); if (!this.meta.urlPreviewEnabled) {
if (!meta.urlPreviewEnabled) {
reply.code(403); reply.code(403);
return { return {
error: new ApiError({ error: new ApiError({
@ -75,14 +74,14 @@ export class UrlPreviewService {
}; };
} }
this.logger.info(meta.urlPreviewSummaryProxyUrl this.logger.info(this.meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${url}@${lang} ...` ? `(Proxy) Getting preview of ${url}@${lang} ...`
: `Getting preview of ${url}@${lang} ...`); : `Getting preview of ${url}@${lang} ...`);
try { try {
const summary = meta.urlPreviewSummaryProxyUrl const summary = this.meta.urlPreviewSummaryProxyUrl
? await this.fetchSummaryFromProxy(url, meta, lang) ? await this.fetchSummaryFromProxy(url, this.meta, lang)
: await this.fetchSummary(url, meta, lang); : await this.fetchSummary(url, this.meta, lang);
this.logger.succ(`Got preview of ${url}: ${summary.title}`); this.logger.succ(`Got preview of ${url}: ${summary.title}`);

View File

@ -40,9 +40,12 @@ html(class='embed')
var VERSION = "#{version}"; var VERSION = "#{version}";
var CLIENT_ENTRY = "#{entry.file}"; var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now) script(type='application/json' id='cherrypick_meta' data-generated-at=now)
!= metaJson != metaJson
script(type='application/json' id='cherrypick_embedCtx' data-generated-at=now)
!= embedCtx
script script
include ../boot.embed.js include ../boot.embed.js

View File

@ -70,7 +70,7 @@ html
var VERSION = "#{version}"; var VERSION = "#{version}";
var CLIENT_ENTRY = "#{entry.file}"; var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now) script(type='application/json' id='cherrypick_meta' data-generated-at=now)
!= metaJson != metaJson
script script

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