diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index acd169bf43..42ac18de1b 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -106,7 +106,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
-# You can set scope to local (default value) or global
+# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch:
@@ -136,6 +136,21 @@ redis:
id: 'aidx'
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
@@ -185,7 +200,7 @@ proxyRemoteFiles: true
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
-# but exceptions can be made from the following settings. Default value is "undefined".
+# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'
diff --git a/.config/example.yml b/.config/example.yml
index b0b7f14059..b11cbd1373 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -205,6 +205,21 @@ redis:
id: 'aidx'
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 31b6212cb5..344edbd65d 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -10,7 +10,7 @@
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
},
"forwardPorts": [3000],
- "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
+ "postCreateCommand": "/bin/bash .devcontainer/init.sh",
"customizations": {
"vscode": {
"extensions": [
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index 7ea0929469..beefcfd0a2 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -132,6 +132,21 @@ redis:
id: 'aidx'
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 2809cd2ca4..a52d086fb6 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -8,6 +8,7 @@ services:
volumes:
- ../:/workspace:cached
+ - node_modules:/workspace/node_modules
command: sleep infinity
@@ -46,6 +47,7 @@ services:
volumes:
postgres-data:
redis-data:
+ node_modules:
networks:
internal_network:
diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh
index 729e1a9d2d..55fb1e6fa6 100755
--- a/.devcontainer/init.sh
+++ b/.devcontainer/init.sh
@@ -2,7 +2,8 @@
set -xe
-sudo chown -R node /workspace
+sudo chown node node_modules
+git config --global --add safe.directory /workspace
git submodule update --init
corepack install
corepack enable
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index e8b65dc3b9..5acad83336 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,3 +2,7 @@ contact_links:
- name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3
about: Chat freely about Misskey
+ # 仮
+ - name: 💬 Start discussion
+ url: https://github.com/misskey-dev/misskey/discussions
+ about: The official forum to join conversation and ask question
diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml
index eee7a78fed..968971dd8d 100644
--- a/.github/workflows/dockle.yml
+++ b/.github/workflows/dockle.yml
@@ -13,10 +13,12 @@ jobs:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
+ DOCKLE_VERSION: 0.4.14
steps:
- uses: actions/checkout@v4.1.1
- - run: |
- curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
+ - name: Download and install dockle v${{ env.DOCKLE_VERSION }}
+ run: |
+ curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml
index 890cb047bd..86ee0b3fb5 100644
--- a/.github/workflows/release-edit-with-push.yml
+++ b/.github/workflows/release-edit-with-push.yml
@@ -23,7 +23,7 @@ jobs:
# headがrelease/かつopenのPRを1つ取得
- name: Get PR
run: |
- echo "pr_number=$(gh pr list --limit 1 --head "${{ github.ref_name }}" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
+ echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
- name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml
index 139503e563..a0fad0e336 100644
--- a/.github/workflows/release-with-ready.yml
+++ b/.github/workflows/release-with-ready.yml
@@ -22,9 +22,11 @@ jobs:
# PR情報を取得
- name: Get PR
run: |
- pr_json=$(gh pr view ${{ github.event.pull_request.number }} --json isDraft,headRefName)
+ pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
id: get_pr
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number }}
release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: check
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7aae6585c6..76fed76b56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## Unreleased
+
+### General
+- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
+- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
+
+### Client
+- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
+- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
+
+### Server
+- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
+- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
+
## 2024.5.0
### Note
@@ -6,6 +20,7 @@
- 管理者向け権限 `read:admin:show-users` は `read:admin:show-user` に統合されました。必要に応じてAPIトークンを再発行してください。
### General
+- Feat: エラートラッキングにSentryを使用できるようになりました
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
- Enhance: アンテナでBotによるノートを除外できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
@@ -19,6 +34,7 @@
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
- Enhance: 配信停止の理由を表示するように
+- Enhance: サーバーのお問い合わせ先URLを設定できるようになりました
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
@@ -26,7 +42,7 @@
### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように
- Feat: 個別のお知らせにリンクで飛べるように
- (Cherry-picked from https://github.com/MisskeyIO/misskey)
+ (Based on https://github.com/MisskeyIO/misskey/pull/639)
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
@@ -72,6 +88,7 @@
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
- Fix: 連合なしの状態の読み書きができない問題を修正
- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正
+- Fix: ファイルを5つ以上添付してもテキストがないとノートが折りたたまれない問題を修正
### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
diff --git a/README.md b/README.md
index 24013a7bd8..92e8fef639 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,10 @@
## Thanks
+
+
+Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors.
+
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
diff --git a/chart/files/default.yml b/chart/files/default.yml
index 4cc291e80a..f98b8ebfee 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -152,6 +152,22 @@ redis:
# ID SETTINGS AFTER THAT!
id: "aidx"
+
+# ┌────────────────┐
+#───┘ Error tracking └──────────────────────────────────────────
+
+# Sentry is available for error tracking.
+# See the Sentry documentation for more details on options.
+
+#sentryForBackend:
+# enableNodeProfiling: true
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
+#sentryForFrontend:
+# options:
+# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
+
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
diff --git a/locales/index.d.ts b/locales/index.d.ts
index d4ded0bb5b..acdc1fc421 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -3364,6 +3364,10 @@ export interface Locale extends ILocale {
* 管理者情報が設定されていません。
*/
"noMaintainerInformationWarning": string;
+ /**
+ * 問い合わせ先URLが設定されていません。
+ */
+ "noInquiryUrlWarning": string;
/**
* Botプロテクションが設定されていません。
*/
@@ -5471,6 +5475,14 @@ export interface Locale extends ILocale {
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
*/
"fanoutTimelineDbFallbackDescription": string;
+ /**
+ * 問い合わせ先URL
+ */
+ "inquiryUrl": string;
+ /**
+ * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
+ */
+ "inquiryUrlDescription": string;
};
"_accountMigration": {
/**
@@ -9293,6 +9305,10 @@ export interface Locale extends ILocale {
* Webhookを作成
*/
"createWebhook": string;
+ /**
+ * Webhookを編集
+ */
+ "modifyWebhook": string;
/**
* 名前
*/
@@ -9339,6 +9355,72 @@ export interface Locale extends ILocale {
*/
"mention": string;
};
+ "_systemEvents": {
+ /**
+ * ユーザーから通報があったとき
+ */
+ "abuseReport": string;
+ /**
+ * ユーザーからの通報を処理したとき
+ */
+ "abuseReportResolved": string;
+ };
+ /**
+ * Webhookを削除しますか?
+ */
+ "deleteConfirm": string;
+ };
+ "_abuseReport": {
+ "_notificationRecipient": {
+ /**
+ * 通報の通知先を追加
+ */
+ "createRecipient": string;
+ /**
+ * 通報の通知先を編集
+ */
+ "modifyRecipient": string;
+ /**
+ * 通知先の種類
+ */
+ "recipientType": string;
+ "_recipientType": {
+ /**
+ * メール
+ */
+ "mail": string;
+ /**
+ * Webhook
+ */
+ "webhook": string;
+ "_captions": {
+ /**
+ * モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
+ */
+ "mail": string;
+ /**
+ * 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
+ */
+ "webhook": string;
+ };
+ };
+ /**
+ * キーワード
+ */
+ "keywords": string;
+ /**
+ * 通知先ユーザー
+ */
+ "notifiedUser": string;
+ /**
+ * 使用するWebhook
+ */
+ "notifiedWebhook": string;
+ /**
+ * 通知先を削除しますか?
+ */
+ "deleteConfirm": string;
+ };
};
"_moderationLogTypes": {
/**
@@ -9485,6 +9567,30 @@ export interface Locale extends ILocale {
* ユーザーのバナーを解除
*/
"unsetUserBanner": string;
+ /**
+ * SystemWebhookを作成
+ */
+ "createSystemWebhook": string;
+ /**
+ * SystemWebhookを更新
+ */
+ "updateSystemWebhook": string;
+ /**
+ * SystemWebhookを削除
+ */
+ "deleteSystemWebhook": string;
+ /**
+ * 通報の通知先を作成
+ */
+ "createAbuseReportNotificationRecipient": string;
+ /**
+ * 通報の通知先を更新
+ */
+ "updateAbuseReportNotificationRecipient": string;
+ /**
+ * 通報の通知先を削除
+ */
+ "deleteAbuseReportNotificationRecipient": string;
};
"_fileViewer": {
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d7ceb971af..3ac1ce82a3 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -837,6 +837,7 @@ administration: "管理"
accounts: "アカウント"
switch: "切り替え"
noMaintainerInformationWarning: "管理者情報が設定されていません。"
+noInquiryUrlWarning: "問い合わせ先URLが設定されていません。"
noBotProtectionWarning: "Botプロテクションが設定されていません。"
configure: "設定する"
postToGallery: "ギャラリーへ投稿"
@@ -1383,6 +1384,8 @@ _serverSettings:
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
+ inquiryUrl: "問い合わせ先URL"
+ inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"
@@ -2465,6 +2468,7 @@ _drivecleaner:
_webhookSettings:
createWebhook: "Webhookを作成"
+ modifyWebhook: "Webhookを編集"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
@@ -2477,6 +2481,26 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
+ _systemEvents:
+ abuseReport: "ユーザーから通報があったとき"
+ abuseReportResolved: "ユーザーからの通報を処理したとき"
+ deleteConfirm: "Webhookを削除しますか?"
+
+_abuseReport:
+ _notificationRecipient:
+ createRecipient: "通報の通知先を追加"
+ modifyRecipient: "通報の通知先を編集"
+ recipientType: "通知先の種類"
+ _recipientType:
+ mail: "メール"
+ webhook: "Webhook"
+ _captions:
+ mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
+ webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
+ keywords: "キーワード"
+ notifiedUser: "通知先ユーザー"
+ notifiedWebhook: "使用するWebhook"
+ deleteConfirm: "通知先を削除しますか?"
_moderationLogTypes:
createRole: "ロールを作成"
@@ -2515,6 +2539,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
+ createSystemWebhook: "SystemWebhookを作成"
+ updateSystemWebhook: "SystemWebhookを更新"
+ deleteSystemWebhook: "SystemWebhookを削除"
+ createAbuseReportNotificationRecipient: "通報の通知先を作成"
+ updateAbuseReportNotificationRecipient: "通報の通知先を更新"
+ deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
_fileViewer:
title: "ファイルの詳細"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 3e500f8642..f92d997b5a 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -316,6 +316,7 @@ selectFile: "选择文件"
selectFiles: "选择文件"
selectFolder: "选择文件夹"
selectFolders: "选择多个文件夹"
+fileNotSelected: "未选择文件"
renameFile: "重命名文件"
folderName: "文件夹名称"
createFolder: "创建文件夹"
@@ -2358,6 +2359,7 @@ _deck:
alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐"
addColumn: "添加列"
+ newNoteNotificationSettings: "新帖子通知设定"
configureColumn: "列设置"
swapLeft: "向左移动"
swapRight: "向右移动"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index fed7b642dc..aac3f7662c 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -316,6 +316,7 @@ selectFile: "選擇檔案"
selectFiles: "選擇檔案"
selectFolder: "選擇資料夾"
selectFolders: "選擇資料夾"
+fileNotSelected: "尚未選擇檔案"
renameFile: "重新命名檔案"
folderName: "資料夾名稱"
createFolder: "新增資料夾"
@@ -471,7 +472,7 @@ retype: "重新輸入"
noteOf: "{user}的貼文"
quoteAttached: "引用"
quoteQuestion: "是否要引用?"
-attachAsFileQuestion: "剪貼簿的文字較長。請問是否要改成附加檔案呢?"
+attachAsFileQuestion: "剪貼簿的文字較長。請問是否要將其以文字檔的方式附加呢?"
noMessagesYet: "沒有訊息"
newMessageExists: "有新的訊息"
onlyOneFileCanBeAttached: "只能加入一個附件"
@@ -1025,6 +1026,7 @@ thisPostMayBeAnnoyingHome: "發佈到首頁"
thisPostMayBeAnnoyingCancel: "退出"
thisPostMayBeAnnoyingIgnore: "直接發佈貼文"
collapseRenotes: "省略顯示已看過的轉發貼文"
+collapseRenotesDescription: "將已做過反應和轉發的貼文折疊顯示。"
internalServerError: "內部伺服器錯誤"
internalServerErrorDescription: "內部伺服器出現意外錯誤。"
copyErrorInfo: "複製錯誤資訊"
@@ -1241,8 +1243,8 @@ alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
inquiry: "聯絡我們"
_delivery:
status: "傳送狀態"
- stop: "已凍結"
- resume: "繼續傳送"
+ stop: "停止傳送"
+ resume: "恢復傳送"
_type:
none: "直播中"
manuallySuspended: "手動暫停中"
@@ -1373,6 +1375,8 @@ _serverSettings:
fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
fanoutTimelineDbFallback: "資料庫的回退"
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
+ inquiryUrl: "聯絡表單網址"
+ inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名"
@@ -2358,6 +2362,7 @@ _deck:
alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位"
addColumn: "新增欄位"
+ newNoteNotificationSettings: "新貼文通知的設定"
configureColumn: "欄位的設定"
swapLeft: "向左移動"
swapRight: "向右移動"
diff --git a/package.json b/package.json
index 6caf4fc114..b1786e16f1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2024.5.0-beta.5",
+ "version": "2024.5.0",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js
new file mode 100644
index 0000000000..4a754f81e2
--- /dev/null
+++ b/packages/backend/migration/1713656541000-abuse-report-notification.js
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AbuseReportNotification1713656541000 {
+ name = 'AbuseReportNotification1713656541000'
+
+ async up(queryRunner) {
+ await queryRunner.query(`
+ CREATE TABLE "system_webhook" (
+ "id" varchar(32) NOT NULL,
+ "isActive" boolean NOT NULL DEFAULT true,
+ "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "latestSentAt" timestamp with time zone NULL DEFAULT NULL,
+ "latestStatus" integer NULL DEFAULT NULL,
+ "name" varchar(255) NOT NULL,
+ "on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
+ "url" varchar(1024) NOT NULL,
+ "secret" varchar(1024) NOT NULL,
+ CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
+ );
+ CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
+ CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
+
+ CREATE TABLE "abuse_report_notification_recipient" (
+ "id" varchar(32) NOT NULL,
+ "isActive" boolean NOT NULL DEFAULT true,
+ "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" varchar(255) NOT NULL,
+ "method" varchar(64) NOT NULL,
+ "userId" varchar(32) NULL DEFAULT NULL,
+ "systemWebhookId" varchar(32) NULL DEFAULT NULL,
+ CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
+ CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
+ CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
+ CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
+ );
+ CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
+ `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
+ DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
+ DROP INDEX "IDX_abuse_report_notification_recipient_method";
+ DROP INDEX "IDX_abuse_report_notification_recipient_userId";
+ DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
+ DROP TABLE "abuse_report_notification_recipient";
+
+ DROP INDEX "IDX_system_webhook_isActive";
+ DROP INDEX "IDX_system_webhook_on";
+ DROP TABLE "system_webhook";
+ `);
+ }
+}
diff --git a/packages/backend/migration/1717117195275-inquiryUrl.js b/packages/backend/migration/1717117195275-inquiryUrl.js
new file mode 100644
index 0000000000..29ca31af14
--- /dev/null
+++ b/packages/backend/migration/1717117195275-inquiryUrl.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class InquiryUrl1717117195275 {
+ name = 'InquiryUrl1717117195275'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 8e29252d75..772dc8f8b5 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
- "node": ">=20.10.0"
+ "node": "^20.10.0 || ^22.0.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
@@ -86,6 +86,8 @@
"@nestjs/core": "10.3.8",
"@nestjs/testing": "10.3.8",
"@peertube/http-signature": "1.7.0",
+ "@sentry/node": "^8.5.0",
+ "@sentry/profiling-node": "^8.5.0",
"@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
@@ -157,7 +159,7 @@
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
- "re2": "1.20.10",
+ "re2": "1.21.2",
"redis-lock": "0.1.4",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts
index 80f1f7a024..d0be19664f 100644
--- a/packages/backend/src/NestLogger.ts
+++ b/packages/backend/src/NestLogger.ts
@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
const logger = new Logger('core', 'cyan');
-const nestLogger = logger.createSubLogger('nest', 'green', false);
+const nestLogger = logger.createSubLogger('nest', 'green');
export class NestLogger implements LoggerService {
/**
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 04c6ca9723..25375c3015 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
-const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const clusterLogger = logger.createSubLogger('cluster', 'orange');
const ev = new Xev();
//#region Events
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 30f9477ccf..4bc5c799cf 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -10,6 +10,8 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import Logger from '@/logger.js';
import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
@@ -23,7 +25,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');
@@ -71,6 +73,24 @@ export async function masterMain() {
bootLogger.succ('Misskey initialized');
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.disableClustering) {
if (envOption.onlyServer) {
await server();
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
index d4a7cd56e5..5d4a15b29f 100644
--- a/packages/backend/src/boot/worker.ts
+++ b/packages/backend/src/boot/worker.ts
@@ -4,13 +4,36 @@
*/
import cluster from 'node:cluster';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { envOption } from '@/env.js';
+import { loadConfig } from '@/config.js';
import { jobQueue, server } from './common.js';
/**
* Init worker process
*/
export async function workerMain() {
+ const config = loadConfig();
+
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 0ca1fa55c1..0ac521d409 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
+import * as Sentry from '@sentry/node';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial & {
@@ -56,6 +57,8 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
+ sentryForBackend?: { options: Partial; enableNodeProfiling: boolean; };
+ sentryForFrontend?: { options: Partial };
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
@@ -166,6 +169,8 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
+ sentryForBackend: { options: Partial; enableNodeProfiling: boolean; } | undefined;
+ sentryForFrontend: { options: Partial } | undefined;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
@@ -234,6 +239,8 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
+ sentryForBackend: config.sentryForBackend,
+ sentryForFrontend: config.sentryForFrontend,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..df752afcd8
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -0,0 +1,406 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
+import { Brackets, In, IsNull, Not } from 'typeorm';
+import * as Redis from 'ioredis';
+import sanitizeHtml from 'sanitize-html';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+import type {
+ AbuseReportNotificationRecipientRepository,
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiUser,
+} from '@/models/_.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportNotificationService implements OnApplicationShutdown {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ private idService: IdService,
+ private roleService: RoleService,
+ private systemWebhookService: SystemWebhookService,
+ private emailService: EmailService,
+ private metaService: MetaService,
+ private moderationLogService: ModerationLogService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ /**
+ * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
+ *
+ * @see RoleService.getModeratorIds
+ * @see GlobalEventService.publishAdminStream
+ */
+ @bindThis
+ public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const moderatorIds = await this.roleService.getModeratorIds(true, true);
+
+ for (const moderatorId of moderatorIds) {
+ for (const abuseReport of abuseReports) {
+ this.globalEventService.publishAdminStream(
+ moderatorId,
+ 'newAbuseUserReport',
+ {
+ id: abuseReport.id,
+ targetUserId: abuseReport.targetUserId,
+ reporterId: abuseReport.reporterId,
+ comment: abuseReport.comment,
+ },
+ );
+ }
+ }
+ }
+
+ /**
+ * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * メールアドレスの送信先は以下の通り.
+ * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
+ * - metaテーブルに設定されているメールアドレス
+ *
+ * @see EmailService.sendEmail
+ */
+ @bindThis
+ public async notifyMail(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
+ .filter(it => it.isActive && it.userProfile?.emailVerified)
+ .map(it => it.userProfile?.email)
+ .filter(isNotNull),
+ );
+
+ // 送信先の鮮度を保つため、毎回取得する
+ const meta = await this.metaService.fetch(true);
+ recipientEMailAddresses.push(
+ ...(meta.email ? [meta.email] : []),
+ );
+
+ if (recipientEMailAddresses.length <= 0) {
+ return;
+ }
+
+ for (const mailAddress of recipientEMailAddresses) {
+ await Promise.all(
+ abuseReports.map(it => {
+ // TODO: 送信処理はJobQueue化したい
+ return this.emailService.sendEmail(
+ mailAddress,
+ 'New Abuse Report',
+ sanitizeHtml(it.comment),
+ sanitizeHtml(it.comment),
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+ *
+ * @see SystemWebhookService.enqueueSystemWebhook
+ */
+ @bindThis
+ public async notifySystemWebhook(
+ abuseReports: MiAbuseUserReport[],
+ type: 'abuseReport' | 'abuseReportResolved',
+ ) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientWebhookIds = await this.fetchWebhookRecipients()
+ .then(it => it
+ .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
+ .map(it => it.systemWebhookId)
+ .filter(isNotNull));
+ for (const webhookId of recipientWebhookIds) {
+ await Promise.all(
+ abuseReports.map(it => {
+ return this.systemWebhookService.enqueueSystemWebhook(
+ webhookId,
+ type,
+ it,
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * 通報の通知先一覧を取得する.
+ *
+ * @param {Object} [params] クエリの取得条件
+ * @param {Object} [params.method] 取得する通知先の通知方法
+ * @param {Object} [opts] 動作時の詳細なオプション
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
+ * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchRecipients(
+ params?: {
+ ids?: MiAbuseReportNotificationRecipient['id'][],
+ method?: RecipientMethod[],
+ },
+ opts?: {
+ removeUnauthorized?: boolean,
+ joinUser?: boolean,
+ joinSystemWebhook?: boolean,
+ },
+ ): Promise {
+ const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
+
+ if (opts?.joinUser) {
+ query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
+ query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
+ }
+
+ if (opts?.joinSystemWebhook) {
+ query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
+ }
+
+ if (params?.ids) {
+ query.andWhere({ id: In(params.ids) });
+ }
+
+ if (params?.method) {
+ query.andWhere(new Brackets(qb => {
+ if (params.method?.includes('email')) {
+ qb.orWhere({ method: 'email', userId: Not(IsNull()) });
+ }
+ if (params.method?.includes('webhook')) {
+ qb.orWhere({ method: 'webhook', userId: IsNull() });
+ }
+ }));
+ }
+
+ const recipients = await query.getMany();
+ if (recipients.length <= 0) {
+ return [];
+ }
+
+ // アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
+ return (opts?.removeUnauthorized ?? true)
+ ? await this.removeUnauthorizedRecipientUsers(recipients)
+ : recipients;
+ }
+
+ /**
+ * EMailの通知先一覧を取得する.
+ * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
+ *
+ * @param {Object} [opts]
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchEMailRecipients(opts?: {
+ removeUnauthorized?: boolean
+ }): Promise {
+ return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
+ }
+
+ /**
+ * Webhookの通知先一覧を取得する.
+ * リレーション先の{@link MiSystemWebhook}も同時に取得する.
+ */
+ @bindThis
+ public fetchWebhookRecipients(): Promise {
+ return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
+ }
+
+ /**
+ * 通知先を作成する.
+ */
+ @bindThis
+ public async createRecipient(
+ params: {
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const id = this.idService.gen();
+ await this.abuseReportNotificationRecipientRepository.insert({
+ ...params,
+ id,
+ });
+
+ const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
+
+ this.moderationLogService
+ .log(updater, 'createAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: created,
+ })
+ .then();
+
+ return created;
+ }
+
+ /**
+ * 通知先を更新する.
+ */
+ @bindThis
+ public async updateRecipient(
+ params: {
+ id: MiAbuseReportNotificationRecipient['id'];
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ await this.abuseReportNotificationRecipientRepository.update(params.id, {
+ isActive: params.isActive,
+ updatedAt: new Date(),
+ name: params.name,
+ method: params.method,
+ userId: params.userId,
+ systemWebhookId: params.systemWebhookId,
+ });
+
+ const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ this.moderationLogService
+ .log(updater, 'updateAbuseReportNotificationRecipient', {
+ recipientId: params.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * 通知先を削除する.
+ */
+ @bindThis
+ public async deleteRecipient(
+ id: MiAbuseReportNotificationRecipient['id'],
+ updater: MiUser,
+ ) {
+ const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
+
+ await this.abuseReportNotificationRecipientRepository.delete(id);
+
+ this.moderationLogService
+ .log(updater, 'deleteAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: entity,
+ })
+ .then();
+ }
+
+ /**
+ * モデレータ権限を持たない(*1)通知先ユーザを削除する.
+ *
+ * *1: 以下の両方を満たすものの事を言う
+ * - 通知先にユーザIDが設定されている
+ * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
+ *
+ * @param recipients 通知先一覧の配列
+ * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
+ */
+ @bindThis
+ private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise {
+ const userRecipients = recipients.filter(it => it.userId !== null);
+ const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(isNotNull));
+ if (recipientUserIds.size <= 0) {
+ // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
+ return recipients;
+ }
+
+ // モデレータ権限の有無で通知先設定を振り分ける
+ const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
+ const authorizedUserRecipients = Array.of();
+ const unauthorizedUserRecipients = Array.of();
+ for (const recipient of userRecipients) {
+ // eslint-disable-next-line
+ if (authorizedUserIds.includes(recipient.userId!)) {
+ authorizedUserRecipients.push(recipient);
+ } else {
+ unauthorizedUserRecipients.push(recipient);
+ }
+ }
+
+ // モデレータ権限を持たない通知先をDBから削除する
+ if (unauthorizedUserRecipients.length > 0) {
+ await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
+ }
+ const nonUserRecipients = recipients.filter(it => it.userId === null);
+ return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'roleUpdated':
+ case 'roleDeleted':
+ case 'userRoleUnassigned': {
+ // 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
+ process.nextTick(async () => {
+ const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
+ userId: Not(IsNull()),
+ });
+ await this.removeUnauthorizedRecipientUsers(recipients);
+ });
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
new file mode 100644
index 0000000000..69c51509ba
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
+import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportService {
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ private idService: IdService,
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private queueService: QueueService,
+ private instanceActorService: InstanceActorService,
+ private apRendererService: ApRendererService,
+ private moderationLogService: ModerationLogService,
+ ) {
+ }
+
+ /**
+ * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
+ * - 管理者用Redisイベント
+ * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async report(params: {
+ targetUserId: MiAbuseUserReport['targetUserId'],
+ targetUserHost: MiAbuseUserReport['targetUserHost'],
+ reporterId: MiAbuseUserReport['reporterId'],
+ reporterHost: MiAbuseUserReport['reporterHost'],
+ comment: string,
+ }[]) {
+ const entities = params.map(param => {
+ return {
+ id: this.idService.gen(),
+ targetUserId: param.targetUserId,
+ targetUserHost: param.targetUserHost,
+ reporterId: param.reporterId,
+ reporterHost: param.reporterHost,
+ comment: param.comment,
+ };
+ });
+
+ const reports = Array.of();
+ for (const entity of entities) {
+ const report = await this.abuseUserReportsRepository.insertOne(entity);
+ reports.push(report);
+ }
+
+ return Promise.all([
+ this.abuseReportNotificationService.notifyAdminStream(reports),
+ this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
+ this.abuseReportNotificationService.notifyMail(reports),
+ ]);
+ }
+
+ /**
+ * 通報を解決し、その内容を下記の手段で管理者各位に通知する.
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @param operator 通報を処理したユーザ
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async resolve(
+ params: {
+ reportId: string;
+ forward: boolean;
+ }[],
+ operator: MiUser,
+ ) {
+ const paramsMap = new Map(params.map(it => [it.reportId, it]));
+ const reports = await this.abuseUserReportsRepository.findBy({
+ id: In(params.map(it => it.reportId)),
+ });
+
+ for (const report of reports) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const ps = paramsMap.get(report.id)!;
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ resolved: true,
+ assigneeId: operator.id,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ });
+
+ if (ps.forward && report.targetUserHost != null) {
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ // eslint-disable-next-line
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+ }
+
+ this.moderationLogService
+ .log(operator, 'resolveAbuseReport', {
+ reportId: report.id,
+ report: report,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ })
+ .then();
+ }
+
+ return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
+ .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
+ }
+}
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index 9b60df2cae..40a9db01c0 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -67,7 +67,7 @@ export class AnnouncementService {
@bindThis
public async create(values: Partial, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
- const announcement = await this.announcementsRepository.insert({
+ const announcement = await this.announcementsRepository.insertOne({
id: this.idService.gen(),
updatedAt: null,
title: values.title,
@@ -79,7 +79,7 @@ export class AnnouncementService {
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
- }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
+ });
const packed = await this.announcementEntityService.pack(announcement);
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
index 21e31d79a4..8b54bbe012 100644
--- a/packages/backend/src/core/AvatarDecorationService.ts
+++ b/packages/backend/src/core/AvatarDecorationService.ts
@@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
@bindThis
public async create(options: Partial, moderator?: MiUser): Promise {
- const created = await this.avatarDecorationsRepository.insert({
+ const created = await this.avatarDecorationsRepository.insertOne({
id: this.idService.gen(),
...options,
- }).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts
index bb8be26ce6..9fd1ebad87 100644
--- a/packages/backend/src/core/ClipService.ts
+++ b/packages/backend/src/core/ClipService.ts
@@ -45,13 +45,13 @@ export class ClipService {
throw new ClipService.TooManyClipsError();
}
- const clip = await this.clipsRepository.insert({
+ const clip = await this.clipsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: name,
isPublic: isPublic,
description: description,
- }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
+ });
return clip;
}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index be80df6f1c..b5b34487ec 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -5,6 +5,13 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -56,7 +63,7 @@ import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
-import { WebhookService } from './WebhookService.js';
+import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
@@ -144,6 +151,8 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
+const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
+const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -196,7 +205,8 @@ const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
-const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
+const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
+const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -225,6 +235,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
+const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -258,6 +269,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
+const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -285,6 +297,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -337,7 +351,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -366,6 +381,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -399,6 +415,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -422,6 +439,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -474,7 +493,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -503,6 +523,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -536,6 +557,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -560,6 +582,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -612,7 +636,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -640,6 +665,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -673,6 +699,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -696,6 +723,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -748,7 +777,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -776,6 +806,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -809,6 +840,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 1c75566755..7e11b9cdca 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}, moderator?: MiUser): Promise {
- const emoji = await this.emojisRepository.insert({
+ const emoji = await this.emojisRepository.insertOne({
id: this.idService.gen(),
updatedAt: new Date(),
name: data.name,
@@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: data.isSensitive,
localOnly: data.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ });
if (data.host == null) {
this.localEmojisCache.refresh();
@@ -346,10 +346,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise> {
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
- const res = {} as any;
+ const res = {} as Record;
for (let i = 0; i < emojiNames.length; i++) {
- if (emojis[i] != null) {
- res[emojiNames[i]] = emojis[i];
+ const resolvedEmoji = emojis[i];
+ if (resolvedEmoji != null) {
+ res[emojiNames[i]] = resolvedEmoji;
}
}
return res;
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 63fa26f69d..37c5d1adf7 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -220,7 +220,7 @@ export class DriveService {
file.size = size;
file.storedInternal = false;
- return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ return await this.driveFilesRepository.insertOne(file);
} else { // use internal storage
const accessKey = randomUUID();
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
@@ -254,7 +254,7 @@ export class DriveService {
file.md5 = hash;
file.size = size;
- return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ return await this.driveFilesRepository.insertOne(file);
}
}
@@ -497,20 +497,20 @@ export class DriveService {
if (user && !force) {
// Check if there is a file with the same hash
- const much = await this.driveFilesRepository.findOneBy({
+ const matched = await this.driveFilesRepository.findOneBy({
md5: info.md5,
userId: user.id,
});
- if (much) {
- this.registerLogger.info(`file with same hash is found: ${much.id}`);
- if (sensitive && !much.isSensitive) {
+ if (matched) {
+ this.registerLogger.info(`file with same hash is found: ${matched.id}`);
+ if (sensitive && !matched.isSensitive) {
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
// Therefore, update the file to sensitive.
- await this.driveFilesRepository.update({ id: much.id }, { isSensitive: true });
- much.isSensitive = true;
+ await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
+ matched.isSensitive = true;
}
- return much;
+ return matched;
}
}
@@ -615,7 +615,7 @@ export class DriveService {
file.type = info.type.mime;
file.storedInternal = false;
- file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+ file = await this.driveFilesRepository.insertOne(file);
} catch (err) {
// duplicate key error (when already registered)
if (isDuplicateKeyValueError(err)) {
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 08f8f80a6e..435dbbae28 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -32,6 +33,7 @@ export class EmailService {
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
+ private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 66db2067d9..6799f2c5bb 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -55,11 +55,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
const index = await this.instancesRepository.findOneBy({ host });
if (index == null) {
- const i = await this.instancesRepository.insert({
+ const i = await this.instancesRepository.insertOne({
id: this.idService.gen(),
host,
firstRetrievedAt: new Date(),
- }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.federatedInstanceCache.set(host, i);
return i;
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index 8d173855f3..aa16468ecb 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -154,7 +154,7 @@ export class FetchInstanceMetadataService {
throw new Error('No wellknown links');
}
- const links = wellknown.links as any[];
+ const links = wellknown.links as ({ rel: string, href: string; })[];
const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 90efd63f3a..a70743bed2 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -227,6 +228,9 @@ export interface InternalEventTypes {
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
+ systemWebhookCreated: MiSystemWebhook;
+ systemWebhookDeleted: MiSystemWebhook;
+ systemWebhookUpdated: MiSystemWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts
index 96d9b09992..f102461a50 100644
--- a/packages/backend/src/core/LoggerService.ts
+++ b/packages/backend/src/core/LoggerService.ts
@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
- public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
- return new Logger(domain, color, store);
+ public getLogger(domain: string, color?: KEYWORD | undefined) {
+ return new Logger(domain, color);
}
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index e5580f36d1..0c9de117d2 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js';
import { QueueService } from '@/core/QueueService.js';
@@ -205,7 +205,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -606,7 +606,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'note', {
+ this.queueService.userWebhookDeliver(webhook, 'note', {
note: noteObj,
});
}
@@ -633,7 +633,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'reply', {
+ this.queueService.userWebhookDeliver(webhook, 'reply', {
note: noteObj,
});
}
@@ -656,7 +656,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'renote', {
+ this.queueService.userWebhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
@@ -788,7 +788,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'mention', {
+ this.queueService.userWebhookDeliver(webhook, 'mention', {
note: detailPackedNote,
});
}
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index 216734e9e5..b10b8e5899 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { QUEUE, baseQueueOptions } from '@/queue/const.js';
+import { baseQueueOptions, QUEUE } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
+import {
+ DeliverJobData,
+ EndedPollNotificationJobData,
+ InboxJobData,
+ RelationshipJobData,
+ UserWebhookDeliverJobData,
+ SystemWebhookDeliverJobData,
+} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
-import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue>;
export type EndedPollNotificationQueue = Bull.Queue;
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue;
export type ObjectStorageQueue = Bull.Queue;
-export type WebhookDeliverQueue = Bull.Queue;
+export type UserWebhookDeliverQueue = Bull.Queue;
+export type SystemWebhookDeliverQueue = Bull.Queue;
const $system: Provider = {
provide: 'queue:system',
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
inject: [DI.config],
};
-const $webhookDeliver: Provider = {
- provide: 'queue:webhookDeliver',
- useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
+const $userWebhookDeliver: Provider = {
+ provide: 'queue:userWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
+ inject: [DI.config],
+};
+
+const $systemWebhookDeliver: Provider = {
+ provide: 'queue:systemWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
exports: [
$system,
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
})
export class QueueModule implements OnApplicationShutdown {
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {}
public async dispose(): Promise {
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
- this.webhookDeliverQueue.close(),
+ this.userWebhookDeliverQueue.close(),
+ this.systemWebhookDeliverQueue.close(),
]);
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index c258a22927..80827a500b 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
+import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
-import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import type {
+ DbJobData,
+ DeliverJobData,
+ RelationshipJobData,
+ SystemWebhookDeliverJobData,
+ ThinUser,
+ UserWebhookDeliverJobData,
+} from '../queue/types.js';
+import type {
+ DbQueue,
+ DeliverQueue,
+ EndedPollNotificationQueue,
+ InboxQueue,
+ ObjectStorageQueue,
+ RelationshipQueue,
+ SystemQueue,
+ UserWebhookDeliverQueue,
+ SystemWebhookDeliverQueue,
+} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
-import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@@ -31,7 +49,8 @@ export class QueueService {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
@@ -431,9 +450,13 @@ export class QueueService {
});
}
+ /**
+ * @see UserWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
@bindThis
- public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
- const data = {
+ public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+ const data: UserWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
@@ -444,7 +467,33 @@ export class QueueService {
eventId: randomUUID(),
};
- return this.webhookDeliverQueue.add(webhook.id, data, {
+ return this.userWebhookDeliverQueue.add(webhook.id, data, {
+ attempts: 4,
+ backoff: {
+ type: 'custom',
+ },
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
+ /**
+ * @see SystemWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
+ @bindThis
+ public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ const data: SystemWebhookDeliverJobData = {
+ type,
+ content,
+ webhookId: webhook.id,
+ to: webhook.url,
+ secret: webhook.secret,
+ createdAt: Date.now(),
+ eventId: randomUUID(),
+ };
+
+ return this.systemWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index e9dc9b57af..8dd3d64f5b 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -53,11 +53,11 @@ export class RelayService {
@bindThis
public async addRelay(inbox: string): Promise {
- const relay = await this.relaysRepository.insert({
+ const relay = await this.relaysRepository.insertOne({
id: this.idService.gen(),
inbox,
status: 'requesting',
- }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
+ });
const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 439bc08845..7f939b99c7 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise {
- const game = await this.reversiGamesRepository.insert({
+ const game = await this.reversiGamesRepository.insertOne({
id: this.idService.gen(),
user1Id: parentId,
user2Id: childId,
@@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
noIrregularRules: options.noIrregularRules,
- }).then(x => this.reversiGamesRepository.findOneOrFail({
- where: { id: x.identifiers[0].id },
- relations: ['user1', 'user2'],
- }));
+ }, { relations: ['user1', 'user2'] });
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game);
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 70c537f9ab..e2ebecb99f 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -410,14 +410,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
- public async getModeratorIds(includeAdmins = true): Promise {
+ public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
- const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
- const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
- roleId: In(moderatorRoles.map(r => r.id)),
- }) : [];
+ const moderatorRoles = includeAdmins
+ ? roles.filter(r => r.isModerator || r.isAdministrator)
+ : roles.filter(r => r.isModerator);
+
// TODO: isRootなアカウントも含める
- return assigns.map(a => a.userId);
+ const assigns = moderatorRoles.length > 0
+ ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
+ : [];
+
+ const now = Date.now();
+ const result = [
+ // Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+ ...new Set(
+ assigns
+ .filter(it =>
+ (excludeExpire)
+ ? (it.expiresAt == null || it.expiresAt.getTime() > now)
+ : true,
+ )
+ .map(a => a.userId),
+ ),
+ ];
+
+ return result.sort((x, y) => x.localeCompare(y));
}
@bindThis
@@ -471,12 +489,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
}
- const created = await this.roleAssignmentsRepository.insert({
+ const created = await this.roleAssignmentsRepository.insertOne({
id: this.idService.gen(now),
expiresAt: expiresAt,
roleId: roleId,
userId: userId,
- }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.rolesRepository.update(roleId, {
lastUsedAt: new Date(),
@@ -558,7 +576,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async create(values: Partial, moderator?: MiUser): Promise {
const date = new Date();
- const created = await this.rolesRepository.insert({
+ const created = await this.rolesRepository.insertOne({
id: this.idService.gen(date.getTime()),
updatedAt: date,
lastUsedAt: date,
@@ -576,7 +594,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
- }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
+ });
this.globalEventService.publishInternalEvent('roleCreated', created);
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
new file mode 100644
index 0000000000..bc6851f788
--- /dev/null
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -0,0 +1,233 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { IdService } from '@/core/IdService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import Logger from '@/logger.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class SystemWebhookService implements OnApplicationShutdown {
+ private logger: Logger;
+ private activeSystemWebhooksFetched = false;
+ private activeSystemWebhooks: MiSystemWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ private idService: IdService,
+ private queueService: QueueService,
+ private moderationLogService: ModerationLogService,
+ private loggerService: LoggerService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ this.logger = this.loggerService.getLogger('webhook');
+ }
+
+ @bindThis
+ public async fetchActiveSystemWebhooks() {
+ if (!this.activeSystemWebhooksFetched) {
+ this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
+ isActive: true,
+ });
+ this.activeSystemWebhooksFetched = true;
+ }
+
+ return this.activeSystemWebhooks;
+ }
+
+ /**
+ * SystemWebhook の一覧を取得する.
+ */
+ @bindThis
+ public async fetchSystemWebhooks(params?: {
+ ids?: MiSystemWebhook['id'][];
+ isActive?: MiSystemWebhook['isActive'];
+ on?: MiSystemWebhook['on'];
+ }): Promise {
+ const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
+ if (params) {
+ if (params.ids && params.ids.length > 0) {
+ query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
+ }
+ if (params.isActive !== undefined) {
+ query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
+ }
+ if (params.on && params.on.length > 0) {
+ query.andWhere(':on <@ systemWebhook.on', { on: params.on });
+ }
+ }
+
+ return query.getMany();
+ }
+
+ /**
+ * SystemWebhook を作成する.
+ */
+ @bindThis
+ public async createSystemWebhook(
+ params: {
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const id = this.idService.gen();
+ await this.systemWebhooksRepository.insert({
+ ...params,
+ id,
+ });
+
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
+ this.moderationLogService
+ .log(updater, 'createSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook: webhook,
+ })
+ .then();
+
+ return webhook;
+ }
+
+ /**
+ * SystemWebhook を更新する.
+ */
+ @bindThis
+ public async updateSystemWebhook(
+ params: {
+ id: MiSystemWebhook['id'];
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
+ await this.systemWebhooksRepository.update(beforeEntity.id, {
+ updatedAt: new Date(),
+ isActive: params.isActive,
+ name: params.name,
+ on: params.on,
+ url: params.url,
+ secret: params.secret,
+ });
+
+ const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
+ this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
+ this.moderationLogService
+ .log(updater, 'updateSystemWebhook', {
+ systemWebhookId: beforeEntity.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * SystemWebhook を削除する.
+ */
+ @bindThis
+ public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ await this.systemWebhooksRepository.delete(id);
+
+ this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
+ this.moderationLogService
+ .log(updater, 'deleteSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook,
+ })
+ .then();
+ }
+
+ /**
+ * SystemWebhook をWebhook配送キューに追加する
+ * @see QueueService.systemWebhookDeliver
+ */
+ @bindThis
+ public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+ const webhookEntity = typeof webhook === 'string'
+ ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
+ : webhook;
+ if (!webhookEntity || !webhookEntity.isActive) {
+ this.logger.info(`Webhook is not active or not found : ${webhook}`);
+ return;
+ }
+
+ if (!webhookEntity.on.includes(type)) {
+ this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
+ return;
+ }
+
+ return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'systemWebhookCreated': {
+ if (body.isActive) {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ break;
+ }
+ case 'systemWebhookUpdated': {
+ if (body.isActive) {
+ const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
+ } else {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ } else {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'systemWebhookDeleted': {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 96f389b54c..2f1310b8ef 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index deeecdeb1f..267a6a3f1b 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
@@ -331,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'follow', {
+ this.queueService.userWebhookDeliver(webhook, 'follow', {
user: packed,
});
}
@@ -345,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'followed', {
+ this.queueService.userWebhookDeliver(webhook, 'followed', {
user: packed,
});
}
@@ -398,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
@@ -517,7 +517,7 @@ export class UserFollowingService implements OnModuleInit {
followerId: follower.id,
});
- const followRequest = await this.followRequestsRepository.insert({
+ const followRequest = await this.followRequestsRepository.insertOne({
id: this.idService.gen(),
followerId: follower.id,
followeeId: followee.id,
@@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
followeeHost: followee.host,
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
- }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
+ });
// Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) {
@@ -740,7 +740,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packedFollowee,
});
}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
new file mode 100644
index 0000000000..e96bfeea95
--- /dev/null
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { WebhooksRepository } from '@/models/_.js';
+import type { MiWebhook } from '@/models/Webhook.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class UserWebhookService implements OnApplicationShutdown {
+ private activeWebhooksFetched = false;
+ private activeWebhooks: MiWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.webhooksRepository)
+ private webhooksRepository: WebhooksRepository,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ public async getActiveWebhooks() {
+ if (!this.activeWebhooksFetched) {
+ this.activeWebhooks = await this.webhooksRepository.findBy({
+ active: true,
+ });
+ this.activeWebhooksFetched = true;
+ }
+
+ return this.activeWebhooks;
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'webhookCreated': {
+ if (body.active) {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ break;
+ }
+ case 'webhookUpdated': {
+ if (body.active) {
+ const i = this.activeWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ };
+ } else {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ } else {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'webhookDeleted': {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
deleted file mode 100644
index 6be34977b0..0000000000
--- a/packages/backend/src/core/WebhookService.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import type { GlobalEvents } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
-
-@Injectable()
-export class WebhookService implements OnApplicationShutdown {
- private webhooksFetched = false;
- private webhooks: MiWebhook[] = [];
-
- constructor(
- @Inject(DI.redisForSub)
- private redisForSub: Redis.Redis,
-
- @Inject(DI.webhooksRepository)
- private webhooksRepository: WebhooksRepository,
- ) {
- //this.onMessage = this.onMessage.bind(this);
- this.redisForSub.on('message', this.onMessage);
- }
-
- @bindThis
- public async getActiveWebhooks() {
- if (!this.webhooksFetched) {
- this.webhooks = await this.webhooksRepository.findBy({
- active: true,
- });
- this.webhooksFetched = true;
- }
-
- return this.webhooks;
- }
-
- @bindThis
- private async onMessage(_: string, data: string): Promise {
- const obj = JSON.parse(data);
-
- if (obj.channel === 'internal') {
- const { type, body } = obj.message as GlobalEvents['internal']['payload'];
- switch (type) {
- case 'webhookCreated':
- if (body.active) {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- break;
- case 'webhookUpdated':
- if (body.active) {
- const i = this.webhooks.findIndex(a => a.id === body.id);
- if (i > -1) {
- this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- };
- } else {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- } else {
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- }
- break;
- case 'webhookDeleted':
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- break;
- default:
- break;
- }
- }
- }
-
- @bindThis
- public dispose(): void {
- this.redisForSub.off('message', this.onMessage);
- }
-
- @bindThis
- public onApplicationShutdown(signal?: string | undefined): void {
- this.dispose();
- }
-}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 1621c41bcc..de3178b482 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -28,6 +28,8 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserR
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -36,9 +38,8 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js';
-import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
+import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
@Injectable()
export class ApInboxService {
@@ -57,9 +58,6 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- @Inject(DI.abuseUserReportsRepository)
- private abuseUserReportsRepository: AbuseUserReportsRepository,
-
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -68,6 +66,7 @@ export class ApInboxService {
private utilityService: UtilityService,
private idService: IdService,
private metaService: MetaService,
+ private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
private reactionService: ReactionService,
@@ -90,13 +89,15 @@ export class ApInboxService {
}
@bindThis
- public async performActivity(actor: MiRemoteUser, activity: IObject): Promise {
+ public async performActivity(actor: MiRemoteUser, activity: IObject): Promise {
+ let result = undefined as string | void;
if (isCollectionOrOrderedCollection(activity)) {
+ const results = [] as [string, string | void][];
const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item);
try {
- await this.performOneActivity(actor, act);
+ results.push([getApId(item), await this.performOneActivity(actor, act)]);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
@@ -105,8 +106,13 @@ export class ApInboxService {
}
}
}
+
+ const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
+ if (hasReason) {
+ result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
+ }
} else {
- await this.performOneActivity(actor, activity);
+ result = await this.performOneActivity(actor, activity);
}
// ついでにリモートユーザーの情報が古かったら更新しておく
@@ -117,42 +123,43 @@ export class ApInboxService {
});
}
}
+ return result;
}
@bindThis
- public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise {
+ public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise {
if (actor.isSuspended) return;
if (isCreate(activity)) {
- await this.create(actor, activity);
+ return await this.create(actor, activity);
} else if (isDelete(activity)) {
- await this.delete(actor, activity);
+ return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
- await this.update(actor, activity);
+ return await this.update(actor, activity);
} else if (isFollow(activity)) {
- await this.follow(actor, activity);
+ return await this.follow(actor, activity);
} else if (isAccept(activity)) {
- await this.accept(actor, activity);
+ return await this.accept(actor, activity);
} else if (isReject(activity)) {
- await this.reject(actor, activity);
+ return await this.reject(actor, activity);
} else if (isAdd(activity)) {
- await this.add(actor, activity).catch(err => this.logger.error(err));
+ return await this.add(actor, activity);
} else if (isRemove(activity)) {
- await this.remove(actor, activity).catch(err => this.logger.error(err));
+ return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
- await this.announce(actor, activity);
+ return await this.announce(actor, activity);
} else if (isLike(activity)) {
- await this.like(actor, activity);
+ return await this.like(actor, activity);
} else if (isUndo(activity)) {
- await this.undo(actor, activity);
+ return await this.undo(actor, activity);
} else if (isBlock(activity)) {
- await this.block(actor, activity);
+ return await this.block(actor, activity);
} else if (isFlag(activity)) {
- await this.flag(actor, activity);
+ return await this.flag(actor, activity);
} else if (isMove(activity)) {
- await this.move(actor, activity);
+ return await this.move(actor, activity);
} else {
- this.logger.warn(`unrecognized activity type: ${activity.type}`);
+ return `unrecognized activity type: ${activity.type}`;
}
}
@@ -234,38 +241,49 @@ export class ApInboxService {
}
@bindThis
- private async add(actor: MiRemoteUser, activity: IAdd): Promise {
+ private async add(actor: MiRemoteUser, activity: IAdd): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return 'target is null';
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
- if (note == null) throw new Error('note not found');
+ if (note == null) return 'note not found';
await this.notePiningService.addPinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
- private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise {
+ private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise {
const uri = getApId(activity);
this.logger.info(`Announce: ${uri}`);
- const targetUri = getApId(activity.object);
+ const resolver = this.apResolverService.createResolver();
- await this.announceNote(actor, activity, targetUri);
+ if (!activity.object) return 'skip: activity has no object property';
+ const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
+ const target = await resolver.resolve(activity.object).catch(e => {
+ this.logger.error(`Resolution failed: ${e}`);
+ return e;
+ });
+
+ if (isPost(target)) return await this.announceNote(actor, activity, target);
+
+ return `skip: unknown object type ${getApType(target)}`;
}
@bindThis
- private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise {
+ private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise {
const uri = getApId(activity);
if (actor.isSuspended) {
@@ -288,24 +306,21 @@ export class ApInboxService {
// Announce対象をresolve
let renote;
try {
- renote = await this.apNoteService.resolveNote(targetUri);
- if (renote == null) throw new Error('announce target is null');
+ renote = await this.apNoteService.resolveNote(target);
+ if (renote == null) return 'announce target is null';
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (!err.isRetryable) {
- this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
- return;
+ return `Ignored announce target ${target.id} - ${err.statusCode}`;
}
-
- this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
+ return `Error in announce target ${target.id} - ${err.statusCode}`;
}
throw err;
}
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
- this.logger.warn('skip: invalid actor for this activity');
- return;
+ return 'skip: invalid actor for this activity';
}
this.logger.info(`Creating the (Re)Note: ${uri}`);
@@ -314,8 +329,7 @@ export class ApInboxService {
const createdAt = activity.published ? new Date(activity.published) : null;
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
- this.logger.warn('skip: malformed createdAt');
- return;
+ return 'skip: malformed createdAt';
}
await this.noteCreateService.create(actor, {
@@ -349,11 +363,15 @@ export class ApInboxService {
}
@bindThis
- private async create(actor: MiRemoteUser, activity: ICreate): Promise {
+ private async create(actor: MiRemoteUser, activity: ICreate): Promise {
const uri = getApId(activity);
this.logger.info(`Create: ${uri}`);
+ if (!activity.object) return 'skip: activity has no object property';
+ const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
// copy audiences between activity <=> object.
if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
@@ -380,7 +398,7 @@ export class ApInboxService {
if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity);
} else {
- this.logger.warn(`Unknown type: ${getApType(object)}`);
+ return `Unknown type: ${getApType(object)}`;
}
}
@@ -422,7 +440,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
// 削除対象objectのtype
@@ -526,14 +544,13 @@ export class ApInboxService {
});
if (users.length < 1) return 'skip';
- await this.abuseUserReportsRepository.insert({
- id: this.idService.gen(),
+ await this.abuseReportService.report([{
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
- });
+ }]);
return 'ok';
}
@@ -581,29 +598,29 @@ export class ApInboxService {
}
@bindThis
- private async remove(actor: MiRemoteUser, activity: IRemove): Promise {
+ private async remove(actor: MiRemoteUser, activity: IRemove): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return 'target is null';
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
- if (note == null) throw new Error('note not found');
+ if (note == null) return 'note not found';
await this.notePiningService.removePinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
private async undo(actor: MiRemoteUser, activity: IUndo): Promise {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
const uri = activity.id ?? activity;
@@ -614,7 +631,7 @@ export class ApInboxService {
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
- throw e;
+ return e;
});
// don't queue because the sender may attempt again when timeout
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 4e361b57bc..c6e6b3a1e8 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -81,20 +81,20 @@ export class ApNoteService {
const expectHost = this.utilityService.extractDbHost(uri);
if (!validPost.includes(getApType(object))) {
- return new Error(`invalid Note: invalid object type ${getApType(object)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
}
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
- return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
- return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
- return new Error('invalid Note: published timestamp is malformed');
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
}
return null;
@@ -407,7 +407,7 @@ export class ApNoteService {
this.logger.info(`register emoji host=${host}, name=${name}`);
- return await this.emojisRepository.insert({
+ return await this.emojisRepository.insertOne({
id: this.idService.gen(),
host,
name,
@@ -416,7 +416,7 @@ export class ApNoteService {
publicUrl: tag.icon.url,
updatedAt: new Date(),
aliases: [],
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
+ });
}));
}
}
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 09322888d5..5b6c6c8ca6 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -328,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
+export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts
index afc728d564..20815ea968 100644
--- a/packages/backend/src/core/chart/ChartLoggerService.ts
+++ b/packages/backend/src/core/chart/ChartLoggerService.ts
@@ -14,6 +14,6 @@ export class ChartLoggerService {
constructor(
private loggerService: LoggerService,
) {
- this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
+ this.logger = this.loggerService.getLogger('chart', 'white');
}
}
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index 5e4555ee96..c2329a2f73 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -47,7 +47,7 @@ export default class FederationChart extends Chart { // eslint-di
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host')
- .where('instance.isSuspended = true');
+ .where('instance.suspensionState != \'none\'');
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
.select('f.followerHost')
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts
index f10e30ef10..af5485a46e 100644
--- a/packages/backend/src/core/chart/core.ts
+++ b/packages/backend/src/core/chart/core.ts
@@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm';
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import type { Repository, DataSource } from 'typeorm';
+import { MiRepository, miRepository } from '@/models/_.js';
+import type { DataSource, Repository } from 'typeorm';
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
@@ -145,10 +146,10 @@ export default abstract class Chart {
group: string | null;
}[] = [];
// ↓にしたいけどfindOneとかで型エラーになる
- //private repositoryForHour: Repository>;
- //private repositoryForDay: Repository>;
- private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
- private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
+ //private repositoryForHour: Repository> & MiRepository>;
+ //private repositoryForDay: Repository> & MiRepository>;
+ private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
+ private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
/**
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
@@ -211,6 +212,10 @@ export default abstract class Chart {
} {
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
name:
+ span === 'hour' ? `ChartX${name}` :
+ span === 'day' ? `ChartDayX${name}` :
+ new Error('not happen') as never,
+ tableName:
span === 'hour' ? `__chart__${camelToSnake(name)}` :
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
new Error('not happen') as never,
@@ -271,8 +276,8 @@ export default abstract class Chart {
this.logger = logger;
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
- this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
- this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
+ this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
+ this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
}
@bindThis
@@ -387,11 +392,11 @@ export default abstract class Chart {
}
// 新規ログ挿入
- log = await repository.insert({
+ log = await repository.insertOne({
date: date,
...(group ? { group: group } : {}),
...columns,
- }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord;
+ }) as RawRecord;
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
new file mode 100644
index 0000000000..6819afafd9
--- /dev/null
+++ b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+
+@Injectable()
+export class AbuseReportNotificationRecipientEntityService {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ private userEntityService: UserEntityService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
+ opts?: {
+ users: Map>,
+ webhooks: Map>,
+ },
+ ): Promise> {
+ const recipient = typeof src === 'object'
+ ? src
+ : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src });
+ const user = recipient.userId
+ ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId))
+ : undefined;
+ const webhook = recipient.systemWebhookId
+ ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId))
+ : undefined;
+
+ return {
+ id: recipient.id,
+ isActive: recipient.isActive,
+ updatedAt: recipient.updatedAt.toISOString(),
+ name: recipient.name,
+ method: recipient.method,
+ userId: recipient.userId ?? undefined,
+ user: user,
+ systemWebhookId: recipient.systemWebhookId ?? undefined,
+ systemWebhook: webhook,
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[],
+ ): Promise[]> {
+ const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object');
+ const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ objs.push(
+ ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ const userIds = objs.map(it => it.userId).filter(isNotNull);
+ const users: Map> = (userIds.length > 0)
+ ? await this.userEntityService.packMany(userIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(isNotNull);
+ const systemWebhooks: Map> = (systemWebhookIds.length > 0)
+ ? await this.systemWebhookEntityService.packMany(systemWebhookIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ return Promise
+ .all(
+ objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index 49f256d870..b0e1d1ab36 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -10,6 +10,8 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,6 +28,11 @@ export class AbuseUserReportEntityService {
@bindThis
public async pack(
src: MiAbuseUserReport['id'] | MiAbuseUserReport,
+ hint?: {
+ packedReporter?: Packed<'UserDetailedNotMe'>,
+ packedTargetUser?: Packed<'UserDetailedNotMe'>,
+ packedAssignee?: Packed<'UserDetailedNotMe'>,
+ },
) {
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
@@ -37,13 +44,13 @@ export class AbuseUserReportEntityService {
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
- reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
+ reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
schema: 'UserDetailedNotMe',
}),
- targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
+ targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
schema: 'UserDetailedNotMe',
}),
- assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
+ assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
@@ -51,9 +58,24 @@ export class AbuseUserReportEntityService {
}
@bindThis
- public packMany(
- reports: any[],
+ public async packMany(
+ reports: MiAbuseUserReport[],
) {
- return Promise.all(reports.map(x => this.pack(x)));
+ const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
+ const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
+ const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
+ const _userMap = await this.userEntityService.packMany(
+ [..._reporters, ..._targetUsers, ..._assignees],
+ null,
+ { schema: 'UserDetailedNotMe' },
+ ).then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ reports.map(report => {
+ const packedReporter = _userMap.get(report.reporterId);
+ const packedTargetUser = _userMap.get(report.targetUserId);
+ const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined;
+ return this.pack(report, { packedReporter, packedTargetUser, packedAssignee });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts
index 4a17a3d80f..e770028af3 100644
--- a/packages/backend/src/core/entities/AntennaEntityService.ts
+++ b/packages/backend/src/core/entities/AntennaEntityService.ts
@@ -43,6 +43,7 @@ export class AntennaEntityService {
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
+ notify: false, // 後方互換性のため
};
}
}
diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts
index c8c1520ceb..1e699032e2 100644
--- a/packages/backend/src/core/entities/BlockingEntityService.ts
+++ b/packages/backend/src/core/entities/BlockingEntityService.ts
@@ -29,6 +29,9 @@ export class BlockingEntityService {
public async pack(
src: MiBlocking['id'] | MiBlocking,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ blockee?: Packed<'UserDetailedNotMe'>,
+ },
): Promise> {
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
@@ -36,17 +39,20 @@ export class BlockingEntityService {
id: blocking.id,
createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId,
- blockee: this.userEntityService.pack(blocking.blockeeId, me, {
+ blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- blockings: any[],
+ public async packMany(
+ blockings: MiBlocking[],
me: { id: MiUser['id'] },
) {
- return Promise.all(blockings.map(x => this.pack(x, me)));
+ const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId);
+ const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) })));
}
}
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index ce49c3458c..3855a28436 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -35,6 +35,9 @@ export class ClipEntityService {
public async pack(
src: MiClip['id'] | MiClip,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
@@ -44,7 +47,7 @@ export class ClipEntityService {
createdAt: this.idService.parse(clip.id).date.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId,
- user: this.userEntityService.pack(clip.user ?? clip.userId),
+ user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic,
@@ -55,11 +58,14 @@ export class ClipEntityService {
}
@bindThis
- public packMany(
+ public async packMany(
clips: MiClip[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(clips.map(x => this.pack(x, me)));
+ const _users = clips.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index 26bf386cbc..02ff2e7754 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -222,6 +222,9 @@ export class DriveFileEntityService {
public async packNullable(
src: MiDriveFile['id'] | MiDriveFile,
options?: PackOptions,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise | null> {
const opts = Object.assign({
detail: false,
@@ -249,7 +252,7 @@ export class DriveFileEntityService {
detail: true,
}) : null,
userId: file.userId,
- user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
+ user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
}
@@ -258,7 +261,10 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise[]> {
- const items = await Promise.all(files.map(f => this.packNullable(f, options)));
+ const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
+ const _userMap = await this.userEntityService.packMany(_user)
+ .then(users => new Map(users.map(user => [user.id, user])));
+ const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
return items.filter(isNotNull);
}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index db4cf6d360..d110f7afc6 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -33,6 +33,9 @@ export class FlashEntityService {
public async pack(
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
@@ -42,7 +45,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title,
summary: flash.summary,
script: flash.script,
@@ -52,11 +55,14 @@ export class FlashEntityService {
}
@bindThis
- public packMany(
- flashs: MiFlash[],
+ public async packMany(
+ flashes: MiFlash[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(flashs.map(x => this.pack(x, me)));
+ const _users = flashes.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts
index 763b75101f..0101ec8aa7 100644
--- a/packages/backend/src/core/entities/FollowRequestEntityService.ts
+++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts
@@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFollowRequest } from '@/models/FollowRequest.js';
import { bindThis } from '@/decorators.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,14 +27,36 @@ export class FollowRequestEntityService {
public async pack(
src: MiFollowRequest['id'] | MiFollowRequest,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedFollower?: Packed<'UserLite'>,
+ packedFollowee?: Packed<'UserLite'>,
+ },
) {
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
return {
id: request.id,
- follower: await this.userEntityService.pack(request.followerId, me),
- followee: await this.userEntityService.pack(request.followeeId, me),
+ follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
+ followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me),
};
}
+
+ @bindThis
+ public async packMany(
+ requests: MiFollowRequest[],
+ me?: { id: MiUser['id'] } | null | undefined,
+ ) {
+ const _followers = requests.map(({ follower, followerId }) => follower ?? followerId);
+ const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId);
+ const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ requests.map(req => {
+ const packedFollower = _userMap.get(req.followerId);
+ const packedFollowee = _userMap.get(req.followeeId);
+ return this.pack(req, me, { packedFollower, packedFollowee });
+ }),
+ );
+ }
}
diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts
index 24cd33e3f7..d2dbaf2270 100644
--- a/packages/backend/src/core/entities/FollowingEntityService.ts
+++ b/packages/backend/src/core/entities/FollowingEntityService.ts
@@ -78,6 +78,10 @@ export class FollowingEntityService {
populateFollowee?: boolean;
populateFollower?: boolean;
},
+ hint?: {
+ packedFollowee?: Packed<'UserDetailedNotMe'>,
+ packedFollower?: Packed<'UserDetailedNotMe'>,
+ },
): Promise> {
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
@@ -88,25 +92,35 @@ export class FollowingEntityService {
createdAt: this.idService.parse(following.id).date.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
- followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
+ followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
- follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
+ follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
});
}
@bindThis
- public packMany(
- followings: any[],
+ public async packMany(
+ followings: MiFollowing[],
me?: { id: MiUser['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
},
) {
- return Promise.all(followings.map(x => this.pack(x, me, opts)));
+ const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : [];
+ const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : [];
+ const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ followings.map(following => {
+ const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined;
+ const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined;
+ return this.pack(following, me, opts, { packedFollowee, packedFollower });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts
index 101182a9e5..9746a4c1af 100644
--- a/packages/backend/src/core/entities/GalleryPostEntityService.ts
+++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts
@@ -35,6 +35,9 @@ export class GalleryPostEntityService {
public async pack(
src: MiGalleryPost['id'] | MiGalleryPost,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
@@ -44,7 +47,7 @@ export class GalleryPostEntityService {
createdAt: this.idService.parse(post.id).date.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
- user: this.userEntityService.pack(post.user ?? post.userId, me),
+ user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
@@ -58,11 +61,14 @@ export class GalleryPostEntityService {
}
@bindThis
- public packMany(
+ public async packMany(
posts: MiGalleryPost[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(posts.map(x => this.pack(x, me)));
+ const _users = posts.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts
index 891543bc0f..26f57e1299 100644
--- a/packages/backend/src/core/entities/InviteCodeEntityService.ts
+++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts
@@ -12,6 +12,7 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -29,6 +30,10 @@ export class InviteCodeEntityService {
public async pack(
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedCreatedBy?: Packed<'UserLite'>,
+ packedUsedBy?: Packed<'UserLite'>,
+ },
): Promise> {
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
where: {
@@ -42,18 +47,28 @@ export class InviteCodeEntityService {
code: target.code,
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
createdAt: this.idService.parse(target.id).date.toISOString(),
- createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
- usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
+ createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
+ usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
used: !!target.usedAt,
});
}
@bindThis
- public packMany(
- targets: any[],
+ public async packMany(
+ tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] },
) {
- return Promise.all(targets.map(x => this.pack(x, me)));
+ const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
+ const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
+ const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ tickets.map(ticket => {
+ const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined;
+ const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined;
+ return this.pack(ticket, me, { packedCreatedBy, packedUsedBy });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 9d054ab6a1..5dfec589e1 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -67,6 +67,7 @@ export class MetaEntityService {
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
+ inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts
index 205e147bd1..bf1b2a002c 100644
--- a/packages/backend/src/core/entities/ModerationLogEntityService.ts
+++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts
@@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
import type { ModerationLogsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { } from '@/models/Blocking.js';
-import type { MiModerationLog } from '@/models/ModerationLog.js';
+import { MiModerationLog } from '@/models/ModerationLog.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -27,6 +28,9 @@ export class ModerationLogEntityService {
@bindThis
public async pack(
src: MiModerationLog['id'] | MiModerationLog,
+ hint?: {
+ packedUser?: Packed<'UserDetailedNotMe'>,
+ },
) {
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
@@ -36,17 +40,20 @@ export class ModerationLogEntityService {
type: log.type,
info: log.info,
userId: log.userId,
- user: this.userEntityService.pack(log.user ?? log.userId, null, {
+ user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- reports: any[],
+ public async packMany(
+ reports: MiModerationLog[],
) {
- return Promise.all(reports.map(x => this.pack(x)));
+ const _users = reports.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts
index 0a52f429a2..d361a20271 100644
--- a/packages/backend/src/core/entities/MutingEntityService.ts
+++ b/packages/backend/src/core/entities/MutingEntityService.ts
@@ -30,6 +30,9 @@ export class MutingEntityService {
public async pack(
src: MiMuting['id'] | MiMuting,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedMutee?: Packed<'UserDetailedNotMe'>,
+ },
): Promise> {
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
@@ -38,18 +41,21 @@ export class MutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
- mutee: this.userEntityService.pack(muting.muteeId, me, {
+ mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- mutings: any[],
+ public async packMany(
+ mutings: MiMuting[],
me: { id: MiUser['id'] },
) {
- return Promise.all(mutings.map(x => this.pack(x, me)));
+ const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
+ const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 22d01462e7..2ce72c50b8 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -290,6 +290,7 @@ export class NoteEntityService implements OnModuleInit {
_hint_?: {
myReactions: Map;
packedFiles: Map | null>;
+ packedUsers: Map>
};
},
): Promise> {
@@ -319,12 +320,13 @@ export class NoteEntityService implements OnModuleInit {
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles;
+ const packedUsers = options?._hint_?.packedUsers;
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
userId: note.userId,
- user: this.userEntityService.pack(note.user ?? note.userId, me),
+ user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
text: text,
cw: note.cw,
visibility: note.visibility,
@@ -449,12 +451,20 @@ export class NoteEntityService implements OnModuleInit {
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
+ const users = [
+ ...notes.map(({ user, userId }) => user ?? userId),
+ ...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
+ ...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
+ ];
+ const packedUsers = await this.userEntityService.packMany(users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap,
packedFiles,
+ packedUsers,
},
})));
}
diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts
index 3f4fa3cf96..46ec13704c 100644
--- a/packages/backend/src/core/entities/NoteReactionEntityService.ts
+++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts
@@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
options?: {
withNote: boolean;
},
+ hints?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const opts = Object.assign({
withNote: false,
@@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
return {
id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(),
- user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
+ user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
@@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
const opts = Object.assign({
withNote: false,
}, options);
-
- return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
+ const _users = reactions.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index 65c69a49a7..142d9e81db 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -40,6 +40,9 @@ export class PageEntityService {
public async pack(
src: MiPage['id'] | MiPage,
me?: { id: MiUser['id'] } | null | undefined,
+ hint?: {
+ packedUser?: Packed<'UserLite'>
+ },
): Promise> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
@@ -91,7 +94,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
- user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,
@@ -110,11 +113,14 @@ export class PageEntityService {
}
@bindThis
- public packMany(
+ public async packMany(
pages: MiPage[],
me?: { id: MiUser['id'] } | null | undefined,
) {
- return Promise.all(pages.map(x => this.pack(x, me)));
+ const _users = pages.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me)
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) })));
}
}
diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
index 0b05a5db80..e4e154109a 100644
--- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts
+++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
@@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
public async pack(
src: MiRenoteMuting['id'] | MiRenoteMuting,
me?: { id: MiUser['id'] } | null | undefined,
+ hints?: {
+ packedMutee?: Packed<'UserDetailedNotMe'>
+ },
): Promise> {
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
@@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
id: muting.id,
createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId,
- mutee: this.userEntityService.pack(muting.muteeId, me, {
+ mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
- public packMany(
- mutings: any[],
+ public async packMany(
+ mutings: MiRenoteMuting[],
me: { id: MiUser['id'] },
) {
- return Promise.all(mutings.map(x => this.pack(x, me)));
+ const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index 32cbe631e4..df042e75c1 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -28,13 +28,15 @@ export class ReversiGameEntityService {
@bindThis
public async packDetail(
src: MiReversiGame['id'] | MiReversiGame,
+ hint?: {
+ packedUser1?: Packed<'UserLite'>,
+ packedUser2?: Packed<'UserLite'>,
+ },
): Promise> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
- const users = await Promise.all([
- this.userEntityService.pack(game.user1 ?? game.user1Id),
- this.userEntityService.pack(game.user2 ?? game.user2Id),
- ]);
+ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
+ const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -49,10 +51,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
- user1: users[0],
- user2: users[1],
+ user1,
+ user2,
winnerId: game.winnerId,
- winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
+ winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -68,22 +70,35 @@ export class ReversiGameEntityService {
}
@bindThis
- public packDetailMany(
- xs: MiReversiGame[],
+ public async packDetailMany(
+ games: MiReversiGame[],
) {
- return Promise.all(xs.map(x => this.packDetail(x)));
+ const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
+ const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
+ const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ games.map(game => {
+ return this.packDetail(game, {
+ packedUser1: _userMap.get(game.user1Id),
+ packedUser2: _userMap.get(game.user2Id),
+ });
+ }),
+ );
}
@bindThis
public async packLite(
src: MiReversiGame['id'] | MiReversiGame,
+ hint?: {
+ packedUser1?: Packed<'UserLite'>,
+ packedUser2?: Packed<'UserLite'>,
+ },
): Promise> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
- const users = await Promise.all([
- this.userEntityService.pack(game.user1 ?? game.user1Id),
- this.userEntityService.pack(game.user2 ?? game.user2Id),
- ]);
+ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
+ const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -94,10 +109,10 @@ export class ReversiGameEntityService {
isEnded: game.isEnded,
user1Id: game.user1Id,
user2Id: game.user2Id,
- user1: users[0],
- user2: users[1],
+ user1,
+ user2,
winnerId: game.winnerId,
- winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
+ winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -111,10 +126,21 @@ export class ReversiGameEntityService {
}
@bindThis
- public packLiteMany(
- xs: MiReversiGame[],
+ public async packLiteMany(
+ games: MiReversiGame[],
) {
- return Promise.all(xs.map(x => this.packLite(x)));
+ const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
+ const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
+ const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return Promise.all(
+ games.map(game => {
+ return this.packLite(game, {
+ packedUser1: _userMap.get(game.user1Id),
+ packedUser2: _userMap.get(game.user2Id),
+ });
+ }),
+ );
}
}
diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
new file mode 100644
index 0000000000..e18734091c
--- /dev/null
+++ b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+
+@Injectable()
+export class SystemWebhookEntityService {
+ constructor(
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiSystemWebhook['id'] | MiSystemWebhook,
+ opts?: {
+ webhooks: Map
+ },
+ ): Promise> {
+ const webhook = typeof src === 'object'
+ ? src
+ : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src });
+
+ return {
+ id: webhook.id,
+ isActive: webhook.isActive,
+ updatedAt: webhook.updatedAt.toISOString(),
+ latestSentAt: webhook.latestSentAt?.toISOString() ?? null,
+ latestStatus: webhook.latestStatus,
+ name: webhook.name,
+ on: webhook.on,
+ url: webhook.url,
+ secret: webhook.secret,
+ };
+ }
+
+ @bindThis
+ public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise[]> {
+ if (src.length === 0) {
+ return [];
+ }
+
+ const webhooks = Array.of();
+ webhooks.push(
+ ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'),
+ );
+
+ const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ webhooks.push(
+ ...await this.systemWebhooksRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ return Promise
+ .all(
+ webhooks.map(x =>
+ this.pack(x, {
+ webhooks: new Map(webhooks.map(x => [x.id, x])),
+ }),
+ ),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts
index 09cab24521..b77249c5cb 100644
--- a/packages/backend/src/core/entities/UserListEntityService.ts
+++ b/packages/backend/src/core/entities/UserListEntityService.ts
@@ -50,11 +50,14 @@ export class UserListEntityService {
public async packMembershipsMany(
memberships: MiUserListMembership[],
) {
+ const _users = memberships.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users)
+ .then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(memberships.map(async x => ({
id: x.id,
createdAt: this.idService.parse(x.id).date.toISOString(),
userId: x.userId,
- user: await this.userEntityService.pack(x.userId),
+ user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
withReplies: x.withReplies,
})));
}
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 919f4794a3..271082b4ff 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -49,6 +49,7 @@ export const DI = {
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'),
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
+ abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
authSessionsRepository: Symbol('authSessionsRepository'),
accessTokensRepository: Symbol('accessTokensRepository'),
@@ -70,6 +71,7 @@ export const DI = {
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
+ systemWebhooksRepository: Symbol('systemWebhooksRepository'),
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index d4705af601..ff5363a425 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
- private store: boolean;
- constructor(context: string, color?: KEYWORD, store = true) {
+ constructor(context: string, color?: KEYWORD) {
this.context = {
name: context,
color: color,
};
- this.store = store;
}
@bindThis
- public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
- const logger = new Logger(context, color, store);
+ public createSubLogger(context: string, color?: KEYWORD): Logger {
+ const logger = new Logger(context, color);
logger.parentLogger = this;
return logger;
}
@bindThis
- private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = [], store = true): void {
+ private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = []): void {
if (envOption.quiet) return;
- if (!this.store) store = false;
- if (level === 'debug') store = false;
if (this.parentLogger) {
- this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store);
+ this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts));
return;
}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 41e5bfe9e4..a721b8663c 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -4,12 +4,12 @@
*/
import {
- packedUserLiteSchema,
- packedUserDetailedNotMeOnlySchema,
packedMeDetailedOnlySchema,
- packedUserDetailedNotMeSchema,
packedMeDetailedSchema,
+ packedUserDetailedNotMeOnlySchema,
+ packedUserDetailedNotMeSchema,
packedUserDetailedSchema,
+ packedUserLiteSchema,
packedUserSchema,
} from '@/models/json-schema/user.js';
import { packedNoteSchema } from '@/models/json-schema/note.js';
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
-import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
+import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@@ -38,25 +38,27 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
import {
- packedRoleLiteSchema,
- packedRoleSchema,
- packedRolePoliciesSchema,
+ packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaLogicsSchema,
- packedRoleCondFormulaValueNot,
- packedRoleCondFormulaValueIsLocalOrRemoteSchema,
packedRoleCondFormulaValueAssignedRoleSchema,
packedRoleCondFormulaValueCreatedSchema,
- packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
+ packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+ packedRoleCondFormulaValueNot,
packedRoleCondFormulaValueSchema,
packedRoleCondFormulaValueUserSettingBooleanSchema,
+ packedRoleLiteSchema,
+ packedRolePoliciesSchema,
+ packedRoleSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
-import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
+import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js';
import {
- packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
+ packedMetaLiteSchema,
} from '@/models/json-schema/meta.js';
+import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
+import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -111,6 +113,8 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
+ SystemWebhook: packedSystemWebhookSchema,
+ AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
};
export type Packed = SchemaType;
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
new file mode 100644
index 0000000000..fbff880afc
--- /dev/null
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
+import { MiUserProfile } from '@/models/UserProfile.js';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+/**
+ * 通報受信時に通知を送信する方法.
+ */
+export type RecipientMethod = 'email' | 'webhook';
+
+@Entity('abuse_report_notification_recipient')
+export class MiAbuseReportNotificationRecipient {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index()
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * 通知方法.
+ */
+ @Index()
+ @Column('varchar', {
+ length: 64,
+ })
+ public method: RecipientMethod;
+
+ /**
+ * 通知先のユーザID.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public userId: MiUser['id'] | null;
+
+ /**
+ * 通知先のユーザ.
+ */
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
+ public user: MiUser | null;
+
+ /**
+ * 通知先のユーザプロフィール.
+ */
+ @ManyToOne(type => MiUserProfile, {})
+ @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
+ public userProfile: MiUserProfile | null;
+
+ /**
+ * 通知先のシステムWebhookId.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public systemWebhookId: string | null;
+
+ /**
+ * 通知先のシステムWebhook.
+ */
+ @ManyToOne(type => MiSystemWebhook, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public systemWebhook: MiSystemWebhook | null;
+}
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 04a34bbbb4..ad306fcad6 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -376,6 +376,12 @@ export class MiMeta {
})
public privacyPolicyUrl: string | null;
+ @Column('varchar', {
+ length: 1024,
+ nullable: true,
+ })
+ public inquiryUrl: string | null;
+
@Column('varchar', {
length: 8192,
nullable: true,
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index bd447570dd..ea0f88baba 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -3,417 +3,500 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { Provider } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js';
+import {
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiAccessToken,
+ MiAd,
+ MiAnnouncement,
+ MiAnnouncementRead,
+ MiAntenna,
+ MiApp,
+ MiAuthSession,
+ MiAvatarDecoration,
+ MiBlocking,
+ MiBubbleGameRecord,
+ MiChannel,
+ MiChannelFavorite,
+ MiChannelFollowing,
+ MiClip,
+ MiClipFavorite,
+ MiClipNote,
+ MiDriveFile,
+ MiDriveFolder,
+ MiEmoji,
+ MiFlash,
+ MiFlashLike,
+ MiFollowing,
+ MiFollowRequest,
+ MiGalleryLike,
+ MiGalleryPost,
+ MiHashtag,
+ MiInstance,
+ MiMeta,
+ MiModerationLog,
+ MiMuting,
+ MiNote,
+ MiNoteFavorite,
+ MiNoteReaction,
+ MiNoteThreadMuting,
+ MiNoteUnread,
+ MiPage,
+ MiPageLike,
+ MiPasswordResetRequest,
+ MiPoll,
+ MiPollVote,
+ MiPromoNote,
+ MiPromoRead,
+ MiRegistrationTicket,
+ MiRegistryItem,
+ MiRelay,
+ MiRenoteMuting,
+ MiRepository,
+ miRepository,
+ MiRetentionAggregation,
+ MiReversiGame,
+ MiRole,
+ MiRoleAssignment,
+ MiSignin,
+ MiSwSubscription,
+ MiSystemWebhook,
+ MiUsedUsername,
+ MiUser,
+ MiUserIp,
+ MiUserKeypair,
+ MiUserList,
+ MiUserListFavorite,
+ MiUserListMembership,
+ MiUserMemo,
+ MiUserNotePining,
+ MiUserPending,
+ MiUserProfile,
+ MiUserPublickey,
+ MiUserSecurityKey,
+ MiWebhook
+} from './_.js';
import type { DataSource } from 'typeorm';
-import type { Provider } from '@nestjs/common';
const $usersRepository: Provider = {
provide: DI.usersRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUser),
+ useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $notesRepository: Provider = {
provide: DI.notesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNote),
+ useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $announcementsRepository: Provider = {
provide: DI.announcementsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAnnouncement),
+ useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $announcementReadsRepository: Provider = {
provide: DI.announcementReadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead),
+ useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $appsRepository: Provider = {
provide: DI.appsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiApp),
+ useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $avatarDecorationsRepository: Provider = {
provide: DI.avatarDecorationsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
+ useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteThreadMutingsRepository: Provider = {
provide: DI.noteThreadMutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteReactionsRepository: Provider = {
provide: DI.noteReactionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteReaction),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $noteUnreadsRepository: Provider = {
provide: DI.noteUnreadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiNoteUnread),
+ useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pollsRepository: Provider = {
provide: DI.pollsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPoll),
+ useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pollVotesRepository: Provider = {
provide: DI.pollVotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPollVote),
+ useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userProfilesRepository: Provider = {
provide: DI.userProfilesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserProfile),
+ useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userKeypairsRepository: Provider = {
provide: DI.userKeypairsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserKeypair),
+ useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userPendingsRepository: Provider = {
provide: DI.userPendingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserPending),
+ useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userSecurityKeysRepository: Provider = {
provide: DI.userSecurityKeysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey),
+ useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userPublickeysRepository: Provider = {
provide: DI.userPublickeysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserPublickey),
+ useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userListsRepository: Provider = {
provide: DI.userListsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserList),
+ useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userListFavoritesRepository: Provider = {
provide: DI.userListFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userListMembershipsRepository: Provider = {
provide: DI.userListMembershipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
+ useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userNotePiningsRepository: Provider = {
provide: DI.userNotePiningsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
+ useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userIpsRepository: Provider = {
provide: DI.userIpsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserIp),
+ useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $usedUsernamesRepository: Provider = {
provide: DI.usedUsernamesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUsedUsername),
+ useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $followingsRepository: Provider = {
provide: DI.followingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFollowing),
+ useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $followRequestsRepository: Provider = {
provide: DI.followRequestsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFollowRequest),
+ useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $instancesRepository: Provider = {
provide: DI.instancesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiInstance),
+ useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $emojisRepository: Provider = {
provide: DI.emojisRepository,
- useFactory: (db: DataSource) => db.getRepository(MiEmoji),
+ useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $driveFilesRepository: Provider = {
provide: DI.driveFilesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
+ useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $driveFoldersRepository: Provider = {
provide: DI.driveFoldersRepository,
- useFactory: (db: DataSource) => db.getRepository(MiDriveFolder),
+ useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $metasRepository: Provider = {
provide: DI.metasRepository,
- useFactory: (db: DataSource) => db.getRepository(MiMeta),
+ useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $mutingsRepository: Provider = {
provide: DI.mutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $renoteMutingsRepository: Provider = {
provide: DI.renoteMutingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting),
+ useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $blockingsRepository: Provider = {
provide: DI.blockingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiBlocking),
+ useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $swSubscriptionsRepository: Provider = {
provide: DI.swSubscriptionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSwSubscription),
+ useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $hashtagsRepository: Provider = {
provide: DI.hashtagsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiHashtag),
+ useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $abuseUserReportsRepository: Provider = {
provide: DI.abuseUserReportsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport),
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $abuseReportNotificationRecipientRepository: Provider = {
+ provide: DI.abuseReportNotificationRecipientRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
inject: [DI.db],
};
const $registrationTicketsRepository: Provider = {
provide: DI.registrationTicketsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket),
+ useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $authSessionsRepository: Provider = {
provide: DI.authSessionsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAuthSession),
+ useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $accessTokensRepository: Provider = {
provide: DI.accessTokensRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAccessToken),
+ useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $signinsRepository: Provider = {
provide: DI.signinsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiSignin),
+ useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pagesRepository: Provider = {
provide: DI.pagesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPage),
+ useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $pageLikesRepository: Provider = {
provide: DI.pageLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPageLike),
+ useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $galleryPostsRepository: Provider = {
provide: DI.galleryPostsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiGalleryPost),
+ useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $galleryLikesRepository: Provider = {
provide: DI.galleryLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiGalleryLike),
+ useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $moderationLogsRepository: Provider = {
provide: DI.moderationLogsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiModerationLog),
+ useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $clipsRepository: Provider = {
provide: DI.clipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClip),
+ useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $clipNotesRepository: Provider = {
provide: DI.clipNotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClipNote),
+ useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $clipFavoritesRepository: Provider = {
provide: DI.clipFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiClipFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $antennasRepository: Provider = {
provide: DI.antennasRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAntenna),
+ useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPromoNote),
+ useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $promoReadsRepository: Provider = {
provide: DI.promoReadsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPromoRead),
+ useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $relaysRepository: Provider = {
provide: DI.relaysRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRelay),
+ useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $channelsRepository: Provider = {
provide: DI.channelsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannel),
+ useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $channelFollowingsRepository: Provider = {
provide: DI.channelFollowingsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing),
+ useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $channelFavoritesRepository: Provider = {
provide: DI.channelFavoritesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite),
+ useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $registryItemsRepository: Provider = {
provide: DI.registryItemsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRegistryItem),
+ useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $webhooksRepository: Provider = {
provide: DI.webhooksRepository,
- useFactory: (db: DataSource) => db.getRepository(MiWebhook),
+ useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository),
+ inject: [DI.db],
+};
+
+const $systemWebhooksRepository: Provider = {
+ provide: DI.systemWebhooksRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
inject: [DI.db],
};
const $adsRepository: Provider = {
provide: DI.adsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiAd),
+ useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $passwordResetRequestsRepository: Provider = {
provide: DI.passwordResetRequestsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest),
+ useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $retentionAggregationsRepository: Provider = {
provide: DI.retentionAggregationsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation),
+ useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $flashsRepository: Provider = {
provide: DI.flashsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFlash),
+ useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $flashLikesRepository: Provider = {
provide: DI.flashLikesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiFlashLike),
+ useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $rolesRepository: Provider = {
provide: DI.rolesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRole),
+ useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $roleAssignmentsRepository: Provider = {
provide: DI.roleAssignmentsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment),
+ useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $userMemosRepository: Provider = {
provide: DI.userMemosRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserMemo),
+ useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
+ useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository),
inject: [DI.db],
};
const $reversiGamesRepository: Provider = {
provide: DI.reversiGamesRepository,
- useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
+ useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository),
inject: [DI.db],
};
@Module({
- imports: [
- ],
+ imports: [],
providers: [
$usersRepository,
$notesRepository,
@@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
$reversiGamesRepository,
],
})
-export class RepositoryModule {}
+export class RepositoryModule {
+}
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
new file mode 100644
index 0000000000..86fb323d1d
--- /dev/null
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -0,0 +1,98 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
+import { Serialized } from '@/types.js';
+import { id } from './util/id.js';
+
+export const systemWebhookEventTypes = [
+ // ユーザからの通報を受けたとき
+ 'abuseReport',
+ // 通報を処理したとき
+ 'abuseReportResolved',
+] as const;
+export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
+
+@Entity('system_webhook')
+export class MiSystemWebhook {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index('IDX_system_webhook_isActive', { synchronize: false })
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 最後に送信された日時.
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestSentAt: Date | null;
+
+ /**
+ * 最後に送信されたステータスコード
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public latestStatus: number | null;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * イベント種別.
+ */
+ @Index('IDX_system_webhook_on', { synchronize: false })
+ @Column('varchar', {
+ length: 128,
+ array: true,
+ default: '{}',
+ })
+ public on: SystemWebhookEventType[];
+
+ /**
+ * Webhook送信先のURL.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public url: string;
+
+ /**
+ * Webhook検証用の値.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public secret: string;
+
+ static deserialize(obj: Serialized): MiSystemWebhook {
+ return {
+ ...obj,
+ updatedAt: new Date(obj.updatedAt),
+ latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
+ };
+ }
+}
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 43d42d80dd..c72bdaa727 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,7 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
+import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
+import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
+import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
+import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
+import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
+import { OrmUtils } from 'typeorm/util/OrmUtils.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
@@ -61,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiUserMemo } from '@/models/UserMemo.js';
import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiChannel } from '@/models/Channel.js';
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
import { MiRole } from '@/models/Role.js';
@@ -70,11 +79,54 @@ import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
+import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
-import type { Repository } from 'typeorm';
+export interface MiRepository {
+ createTableColumnNames(this: Repository & MiRepository): string[];
+ insertOne(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>): Promise;
+ selectAliasColumnNames(this: Repository & MiRepository, queryBuilder: InsertQueryBuilder, builder: SelectQueryBuilder): void;
+}
+
+export const miRepository = {
+ createTableColumnNames() {
+ return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
+ },
+ async insertOne(entity, findOptions?) {
+ const queryBuilder = this.createQueryBuilder().insert().values(entity);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const mainAlias = queryBuilder.expressionMap.mainAlias!;
+ const name = mainAlias.name;
+ mainAlias.name = 't';
+ const columnNames = this.createTableColumnNames();
+ queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
+ const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ builder.expressionMap.mainAlias!.tablePath = 'cte';
+ this.selectAliasColumnNames(queryBuilder, builder);
+ if (findOptions) {
+ builder.setFindOptions(findOptions);
+ }
+ const raw = await builder.execute();
+ mainAlias.name = name;
+ const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw);
+ const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw);
+ const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias);
+ return result[0];
+ },
+ selectAliasColumnNames(queryBuilder, builder) {
+ let selectOrAddSelect = (selection: string, selectionAliasName?: string) => {
+ selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
+ return builder.select(selection, selectionAliasName);
+ };
+ for (const columnName of this.createTableColumnNames()) {
+ selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
+ }
+ },
+} satisfies MiRepository;
export {
MiAbuseUserReport,
+ MiAbuseReportNotificationRecipient,
MiAccessToken,
MiAd,
MiAnnouncement,
@@ -132,6 +184,7 @@ export {
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
+ MiSystemWebhook,
MiChannel,
MiRetentionAggregation,
MiRole,
@@ -143,70 +196,72 @@ export {
MiReversiGame,
};
-export type AbuseUserReportsRepository = Repository;
-export type AccessTokensRepository = Repository;
-export type AdsRepository = Repository;
-export type AnnouncementsRepository = Repository;
-export type AnnouncementReadsRepository = Repository;
-export type AntennasRepository = Repository;
-export type AppsRepository = Repository;
-export type AvatarDecorationsRepository = Repository;
-export type AuthSessionsRepository = Repository;
-export type BlockingsRepository = Repository;
-export type ChannelFollowingsRepository = Repository;
-export type ChannelFavoritesRepository = Repository;
-export type ClipsRepository = Repository;
-export type ClipNotesRepository = Repository;
-export type ClipFavoritesRepository = Repository;
-export type DriveFilesRepository = Repository;
-export type DriveFoldersRepository = Repository;
-export type EmojisRepository = Repository;
-export type FollowingsRepository = Repository;
-export type FollowRequestsRepository = Repository;
-export type GalleryLikesRepository = Repository;
-export type GalleryPostsRepository = Repository;
-export type HashtagsRepository = Repository;
-export type InstancesRepository = Repository;
-export type MetasRepository = Repository;
-export type ModerationLogsRepository = Repository;
-export type MutingsRepository = Repository;
-export type RenoteMutingsRepository = Repository;
-export type NotesRepository = Repository;
-export type NoteFavoritesRepository = Repository;
-export type NoteReactionsRepository = Repository;
-export type NoteThreadMutingsRepository = Repository;
-export type NoteUnreadsRepository = Repository;
-export type PagesRepository = Repository;
-export type PageLikesRepository = Repository;
-export type PasswordResetRequestsRepository = Repository;
-export type PollsRepository = Repository;
-export type PollVotesRepository = Repository;
-export type PromoNotesRepository = Repository;
-export type PromoReadsRepository = Repository;
-export type RegistrationTicketsRepository = Repository;
-export type RegistryItemsRepository = Repository;
-export type RelaysRepository = Repository;
-export type SigninsRepository = Repository;
-export type SwSubscriptionsRepository = Repository;
-export type UsedUsernamesRepository = Repository;
-export type UsersRepository = Repository;
-export type UserIpsRepository = Repository;
-export type UserKeypairsRepository = Repository;
-export type UserListsRepository = Repository;
-export type UserListFavoritesRepository = Repository;
-export type UserListMembershipsRepository = Repository;
-export type UserNotePiningsRepository = Repository;
-export type UserPendingsRepository = Repository;
-export type UserProfilesRepository = Repository;
-export type UserPublickeysRepository = Repository;
-export type UserSecurityKeysRepository = Repository;
-export type WebhooksRepository = Repository;
-export type ChannelsRepository = Repository;
-export type RetentionAggregationsRepository = Repository;
-export type RolesRepository = Repository;
-export type RoleAssignmentsRepository = Repository;
-export type FlashsRepository = Repository;
-export type FlashLikesRepository = Repository;
-export type UserMemoRepository = Repository;
-export type BubbleGameRecordsRepository = Repository;
-export type ReversiGamesRepository = Repository;
+export type AbuseUserReportsRepository = Repository & MiRepository;
+export type AbuseReportNotificationRecipientRepository = Repository