Merge branch 'develop' into feature/default-post-target-detect-from-path
This commit is contained in:
commit
1ca4d5fc65
|
@ -1,5 +1,3 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
|
@ -8,6 +6,7 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../:/workspace:cached
|
- ../:/workspace:cached
|
||||||
|
- node_modules:/workspace/node_modules
|
||||||
|
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
|
|
||||||
|
@ -46,6 +45,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
redis-data:
|
redis-data:
|
||||||
|
node_modules:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
internal_network:
|
internal_network:
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Misskey",
|
"name": "Misskey",
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
"dockerComposeFile": "compose.yml",
|
||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
"features": {
|
"features": {
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
|
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh",
|
"postCreateCommand": "/bin/bash .devcontainer/init.sh",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
set -xe
|
set -xe
|
||||||
|
|
||||||
sudo chown -R node /workspace
|
sudo chown node node_modules
|
||||||
|
git config --global --add safe.directory /workspace
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
corepack install
|
corepack install
|
||||||
corepack enable
|
corepack enable
|
||||||
|
|
|
@ -7,7 +7,7 @@ Dockerfile
|
||||||
build/
|
build/
|
||||||
built/
|
built/
|
||||||
db/
|
db/
|
||||||
docker-compose.yml
|
.devcontainer/compose.yml
|
||||||
node_modules/
|
node_modules/
|
||||||
packages/*/node_modules
|
packages/*/node_modules
|
||||||
redis/
|
redis/
|
||||||
|
@ -28,4 +28,4 @@ fluent-emojis/
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
packages/*/.vscode/
|
packages/*/.vscode/
|
||||||
packages/backend/test/docker-compose.yml
|
packages/backend/test/compose.yml
|
||||||
|
|
|
@ -2,3 +2,7 @@ contact_links:
|
||||||
- name: 💬 Misskey official Discord
|
- name: 💬 Misskey official Discord
|
||||||
url: https://discord.gg/Wp8gVStHW3
|
url: https://discord.gg/Wp8gVStHW3
|
||||||
about: Chat freely about Misskey
|
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
|
||||||
|
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -48,7 +48,7 @@ jobs:
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and Push to Docker Hub
|
- name: Build and Push to Docker Hub
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -13,14 +13,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
|
DOCKLE_VERSION: 0.4.14
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
- run: |
|
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
|
||||||
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
|
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
|
sudo dpkg -i dockle.deb
|
||||||
- run: |
|
- run: |
|
||||||
cp .config/docker_example.env .config/docker.env
|
cp .config/docker_example.env .config/docker.env
|
||||||
cp ./docker-compose_example.yml ./docker-compose.yml
|
cp ./compose_example.yml ./compose.yml
|
||||||
- run: |
|
- run: |
|
||||||
docker compose up -d web
|
docker compose up -d web
|
||||||
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
|
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
|
||||||
|
|
|
@ -10,14 +10,14 @@ on:
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
- packages/shared/.eslintrc.js
|
- packages/shared/eslint.config.js
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
- packages/misskey-js/**
|
- packages/misskey-js/**
|
||||||
- packages/shared/.eslintrc.js
|
- packages/shared/eslint.config.js
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pnpm_install:
|
pnpm_install:
|
||||||
|
|
|
@ -88,7 +88,7 @@ jobs:
|
||||||
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
|
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
|
||||||
BRANCH="$HEAD_REF"
|
BRANCH="$HEAD_REF"
|
||||||
fi
|
fi
|
||||||
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
|
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER")
|
||||||
env:
|
env:
|
||||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
|
|
@ -35,8 +35,8 @@ coverage
|
||||||
!/.config/example.yml
|
!/.config/example.yml
|
||||||
!/.config/docker_example.yml
|
!/.config/docker_example.yml
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
docker-compose.yml
|
.devcontainer/compose.yml
|
||||||
!/.devcontainer/docker-compose.yml
|
!/.devcontainer/compose.yml
|
||||||
|
|
||||||
# misskey
|
# misskey
|
||||||
/build
|
/build
|
||||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,5 +1,37 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||||
|
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||||
|
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
||||||
|
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
||||||
|
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||||
|
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
|
||||||
|
- Fix: アンテナの編集画面のボタンに隙間を追加
|
||||||
|
- Fix: テーマプレビューが見れない問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||||
|
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
|
||||||
|
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
|
||||||
|
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
|
||||||
|
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
|
||||||
|
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
|
||||||
|
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
|
||||||
|
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
|
||||||
|
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
|
||||||
|
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
|
||||||
|
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
||||||
|
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
|
||||||
|
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
|
||||||
|
- Fix: 空文字列のリアクションはフォールバックされるように
|
||||||
|
- Fix: リノートにリアクションできないように
|
||||||
|
|
||||||
|
## 2024.5.0
|
||||||
|
|
||||||
### Note
|
### Note
|
||||||
- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。
|
- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。
|
||||||
- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。
|
- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。
|
||||||
|
@ -20,6 +52,7 @@
|
||||||
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
|
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
|
||||||
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
|
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
|
||||||
- Enhance: 配信停止の理由を表示するように
|
- Enhance: 配信停止の理由を表示するように
|
||||||
|
- Enhance: サーバーのお問い合わせ先URLを設定できるようになりました
|
||||||
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
|
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
|
||||||
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
|
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
|
||||||
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
|
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
|
||||||
|
@ -75,6 +108,7 @@
|
||||||
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
|
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
|
||||||
- Fix: 連合なしの状態の読み書きができない問題を修正
|
- Fix: 連合なしの状態の読み書きができない問題を修正
|
||||||
- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正
|
- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正
|
||||||
|
- Fix: ファイルを5つ以上添付してもテキストがないとノートが折りたたまれない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
|
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
|
||||||
|
|
|
@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
|
||||||
```
|
```
|
||||||
Prepare DB/Redis for testing.
|
Prepare DB/Redis for testing.
|
||||||
```
|
```
|
||||||
docker compose -f packages/backend/test/docker-compose.yml up
|
docker compose -f packages/backend/test/compose.yaml up
|
||||||
```
|
```
|
||||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,10 @@ RUN apt-get update \
|
||||||
USER misskey
|
USER misskey
|
||||||
WORKDIR /misskey
|
WORKDIR /misskey
|
||||||
|
|
||||||
|
# add package.json to add pnpm
|
||||||
|
COPY --chown=misskey:misskey ./package.json ./package.json
|
||||||
|
RUN corepack install
|
||||||
|
|
||||||
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
|
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
|
||||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
|
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
|
||||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||||
|
|
|
@ -28,6 +28,10 @@
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
|
<a href="https://sentry.io/"><img src="https://github.com/misskey-dev/misskey/assets/4439005/98576556-222f-467a-94be-e98dbda1d852" height="30" alt="Sentry" /></a>
|
||||||
|
|
||||||
|
Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors.
|
||||||
|
|
||||||
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
|
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
|
||||||
|
|
||||||
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
|
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
|
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
|
||||||
|
|
||||||
services:
|
services:
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
|
@ -3364,6 +3364,10 @@ export interface Locale extends ILocale {
|
||||||
* 管理者情報が設定されていません。
|
* 管理者情報が設定されていません。
|
||||||
*/
|
*/
|
||||||
"noMaintainerInformationWarning": string;
|
"noMaintainerInformationWarning": string;
|
||||||
|
/**
|
||||||
|
* 問い合わせ先URLが設定されていません。
|
||||||
|
*/
|
||||||
|
"noInquiryUrlWarning": string;
|
||||||
/**
|
/**
|
||||||
* Botプロテクションが設定されていません。
|
* Botプロテクションが設定されていません。
|
||||||
*/
|
*/
|
||||||
|
@ -5471,6 +5475,14 @@ export interface Locale extends ILocale {
|
||||||
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
|
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
|
||||||
*/
|
*/
|
||||||
"fanoutTimelineDbFallbackDescription": string;
|
"fanoutTimelineDbFallbackDescription": string;
|
||||||
|
/**
|
||||||
|
* 問い合わせ先URL
|
||||||
|
*/
|
||||||
|
"inquiryUrl": string;
|
||||||
|
/**
|
||||||
|
* サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
|
||||||
|
*/
|
||||||
|
"inquiryUrlDescription": string;
|
||||||
};
|
};
|
||||||
"_accountMigration": {
|
"_accountMigration": {
|
||||||
/**
|
/**
|
||||||
|
@ -9305,6 +9317,10 @@ export interface Locale extends ILocale {
|
||||||
* Webhookを作成
|
* Webhookを作成
|
||||||
*/
|
*/
|
||||||
"createWebhook": string;
|
"createWebhook": string;
|
||||||
|
/**
|
||||||
|
* Webhookを編集
|
||||||
|
*/
|
||||||
|
"modifyWebhook": string;
|
||||||
/**
|
/**
|
||||||
* 名前
|
* 名前
|
||||||
*/
|
*/
|
||||||
|
@ -9351,6 +9367,72 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"mention": string;
|
"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": {
|
"_moderationLogTypes": {
|
||||||
/**
|
/**
|
||||||
|
@ -9497,6 +9579,30 @@ export interface Locale extends ILocale {
|
||||||
* ユーザーのバナーを解除
|
* ユーザーのバナーを解除
|
||||||
*/
|
*/
|
||||||
"unsetUserBanner": string;
|
"unsetUserBanner": string;
|
||||||
|
/**
|
||||||
|
* SystemWebhookを作成
|
||||||
|
*/
|
||||||
|
"createSystemWebhook": string;
|
||||||
|
/**
|
||||||
|
* SystemWebhookを更新
|
||||||
|
*/
|
||||||
|
"updateSystemWebhook": string;
|
||||||
|
/**
|
||||||
|
* SystemWebhookを削除
|
||||||
|
*/
|
||||||
|
"deleteSystemWebhook": string;
|
||||||
|
/**
|
||||||
|
* 通報の通知先を作成
|
||||||
|
*/
|
||||||
|
"createAbuseReportNotificationRecipient": string;
|
||||||
|
/**
|
||||||
|
* 通報の通知先を更新
|
||||||
|
*/
|
||||||
|
"updateAbuseReportNotificationRecipient": string;
|
||||||
|
/**
|
||||||
|
* 通報の通知先を削除
|
||||||
|
*/
|
||||||
|
"deleteAbuseReportNotificationRecipient": string;
|
||||||
};
|
};
|
||||||
"_fileViewer": {
|
"_fileViewer": {
|
||||||
/**
|
/**
|
||||||
|
@ -9667,7 +9773,7 @@ export interface Locale extends ILocale {
|
||||||
"_dataSaver": {
|
"_dataSaver": {
|
||||||
"_media": {
|
"_media": {
|
||||||
/**
|
/**
|
||||||
* メディアの読み込み
|
* メディアの読み込みを無効化
|
||||||
*/
|
*/
|
||||||
"title": string;
|
"title": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9677,7 +9783,7 @@ export interface Locale extends ILocale {
|
||||||
};
|
};
|
||||||
"_avatar": {
|
"_avatar": {
|
||||||
/**
|
/**
|
||||||
* アイコン画像
|
* アイコン画像のアニメーションを無効化
|
||||||
*/
|
*/
|
||||||
"title": string;
|
"title": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9687,7 +9793,7 @@ export interface Locale extends ILocale {
|
||||||
};
|
};
|
||||||
"_urlPreview": {
|
"_urlPreview": {
|
||||||
/**
|
/**
|
||||||
* URLプレビューのサムネイル
|
* URLプレビューのサムネイルを非表示
|
||||||
*/
|
*/
|
||||||
"title": string;
|
"title": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9697,7 +9803,7 @@ export interface Locale extends ILocale {
|
||||||
};
|
};
|
||||||
"_code": {
|
"_code": {
|
||||||
/**
|
/**
|
||||||
* コードハイライト
|
* コードハイライトを非表示
|
||||||
*/
|
*/
|
||||||
"title": string;
|
"title": string;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -52,7 +52,11 @@ const primaries = {
|
||||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||||
|
|
||||||
export function build() {
|
export function build() {
|
||||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
// vitestの挙動を調整するため、一度ローカル変数化する必要がある
|
||||||
|
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
|
||||||
|
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
|
||||||
|
const metaUrl = import.meta.url;
|
||||||
|
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
|
||||||
|
|
||||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||||
const removeEmpty = (obj) => {
|
const removeEmpty = (obj) => {
|
||||||
|
|
|
@ -837,6 +837,7 @@ administration: "管理"
|
||||||
accounts: "アカウント"
|
accounts: "アカウント"
|
||||||
switch: "切り替え"
|
switch: "切り替え"
|
||||||
noMaintainerInformationWarning: "管理者情報が設定されていません。"
|
noMaintainerInformationWarning: "管理者情報が設定されていません。"
|
||||||
|
noInquiryUrlWarning: "問い合わせ先URLが設定されていません。"
|
||||||
noBotProtectionWarning: "Botプロテクションが設定されていません。"
|
noBotProtectionWarning: "Botプロテクションが設定されていません。"
|
||||||
configure: "設定する"
|
configure: "設定する"
|
||||||
postToGallery: "ギャラリーへ投稿"
|
postToGallery: "ギャラリーへ投稿"
|
||||||
|
@ -1383,6 +1384,8 @@ _serverSettings:
|
||||||
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
|
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
|
||||||
fanoutTimelineDbFallback: "データベースへのフォールバック"
|
fanoutTimelineDbFallback: "データベースへのフォールバック"
|
||||||
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
||||||
|
inquiryUrl: "問い合わせ先URL"
|
||||||
|
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
||||||
|
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||||
|
@ -2468,6 +2471,7 @@ _drivecleaner:
|
||||||
|
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
createWebhook: "Webhookを作成"
|
createWebhook: "Webhookを作成"
|
||||||
|
modifyWebhook: "Webhookを編集"
|
||||||
name: "名前"
|
name: "名前"
|
||||||
secret: "シークレット"
|
secret: "シークレット"
|
||||||
events: "Webhookを実行するタイミング"
|
events: "Webhookを実行するタイミング"
|
||||||
|
@ -2480,6 +2484,26 @@ _webhookSettings:
|
||||||
renote: "Renoteされたとき"
|
renote: "Renoteされたとき"
|
||||||
reaction: "リアクションがあったとき"
|
reaction: "リアクションがあったとき"
|
||||||
mention: "メンションされたとき"
|
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:
|
_moderationLogTypes:
|
||||||
createRole: "ロールを作成"
|
createRole: "ロールを作成"
|
||||||
|
@ -2518,6 +2542,12 @@ _moderationLogTypes:
|
||||||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||||
unsetUserAvatar: "ユーザーのアイコンを解除"
|
unsetUserAvatar: "ユーザーのアイコンを解除"
|
||||||
unsetUserBanner: "ユーザーのバナーを解除"
|
unsetUserBanner: "ユーザーのバナーを解除"
|
||||||
|
createSystemWebhook: "SystemWebhookを作成"
|
||||||
|
updateSystemWebhook: "SystemWebhookを更新"
|
||||||
|
deleteSystemWebhook: "SystemWebhookを削除"
|
||||||
|
createAbuseReportNotificationRecipient: "通報の通知先を作成"
|
||||||
|
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
|
||||||
|
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "ファイルの詳細"
|
title: "ファイルの詳細"
|
||||||
|
@ -2572,16 +2602,16 @@ _externalResourceInstaller:
|
||||||
|
|
||||||
_dataSaver:
|
_dataSaver:
|
||||||
_media:
|
_media:
|
||||||
title: "メディアの読み込み"
|
title: "メディアの読み込みを無効化"
|
||||||
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
|
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
|
||||||
_avatar:
|
_avatar:
|
||||||
title: "アイコン画像"
|
title: "アイコン画像のアニメーションを無効化"
|
||||||
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
|
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
|
||||||
_urlPreview:
|
_urlPreview:
|
||||||
title: "URLプレビューのサムネイル"
|
title: "URLプレビューのサムネイルを非表示"
|
||||||
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
|
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
|
||||||
_code:
|
_code:
|
||||||
title: "コードハイライト"
|
title: "コードハイライトを非表示"
|
||||||
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
||||||
|
|
||||||
_hemisphere:
|
_hemisphere:
|
||||||
|
|
|
@ -316,6 +316,7 @@ selectFile: "选择文件"
|
||||||
selectFiles: "选择文件"
|
selectFiles: "选择文件"
|
||||||
selectFolder: "选择文件夹"
|
selectFolder: "选择文件夹"
|
||||||
selectFolders: "选择多个文件夹"
|
selectFolders: "选择多个文件夹"
|
||||||
|
fileNotSelected: "未选择文件"
|
||||||
renameFile: "重命名文件"
|
renameFile: "重命名文件"
|
||||||
folderName: "文件夹名称"
|
folderName: "文件夹名称"
|
||||||
createFolder: "创建文件夹"
|
createFolder: "创建文件夹"
|
||||||
|
@ -2358,6 +2359,7 @@ _deck:
|
||||||
alwaysShowMainColumn: "总是显示主列"
|
alwaysShowMainColumn: "总是显示主列"
|
||||||
columnAlign: "列对齐"
|
columnAlign: "列对齐"
|
||||||
addColumn: "添加列"
|
addColumn: "添加列"
|
||||||
|
newNoteNotificationSettings: "新帖子通知设定"
|
||||||
configureColumn: "列设置"
|
configureColumn: "列设置"
|
||||||
swapLeft: "向左移动"
|
swapLeft: "向左移动"
|
||||||
swapRight: "向右移动"
|
swapRight: "向右移动"
|
||||||
|
|
|
@ -316,6 +316,7 @@ selectFile: "選擇檔案"
|
||||||
selectFiles: "選擇檔案"
|
selectFiles: "選擇檔案"
|
||||||
selectFolder: "選擇資料夾"
|
selectFolder: "選擇資料夾"
|
||||||
selectFolders: "選擇資料夾"
|
selectFolders: "選擇資料夾"
|
||||||
|
fileNotSelected: "尚未選擇檔案"
|
||||||
renameFile: "重新命名檔案"
|
renameFile: "重新命名檔案"
|
||||||
folderName: "資料夾名稱"
|
folderName: "資料夾名稱"
|
||||||
createFolder: "新增資料夾"
|
createFolder: "新增資料夾"
|
||||||
|
@ -471,7 +472,7 @@ retype: "重新輸入"
|
||||||
noteOf: "{user}的貼文"
|
noteOf: "{user}的貼文"
|
||||||
quoteAttached: "引用"
|
quoteAttached: "引用"
|
||||||
quoteQuestion: "是否要引用?"
|
quoteQuestion: "是否要引用?"
|
||||||
attachAsFileQuestion: "剪貼簿的文字較長。請問是否要改成附加檔案呢?"
|
attachAsFileQuestion: "剪貼簿的文字較長。請問是否要將其以文字檔的方式附加呢?"
|
||||||
noMessagesYet: "沒有訊息"
|
noMessagesYet: "沒有訊息"
|
||||||
newMessageExists: "有新的訊息"
|
newMessageExists: "有新的訊息"
|
||||||
onlyOneFileCanBeAttached: "只能加入一個附件"
|
onlyOneFileCanBeAttached: "只能加入一個附件"
|
||||||
|
@ -1025,6 +1026,7 @@ thisPostMayBeAnnoyingHome: "發佈到首頁"
|
||||||
thisPostMayBeAnnoyingCancel: "退出"
|
thisPostMayBeAnnoyingCancel: "退出"
|
||||||
thisPostMayBeAnnoyingIgnore: "直接發佈貼文"
|
thisPostMayBeAnnoyingIgnore: "直接發佈貼文"
|
||||||
collapseRenotes: "省略顯示已看過的轉發貼文"
|
collapseRenotes: "省略顯示已看過的轉發貼文"
|
||||||
|
collapseRenotesDescription: "將已做過反應和轉發的貼文折疊顯示。"
|
||||||
internalServerError: "內部伺服器錯誤"
|
internalServerError: "內部伺服器錯誤"
|
||||||
internalServerErrorDescription: "內部伺服器出現意外錯誤。"
|
internalServerErrorDescription: "內部伺服器出現意外錯誤。"
|
||||||
copyErrorInfo: "複製錯誤資訊"
|
copyErrorInfo: "複製錯誤資訊"
|
||||||
|
@ -1241,8 +1243,8 @@ alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
|
||||||
inquiry: "聯絡我們"
|
inquiry: "聯絡我們"
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "傳送狀態"
|
status: "傳送狀態"
|
||||||
stop: "已凍結"
|
stop: "停止傳送"
|
||||||
resume: "繼續傳送"
|
resume: "恢復傳送"
|
||||||
_type:
|
_type:
|
||||||
none: "直播中"
|
none: "直播中"
|
||||||
manuallySuspended: "手動暫停中"
|
manuallySuspended: "手動暫停中"
|
||||||
|
@ -1373,6 +1375,8 @@ _serverSettings:
|
||||||
fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
|
fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
|
||||||
fanoutTimelineDbFallback: "資料庫的回退"
|
fanoutTimelineDbFallback: "資料庫的回退"
|
||||||
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
|
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
|
||||||
|
inquiryUrl: "聯絡表單網址"
|
||||||
|
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "從其他帳戶遷移到這個帳戶"
|
moveFrom: "從其他帳戶遷移到這個帳戶"
|
||||||
moveFromSub: "為另一個帳戶建立別名"
|
moveFromSub: "為另一個帳戶建立別名"
|
||||||
|
@ -2358,6 +2362,7 @@ _deck:
|
||||||
alwaysShowMainColumn: "總是顯示主欄"
|
alwaysShowMainColumn: "總是顯示主欄"
|
||||||
columnAlign: "對齊欄位"
|
columnAlign: "對齊欄位"
|
||||||
addColumn: "新增欄位"
|
addColumn: "新增欄位"
|
||||||
|
newNoteNotificationSettings: "新貼文通知的設定"
|
||||||
configureColumn: "欄位的設定"
|
configureColumn: "欄位的設定"
|
||||||
swapLeft: "向左移動"
|
swapLeft: "向左移動"
|
||||||
swapRight: "向右移動"
|
swapRight: "向右移動"
|
||||||
|
|
22
package.json
22
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.3.1",
|
"version": "2024.5.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -55,20 +55,22 @@
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.38",
|
"postcss": "8.4.38",
|
||||||
"tar": "6.2.1",
|
"tar": "6.2.1",
|
||||||
"terser": "5.30.3",
|
"terser": "5.31.1",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.5.3",
|
||||||
"esbuild": "0.20.2",
|
"esbuild": "0.22.0",
|
||||||
"glob": "10.3.12"
|
"glob": "10.3.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.12.7",
|
"@misskey-dev/eslint-plugin": "2.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
"@types/node": "20.14.9",
|
||||||
"@typescript-eslint/parser": "7.7.1",
|
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||||
|
"@typescript-eslint/parser": "7.15.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.7.3",
|
"cypress": "13.13.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "9.6.0",
|
||||||
|
"globals": "15.7.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"start-server-and-test": "2.0.3"
|
"start-server-and-test": "2.0.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs-core": "4.4.0"
|
"@tensorflow/tfjs-core": "4.4.0"
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
/built
|
|
||||||
/.eslintrc.js
|
|
||||||
/@types/**/*
|
|
|
@ -1,32 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parserOptions: {
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'../shared/.eslintrc.js',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'import/order': ['warn', {
|
|
||||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
|
||||||
'pathGroups': [
|
|
||||||
{
|
|
||||||
'pattern': '@/**',
|
|
||||||
'group': 'external',
|
|
||||||
'position': 'after'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
'no-restricted-globals': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
'name': '__dirname',
|
|
||||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': '__filename',
|
|
||||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import sharedConfig from '../shared/eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
ignores: ['**/node_modules', 'built', '@types/**/*'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/order': ['warn', {
|
||||||
|
groups: [
|
||||||
|
'builtin',
|
||||||
|
'external',
|
||||||
|
'internal',
|
||||||
|
'parent',
|
||||||
|
'sibling',
|
||||||
|
'index',
|
||||||
|
'object',
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
pathGroups: [{
|
||||||
|
pattern: '@/**',
|
||||||
|
group: 'external',
|
||||||
|
position: 'after',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
'no-restricted-globals': ['error', {
|
||||||
|
name: '__dirname',
|
||||||
|
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||||
|
}, {
|
||||||
|
name: '__filename',
|
||||||
|
message: 'Not in ESModule. Use `import.meta.url` instead.',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -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";
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.10.0"
|
"node": "^20.10.0 || ^22.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./built/boot/entry.js",
|
"start": "node ./built/boot/entry.js",
|
||||||
|
@ -65,43 +65,43 @@
|
||||||
"utf-8-validate": "6.0.3"
|
"utf-8-validate": "6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.600.0",
|
||||||
"@aws-sdk/lib-storage": "3.412.0",
|
"@aws-sdk/lib-storage": "3.600.0",
|
||||||
"@bull-board/api": "5.17.0",
|
"@bull-board/api": "5.20.5",
|
||||||
"@bull-board/fastify": "5.17.0",
|
"@bull-board/fastify": "5.20.5",
|
||||||
"@bull-board/ui": "5.17.0",
|
"@bull-board/ui": "5.20.5",
|
||||||
"@discordapp/twemoji": "15.0.3",
|
"@discordapp/twemoji": "15.0.3",
|
||||||
"@fastify/accepts": "4.3.0",
|
"@fastify/accepts": "4.3.0",
|
||||||
"@fastify/cookie": "9.3.1",
|
"@fastify/cookie": "9.3.1",
|
||||||
"@fastify/cors": "9.0.1",
|
"@fastify/cors": "9.0.1",
|
||||||
"@fastify/express": "3.0.0",
|
"@fastify/express": "3.0.0",
|
||||||
"@fastify/http-proxy": "9.5.0",
|
"@fastify/http-proxy": "9.5.0",
|
||||||
"@fastify/multipart": "8.2.0",
|
"@fastify/multipart": "8.3.0",
|
||||||
"@fastify/static": "7.0.3",
|
"@fastify/static": "7.0.4",
|
||||||
"@fastify/view": "9.1.0",
|
"@fastify/view": "9.1.0",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.1.0",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@napi-rs/canvas": "^0.1.52",
|
"@napi-rs/canvas": "^0.1.53",
|
||||||
"@nestjs/common": "10.3.8",
|
"@nestjs/common": "10.3.10",
|
||||||
"@nestjs/core": "10.3.8",
|
"@nestjs/core": "10.3.10",
|
||||||
"@nestjs/testing": "10.3.8",
|
"@nestjs/testing": "10.3.10",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "^8.5.0",
|
"@sentry/node": "8.13.0",
|
||||||
"@sentry/profiling-node": "^8.5.0",
|
"@sentry/profiling-node": "8.13.0",
|
||||||
"@simplewebauthn/server": "10.0.0",
|
"@simplewebauthn/server": "10.0.0",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"@smithy/node-http-handler": "2.5.0",
|
"@smithy/node-http-handler": "2.5.0",
|
||||||
"@swc/cli": "0.3.12",
|
"@swc/cli": "0.3.12",
|
||||||
"@swc/core": "1.4.17",
|
"@swc/core": "1.6.6",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.13.0",
|
"ajv": "8.16.0",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"bullmq": "5.7.8",
|
"bullmq": "5.8.3",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.2",
|
"cbor": "9.0.2",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
|
@ -112,27 +112,27 @@
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fastify": "4.26.2",
|
"fastify": "4.28.1",
|
||||||
"fastify-raw-body": "4.3.0",
|
"fastify-raw-body": "4.3.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.0.0",
|
"file-type": "19.0.0",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "14.2.1",
|
"got": "14.4.1",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"htmlescape": "1.1.1",
|
"htmlescape": "1.1.1",
|
||||||
"http-link-header": "1.1.3",
|
"http-link-header": "1.1.3",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "4.0.1",
|
||||||
"ipaddr.js": "2.2.0",
|
"ipaddr.js": "2.2.0",
|
||||||
"is-svg": "5.0.0",
|
"is-svg": "5.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "24.0.0",
|
"jsdom": "24.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.2",
|
"jsonld": "8.3.2",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"meilisearch": "0.38.0",
|
"meilisearch": "0.41.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@ -142,24 +142,24 @@
|
||||||
"nanoid": "5.0.7",
|
"nanoid": "5.0.7",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.13",
|
"nodemailer": "6.9.14",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"oauth2orize": "1.12.0",
|
"oauth2orize": "1.12.0",
|
||||||
"oauth2orize-pkce": "0.1.2",
|
"oauth2orize-pkce": "0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.2.3",
|
"otpauth": "9.3.1",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.11.5",
|
"pg": "8.12.0",
|
||||||
"pkce-challenge": "4.1.0",
|
"pkce-challenge": "4.1.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.3",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.20.10",
|
"re2": "1.21.3",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
@ -167,27 +167,26 @@
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
"secure-json-parse": "2.7.0",
|
"secure-json-parse": "2.7.0",
|
||||||
"sharp": "0.33.3",
|
"sharp": "0.33.4",
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"systeminformation": "5.22.7",
|
"systeminformation": "5.22.11",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typeorm": "0.3.20",
|
"typeorm": "0.3.20",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.5.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.7",
|
"web-push": "3.6.7",
|
||||||
"ws": "8.17.0",
|
"ws": "8.17.1",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@nestjs/platform-express": "10.3.10",
|
||||||
"@nestjs/platform-express": "10.3.8",
|
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"@swc/jest": "0.2.36",
|
"@swc/jest": "0.2.36",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
|
@ -197,22 +196,21 @@
|
||||||
"@types/color-convert": "2.0.3",
|
"@types/color-convert": "2.0.3",
|
||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/fluent-ffmpeg": "2.1.24",
|
"@types/fluent-ffmpeg": "2.1.24",
|
||||||
"@types/htmlescape": "^1.1.3",
|
"@types/htmlescape": "1.1.3",
|
||||||
"@types/http-link-header": "1.0.5",
|
"@types/http-link-header": "1.0.7",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.6",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/jsonld": "1.5.13",
|
"@types/jsonld": "1.5.14",
|
||||||
"@types/jsrsasign": "10.5.14",
|
"@types/jsrsasign": "10.5.14",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.14.9",
|
||||||
"@types/node-fetch": "3.0.3",
|
|
||||||
"@types/nodemailer": "6.4.15",
|
"@types/nodemailer": "6.4.15",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.5",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.11.5",
|
"@types/pg": "8.11.6",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
|
@ -228,18 +226,17 @@
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.3",
|
"@types/web-push": "3.6.3",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||||
"@typescript-eslint/parser": "7.7.1",
|
"@typescript-eslint/parser": "7.15.0",
|
||||||
"aws-sdk-client-mock": "3.0.1",
|
"aws-sdk-client-mock": "4.0.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.57.0",
|
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"execa": "8.0.1",
|
"execa": "9.2.0",
|
||||||
"fkill": "^9.0.0",
|
"fkill": "9.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-mock": "29.7.0",
|
"jest-mock": "29.7.0",
|
||||||
"nodemon": "3.1.0",
|
"nodemon": "3.1.4",
|
||||||
"pid-port": "1.0.0",
|
"pid-port": "1.0.0",
|
||||||
"simple-oauth2": "5.0.0"
|
"simple-oauth2": "5.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ function execStart() {
|
||||||
|
|
||||||
async function killProc() {
|
async function killProc() {
|
||||||
if (backendProcess) {
|
if (backendProcess) {
|
||||||
|
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
|
||||||
backendProcess.kill();
|
backendProcess.kill();
|
||||||
await new Promise(resolve => backendProcess.on('exit', resolve));
|
await new Promise(resolve => backendProcess.on('exit', resolve));
|
||||||
backendProcess = undefined;
|
backendProcess = undefined;
|
||||||
|
@ -46,6 +47,7 @@ async function killProc() {
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
||||||
|
serialization: "json",
|
||||||
})
|
})
|
||||||
.on('message', async (message) => {
|
.on('message', async (message) => {
|
||||||
if (message.type === 'exit') {
|
if (message.type === 'exit') {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const nestLogger = logger.createSubLogger('nest', 'green', false);
|
const nestLogger = logger.createSubLogger('nest', 'green');
|
||||||
|
|
||||||
export class NestLogger implements LoggerService {
|
export class NestLogger implements LoggerService {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
|
||||||
EventEmitter.defaultMaxListeners = 128;
|
EventEmitter.defaultMaxListeners = 128;
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
const clusterLogger = logger.createSubLogger('cluster', 'orange');
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
//#region Events
|
//#region Events
|
||||||
|
|
|
@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
|
||||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||||
|
|
||||||
const themeColor = chalk.hex('#86b300');
|
const themeColor = chalk.hex('#86b300');
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
|
import { loadConfig } from '@/config.js';
|
||||||
import { jobQueue, server } from './common.js';
|
import { jobQueue, server } from './common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init worker process
|
* Init worker process
|
||||||
*/
|
*/
|
||||||
export async function workerMain() {
|
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) {
|
if (envOption.onlyServer) {
|
||||||
await server();
|
await server();
|
||||||
} else if (envOption.onlyQueue) {
|
} else if (envOption.onlyQueue) {
|
||||||
|
|
|
@ -0,0 +1,405 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import { Brackets, In, IsNull, Not } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import type {
|
||||||
|
AbuseReportNotificationRecipientRepository,
|
||||||
|
MiAbuseReportNotificationRecipient,
|
||||||
|
MiAbuseUserReport,
|
||||||
|
MiUser,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { IdService } from './IdService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.abuseReportNotificationRecipientRepository)
|
||||||
|
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
private idService: IdService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private emailService: EmailService,
|
||||||
|
private metaService: MetaService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||||
|
* 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
|
||||||
|
*
|
||||||
|
* @see RoleService.getModeratorIds
|
||||||
|
* @see GlobalEventService.publishAdminStream
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
|
||||||
|
if (abuseReports.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moderatorIds = await this.roleService.getModeratorIds(true, true);
|
||||||
|
|
||||||
|
for (const moderatorId of moderatorIds) {
|
||||||
|
for (const abuseReport of abuseReports) {
|
||||||
|
this.globalEventService.publishAdminStream(
|
||||||
|
moderatorId,
|
||||||
|
'newAbuseUserReport',
|
||||||
|
{
|
||||||
|
id: abuseReport.id,
|
||||||
|
targetUserId: abuseReport.targetUserId,
|
||||||
|
reporterId: abuseReport.reporterId,
|
||||||
|
comment: abuseReport.comment,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||||
|
* メールアドレスの送信先は以下の通り.
|
||||||
|
* - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
|
||||||
|
* - metaテーブルに設定されているメールアドレス
|
||||||
|
*
|
||||||
|
* @see EmailService.sendEmail
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async notifyMail(abuseReports: MiAbuseUserReport[]) {
|
||||||
|
if (abuseReports.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
|
||||||
|
.filter(it => it.isActive && it.userProfile?.emailVerified)
|
||||||
|
.map(it => it.userProfile?.email)
|
||||||
|
.filter(x => x != null),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 送信先の鮮度を保つため、毎回取得する
|
||||||
|
const meta = await this.metaService.fetch(true);
|
||||||
|
recipientEMailAddresses.push(
|
||||||
|
...(meta.email ? [meta.email] : []),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recipientEMailAddresses.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mailAddress of recipientEMailAddresses) {
|
||||||
|
await Promise.all(
|
||||||
|
abuseReports.map(it => {
|
||||||
|
// TODO: 送信処理はJobQueue化したい
|
||||||
|
return this.emailService.sendEmail(
|
||||||
|
mailAddress,
|
||||||
|
'New Abuse Report',
|
||||||
|
sanitizeHtml(it.comment),
|
||||||
|
sanitizeHtml(it.comment),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||||
|
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
|
||||||
|
*
|
||||||
|
* @see SystemWebhookService.enqueueSystemWebhook
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async notifySystemWebhook(
|
||||||
|
abuseReports: MiAbuseUserReport[],
|
||||||
|
type: 'abuseReport' | 'abuseReportResolved',
|
||||||
|
) {
|
||||||
|
if (abuseReports.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientWebhookIds = await this.fetchWebhookRecipients()
|
||||||
|
.then(it => it
|
||||||
|
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||||
|
.map(it => it.systemWebhookId)
|
||||||
|
.filter(x => x != null));
|
||||||
|
for (const webhookId of recipientWebhookIds) {
|
||||||
|
await Promise.all(
|
||||||
|
abuseReports.map(it => {
|
||||||
|
return this.systemWebhookService.enqueueSystemWebhook(
|
||||||
|
webhookId,
|
||||||
|
type,
|
||||||
|
it,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通報の通知先一覧を取得する.
|
||||||
|
*
|
||||||
|
* @param {Object} [params] クエリの取得条件
|
||||||
|
* @param {Object} [params.method] 取得する通知先の通知方法
|
||||||
|
* @param {Object} [opts] 動作時の詳細なオプション
|
||||||
|
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||||
|
* @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
|
||||||
|
* @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
|
||||||
|
* @see removeUnauthorizedRecipientUsers
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async fetchRecipients(
|
||||||
|
params?: {
|
||||||
|
ids?: MiAbuseReportNotificationRecipient['id'][],
|
||||||
|
method?: RecipientMethod[],
|
||||||
|
},
|
||||||
|
opts?: {
|
||||||
|
removeUnauthorized?: boolean,
|
||||||
|
joinUser?: boolean,
|
||||||
|
joinSystemWebhook?: boolean,
|
||||||
|
},
|
||||||
|
): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||||
|
const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
|
||||||
|
|
||||||
|
if (opts?.joinUser) {
|
||||||
|
query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
|
||||||
|
query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.joinSystemWebhook) {
|
||||||
|
query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.ids) {
|
||||||
|
query.andWhere({ id: In(params.ids) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.method) {
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
if (params.method?.includes('email')) {
|
||||||
|
qb.orWhere({ method: 'email', userId: Not(IsNull()) });
|
||||||
|
}
|
||||||
|
if (params.method?.includes('webhook')) {
|
||||||
|
qb.orWhere({ method: 'webhook', userId: IsNull() });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipients = await query.getMany();
|
||||||
|
if (recipients.length <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
|
||||||
|
return (opts?.removeUnauthorized ?? true)
|
||||||
|
? await this.removeUnauthorizedRecipientUsers(recipients)
|
||||||
|
: recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EMailの通知先一覧を取得する.
|
||||||
|
* リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
|
||||||
|
*
|
||||||
|
* @param {Object} [opts]
|
||||||
|
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||||
|
* @see removeUnauthorizedRecipientUsers
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async fetchEMailRecipients(opts?: {
|
||||||
|
removeUnauthorized?: boolean
|
||||||
|
}): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||||
|
return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhookの通知先一覧を取得する.
|
||||||
|
* リレーション先の{@link MiSystemWebhook}も同時に取得する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||||
|
return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先を作成する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async createRecipient(
|
||||||
|
params: {
|
||||||
|
isActive: MiAbuseReportNotificationRecipient['isActive'];
|
||||||
|
name: MiAbuseReportNotificationRecipient['name'];
|
||||||
|
method: MiAbuseReportNotificationRecipient['method'];
|
||||||
|
userId: MiAbuseReportNotificationRecipient['userId'];
|
||||||
|
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
|
||||||
|
},
|
||||||
|
updater: MiUser,
|
||||||
|
): Promise<MiAbuseReportNotificationRecipient> {
|
||||||
|
const id = this.idService.gen();
|
||||||
|
await this.abuseReportNotificationRecipientRepository.insert({
|
||||||
|
...params,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
|
||||||
|
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'createAbuseReportNotificationRecipient', {
|
||||||
|
recipientId: id,
|
||||||
|
recipient: created,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先を更新する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async updateRecipient(
|
||||||
|
params: {
|
||||||
|
id: MiAbuseReportNotificationRecipient['id'];
|
||||||
|
isActive: MiAbuseReportNotificationRecipient['isActive'];
|
||||||
|
name: MiAbuseReportNotificationRecipient['name'];
|
||||||
|
method: MiAbuseReportNotificationRecipient['method'];
|
||||||
|
userId: MiAbuseReportNotificationRecipient['userId'];
|
||||||
|
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
|
||||||
|
},
|
||||||
|
updater: MiUser,
|
||||||
|
): Promise<MiAbuseReportNotificationRecipient> {
|
||||||
|
const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
|
||||||
|
|
||||||
|
await this.abuseReportNotificationRecipientRepository.update(params.id, {
|
||||||
|
isActive: params.isActive,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: params.name,
|
||||||
|
method: params.method,
|
||||||
|
userId: params.userId,
|
||||||
|
systemWebhookId: params.systemWebhookId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
|
||||||
|
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'updateAbuseReportNotificationRecipient', {
|
||||||
|
recipientId: params.id,
|
||||||
|
before: beforeEntity,
|
||||||
|
after: afterEntity,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
|
||||||
|
return afterEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先を削除する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async deleteRecipient(
|
||||||
|
id: MiAbuseReportNotificationRecipient['id'],
|
||||||
|
updater: MiUser,
|
||||||
|
) {
|
||||||
|
const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
|
||||||
|
|
||||||
|
await this.abuseReportNotificationRecipientRepository.delete(id);
|
||||||
|
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'deleteAbuseReportNotificationRecipient', {
|
||||||
|
recipientId: id,
|
||||||
|
recipient: entity,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* モデレータ権限を持たない(*1)通知先ユーザを削除する.
|
||||||
|
*
|
||||||
|
* *1: 以下の両方を満たすものの事を言う
|
||||||
|
* - 通知先にユーザIDが設定されている
|
||||||
|
* - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
|
||||||
|
*
|
||||||
|
* @param recipients 通知先一覧の配列
|
||||||
|
* @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||||
|
const userRecipients = recipients.filter(it => it.userId !== null);
|
||||||
|
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
|
||||||
|
if (recipientUserIds.size <= 0) {
|
||||||
|
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
// モデレータ権限の有無で通知先設定を振り分ける
|
||||||
|
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
|
||||||
|
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||||
|
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||||
|
for (const recipient of userRecipients) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (authorizedUserIds.includes(recipient.userId!)) {
|
||||||
|
authorizedUserRecipients.push(recipient);
|
||||||
|
} else {
|
||||||
|
unauthorizedUserRecipients.push(recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// モデレータ権限を持たない通知先をDBから削除する
|
||||||
|
if (unauthorizedUserRecipients.length > 0) {
|
||||||
|
await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
|
||||||
|
}
|
||||||
|
const nonUserRecipients = recipients.filter(it => it.userId === null);
|
||||||
|
return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
if (obj.channel !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'roleUpdated':
|
||||||
|
case 'roleDeleted':
|
||||||
|
case 'userRoleUnassigned': {
|
||||||
|
// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
|
||||||
|
process.nextTick(async () => {
|
||||||
|
const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
|
||||||
|
userId: Not(IsNull()),
|
||||||
|
});
|
||||||
|
await this.removeUnauthorizedRecipientUsers(recipients);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { IdService } from './IdService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AbuseReportService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.abuseUserReportsRepository)
|
||||||
|
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
private idService: IdService,
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
private instanceActorService: InstanceActorService,
|
||||||
|
private apRendererService: ApRendererService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
|
||||||
|
* - 管理者用Redisイベント
|
||||||
|
* - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
|
||||||
|
* - SystemWebhook
|
||||||
|
*
|
||||||
|
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||||
|
* @see AbuseReportNotificationService.notify
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async report(params: {
|
||||||
|
targetUserId: MiAbuseUserReport['targetUserId'],
|
||||||
|
targetUserHost: MiAbuseUserReport['targetUserHost'],
|
||||||
|
reporterId: MiAbuseUserReport['reporterId'],
|
||||||
|
reporterHost: MiAbuseUserReport['reporterHost'],
|
||||||
|
comment: string,
|
||||||
|
}[]) {
|
||||||
|
const entities = params.map(param => {
|
||||||
|
return {
|
||||||
|
id: this.idService.gen(),
|
||||||
|
targetUserId: param.targetUserId,
|
||||||
|
targetUserHost: param.targetUserHost,
|
||||||
|
reporterId: param.reporterId,
|
||||||
|
reporterHost: param.reporterHost,
|
||||||
|
comment: param.comment,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const reports = Array.of<MiAbuseUserReport>();
|
||||||
|
for (const entity of entities) {
|
||||||
|
const report = await this.abuseUserReportsRepository.insertOne(entity);
|
||||||
|
reports.push(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
this.abuseReportNotificationService.notifyAdminStream(reports),
|
||||||
|
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
|
||||||
|
this.abuseReportNotificationService.notifyMail(reports),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通報を解決し、その内容を下記の手段で管理者各位に通知する.
|
||||||
|
* - SystemWebhook
|
||||||
|
*
|
||||||
|
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||||
|
* @param operator 通報を処理したユーザ
|
||||||
|
* @see AbuseReportNotificationService.notify
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async resolve(
|
||||||
|
params: {
|
||||||
|
reportId: string;
|
||||||
|
forward: boolean;
|
||||||
|
}[],
|
||||||
|
operator: MiUser,
|
||||||
|
) {
|
||||||
|
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||||
|
const reports = await this.abuseUserReportsRepository.findBy({
|
||||||
|
id: In(params.map(it => it.reportId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const report of reports) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const ps = paramsMap.get(report.id)!;
|
||||||
|
|
||||||
|
await this.abuseUserReportsRepository.update(report.id, {
|
||||||
|
resolved: true,
|
||||||
|
assigneeId: operator.id,
|
||||||
|
forwarded: ps.forward && report.targetUserHost !== null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ps.forward && report.targetUserHost != null) {
|
||||||
|
const actor = await this.instanceActorService.getInstanceActor();
|
||||||
|
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||||
|
const contextAssignedFlag = this.apRendererService.addContext(flag);
|
||||||
|
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.moderationLogService
|
||||||
|
.log(operator, 'resolveAbuseReport', {
|
||||||
|
reportId: report.id,
|
||||||
|
report: report,
|
||||||
|
forwarded: ps.forward && report.targetUserHost !== null,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||||
|
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,7 +67,7 @@ export class AnnouncementService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
|
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
|
||||||
const announcement = await this.announcementsRepository.insert({
|
const announcement = await this.announcementsRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
title: values.title,
|
title: values.title,
|
||||||
|
@ -79,7 +79,7 @@ export class AnnouncementService {
|
||||||
silence: values.silence,
|
silence: values.silence,
|
||||||
needConfirmationToRead: values.needConfirmationToRead,
|
needConfirmationToRead: values.needConfirmationToRead,
|
||||||
userId: values.userId,
|
userId: values.userId,
|
||||||
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
const packed = await this.announcementEntityService.pack(announcement);
|
const packed = await this.announcementEntityService.pack(announcement);
|
||||||
|
|
||||||
|
|
|
@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
|
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
|
||||||
const created = await this.avatarDecorationsRepository.insert({
|
const created = await this.avatarDecorationsRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
...options,
|
...options,
|
||||||
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
|
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
|
||||||
|
|
||||||
|
|
|
@ -41,17 +41,17 @@ export class ClipService {
|
||||||
const currentCount = await this.clipsRepository.countBy({
|
const currentCount = await this.clipsRepository.countBy({
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||||
throw new ClipService.TooManyClipsError();
|
throw new ClipService.TooManyClipsError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const clip = await this.clipsRepository.insert({
|
const clip = await this.clipsRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
name: name,
|
name: name,
|
||||||
isPublic: isPublic,
|
isPublic: isPublic,
|
||||||
description: description,
|
description: description,
|
||||||
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
return clip;
|
return clip;
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ export class ClipService {
|
||||||
const currentCount = await this.clipNotesRepository.countBy({
|
const currentCount = await this.clipNotesRepository.countBy({
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
});
|
});
|
||||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||||
throw new ClipService.TooManyClipNotesError();
|
throw new ClipService.TooManyClipNotesError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,13 @@
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
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 { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
|
@ -56,7 +63,7 @@ import { UserMutingService } from './UserMutingService.js';
|
||||||
import { UserSuspendService } from './UserSuspendService.js';
|
import { UserSuspendService } from './UserSuspendService.js';
|
||||||
import { UserAuthService } from './UserAuthService.js';
|
import { UserAuthService } from './UserAuthService.js';
|
||||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||||
import { WebhookService } from './WebhookService.js';
|
import { UserWebhookService } from './UserWebhookService.js';
|
||||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||||
import { UtilityService } from './UtilityService.js';
|
import { UtilityService } from './UtilityService.js';
|
||||||
import { FileInfoService } from './FileInfoService.js';
|
import { FileInfoService } from './FileInfoService.js';
|
||||||
|
@ -144,6 +151,8 @@ import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
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 $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||||
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
||||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
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 $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
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 $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
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 $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
|
||||||
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
|
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
|
||||||
|
const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
|
||||||
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
|
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
|
||||||
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
|
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
|
||||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
|
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 $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
|
||||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||||
|
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||||
|
|
||||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||||
|
@ -285,6 +297,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LoggerService,
|
LoggerService,
|
||||||
|
AbuseReportService,
|
||||||
|
AbuseReportNotificationService,
|
||||||
AccountMoveService,
|
AccountMoveService,
|
||||||
AccountUpdateService,
|
AccountUpdateService,
|
||||||
AiService,
|
AiService,
|
||||||
|
@ -337,7 +351,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
WebhookService,
|
UserWebhookService,
|
||||||
|
SystemWebhookService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -366,6 +381,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
|
|
||||||
AbuseUserReportEntityService,
|
AbuseUserReportEntityService,
|
||||||
AnnouncementEntityService,
|
AnnouncementEntityService,
|
||||||
|
AbuseReportNotificationRecipientEntityService,
|
||||||
AntennaEntityService,
|
AntennaEntityService,
|
||||||
AppEntityService,
|
AppEntityService,
|
||||||
AuthSessionEntityService,
|
AuthSessionEntityService,
|
||||||
|
@ -399,6 +415,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
RoleEntityService,
|
RoleEntityService,
|
||||||
ReversiGameEntityService,
|
ReversiGameEntityService,
|
||||||
MetaEntityService,
|
MetaEntityService,
|
||||||
|
SystemWebhookEntityService,
|
||||||
|
|
||||||
ApAudienceService,
|
ApAudienceService,
|
||||||
ApDbResolverService,
|
ApDbResolverService,
|
||||||
|
@ -422,6 +439,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
|
|
||||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||||
$LoggerService,
|
$LoggerService,
|
||||||
|
$AbuseReportService,
|
||||||
|
$AbuseReportNotificationService,
|
||||||
$AccountMoveService,
|
$AccountMoveService,
|
||||||
$AccountUpdateService,
|
$AccountUpdateService,
|
||||||
$AiService,
|
$AiService,
|
||||||
|
@ -474,7 +493,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$WebhookService,
|
$UserWebhookService,
|
||||||
|
$SystemWebhookService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
@ -503,6 +523,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
|
|
||||||
$AbuseUserReportEntityService,
|
$AbuseUserReportEntityService,
|
||||||
$AnnouncementEntityService,
|
$AnnouncementEntityService,
|
||||||
|
$AbuseReportNotificationRecipientEntityService,
|
||||||
$AntennaEntityService,
|
$AntennaEntityService,
|
||||||
$AppEntityService,
|
$AppEntityService,
|
||||||
$AuthSessionEntityService,
|
$AuthSessionEntityService,
|
||||||
|
@ -536,6 +557,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$RoleEntityService,
|
$RoleEntityService,
|
||||||
$ReversiGameEntityService,
|
$ReversiGameEntityService,
|
||||||
$MetaEntityService,
|
$MetaEntityService,
|
||||||
|
$SystemWebhookEntityService,
|
||||||
|
|
||||||
$ApAudienceService,
|
$ApAudienceService,
|
||||||
$ApDbResolverService,
|
$ApDbResolverService,
|
||||||
|
@ -560,6 +582,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
exports: [
|
exports: [
|
||||||
QueueModule,
|
QueueModule,
|
||||||
LoggerService,
|
LoggerService,
|
||||||
|
AbuseReportService,
|
||||||
|
AbuseReportNotificationService,
|
||||||
AccountMoveService,
|
AccountMoveService,
|
||||||
AccountUpdateService,
|
AccountUpdateService,
|
||||||
AiService,
|
AiService,
|
||||||
|
@ -612,7 +636,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
WebhookService,
|
UserWebhookService,
|
||||||
|
SystemWebhookService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -640,6 +665,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
|
|
||||||
AbuseUserReportEntityService,
|
AbuseUserReportEntityService,
|
||||||
AnnouncementEntityService,
|
AnnouncementEntityService,
|
||||||
|
AbuseReportNotificationRecipientEntityService,
|
||||||
AntennaEntityService,
|
AntennaEntityService,
|
||||||
AppEntityService,
|
AppEntityService,
|
||||||
AuthSessionEntityService,
|
AuthSessionEntityService,
|
||||||
|
@ -673,6 +699,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
RoleEntityService,
|
RoleEntityService,
|
||||||
ReversiGameEntityService,
|
ReversiGameEntityService,
|
||||||
MetaEntityService,
|
MetaEntityService,
|
||||||
|
SystemWebhookEntityService,
|
||||||
|
|
||||||
ApAudienceService,
|
ApAudienceService,
|
||||||
ApDbResolverService,
|
ApDbResolverService,
|
||||||
|
@ -696,6 +723,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
|
|
||||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||||
$LoggerService,
|
$LoggerService,
|
||||||
|
$AbuseReportService,
|
||||||
|
$AbuseReportNotificationService,
|
||||||
$AccountMoveService,
|
$AccountMoveService,
|
||||||
$AccountUpdateService,
|
$AccountUpdateService,
|
||||||
$AiService,
|
$AiService,
|
||||||
|
@ -748,7 +777,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$WebhookService,
|
$UserWebhookService,
|
||||||
|
$SystemWebhookService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
@ -776,6 +806,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
|
|
||||||
$AbuseUserReportEntityService,
|
$AbuseUserReportEntityService,
|
||||||
$AnnouncementEntityService,
|
$AnnouncementEntityService,
|
||||||
|
$AbuseReportNotificationRecipientEntityService,
|
||||||
$AntennaEntityService,
|
$AntennaEntityService,
|
||||||
$AppEntityService,
|
$AppEntityService,
|
||||||
$AuthSessionEntityService,
|
$AuthSessionEntityService,
|
||||||
|
@ -809,6 +840,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$RoleEntityService,
|
$RoleEntityService,
|
||||||
$ReversiGameEntityService,
|
$ReversiGameEntityService,
|
||||||
$MetaEntityService,
|
$MetaEntityService,
|
||||||
|
$SystemWebhookEntityService,
|
||||||
|
|
||||||
$ApAudienceService,
|
$ApAudienceService,
|
||||||
$ApDbResolverService,
|
$ApDbResolverService,
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
||||||
}, moderator?: MiUser): Promise<MiEmoji> {
|
}, moderator?: MiUser): Promise<MiEmoji> {
|
||||||
const emoji = await this.emojisRepository.insert({
|
const emoji = await this.emojisRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
isSensitive: data.isSensitive,
|
isSensitive: data.isSensitive,
|
||||||
localOnly: data.localOnly,
|
localOnly: data.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
if (data.host == null) {
|
if (data.host == null) {
|
||||||
this.localEmojisCache.refresh();
|
this.localEmojisCache.refresh();
|
||||||
|
@ -346,10 +346,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
|
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
|
||||||
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
|
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
|
||||||
const res = {} as any;
|
const res = {} as Record<string, string>;
|
||||||
for (let i = 0; i < emojiNames.length; i++) {
|
for (let i = 0; i < emojiNames.length; i++) {
|
||||||
if (emojis[i] != null) {
|
const resolvedEmoji = emojis[i];
|
||||||
res[emojiNames[i]] = emojis[i];
|
if (resolvedEmoji != null) {
|
||||||
|
res[emojiNames[i]] = resolvedEmoji;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -220,7 +220,7 @@ export class DriveService {
|
||||||
file.size = size;
|
file.size = size;
|
||||||
file.storedInternal = false;
|
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
|
} else { // use internal storage
|
||||||
const accessKey = randomUUID();
|
const accessKey = randomUUID();
|
||||||
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||||
|
@ -254,7 +254,7 @@ export class DriveService {
|
||||||
file.md5 = hash;
|
file.md5 = hash;
|
||||||
file.size = size;
|
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) {
|
if (user && !force) {
|
||||||
// Check if there is a file with the same hash
|
// 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,
|
md5: info.md5,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (much) {
|
if (matched) {
|
||||||
this.registerLogger.info(`file with same hash is found: ${much.id}`);
|
this.registerLogger.info(`file with same hash is found: ${matched.id}`);
|
||||||
if (sensitive && !much.isSensitive) {
|
if (sensitive && !matched.isSensitive) {
|
||||||
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
|
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
|
||||||
// Therefore, update the file to sensitive.
|
// Therefore, update the file to sensitive.
|
||||||
await this.driveFilesRepository.update({ id: much.id }, { isSensitive: true });
|
await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
|
||||||
much.isSensitive = true;
|
matched.isSensitive = true;
|
||||||
}
|
}
|
||||||
return much;
|
return matched;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,7 +615,7 @@ export class DriveService {
|
||||||
file.type = info.type.mime;
|
file.type = info.type.mime;
|
||||||
file.storedInternal = false;
|
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) {
|
} catch (err) {
|
||||||
// duplicate key error (when already registered)
|
// duplicate key error (when already registered)
|
||||||
if (isDuplicateKeyValueError(err)) {
|
if (isDuplicateKeyValueError(err)) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
|
@ -32,6 +33,7 @@ export class EmailService {
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('email');
|
this.logger = this.loggerService.getLogger('email');
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||||
let noteIds: string[];
|
|
||||||
let shouldFallbackToDb = false;
|
|
||||||
|
|
||||||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
||||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
||||||
|
|
||||||
|
@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
|
||||||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||||
|
|
||||||
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
||||||
const redisResultIds = Array.from(new Set(redisResult.flat(1)));
|
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
|
||||||
|
|
||||||
redisResultIds.sort(idCompare);
|
let noteIds = redisResultIds.slice(0, ps.limit);
|
||||||
noteIds = redisResultIds.slice(0, ps.limit);
|
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
||||||
|
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
|
||||||
|
|
||||||
if (!shouldFallbackToDb) {
|
if (!shouldFallbackToDb) {
|
||||||
let filter = ps.noteFilter ?? (_note => true);
|
let filter = ps.noteFilter ?? (_note => true);
|
||||||
|
|
|
@ -55,11 +55,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
||||||
const index = await this.instancesRepository.findOneBy({ host });
|
const index = await this.instancesRepository.findOneBy({ host });
|
||||||
|
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
const i = await this.instancesRepository.insert({
|
const i = await this.instancesRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
host,
|
host,
|
||||||
firstRetrievedAt: new Date(),
|
firstRetrievedAt: new Date(),
|
||||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
this.federatedInstanceCache.set(host, i);
|
this.federatedInstanceCache.set(host, i);
|
||||||
return i;
|
return i;
|
||||||
|
|
|
@ -154,7 +154,7 @@ export class FetchInstanceMetadataService {
|
||||||
throw new Error('No wellknown links');
|
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 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');
|
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||||
|
|
|
@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import type { MiSignin } from '@/models/Signin.js';
|
import type { MiSignin } from '@/models/Signin.js';
|
||||||
import type { MiPage } from '@/models/Page.js';
|
import type { MiPage } from '@/models/Page.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
@ -227,6 +228,9 @@ export interface InternalEventTypes {
|
||||||
webhookCreated: MiWebhook;
|
webhookCreated: MiWebhook;
|
||||||
webhookDeleted: MiWebhook;
|
webhookDeleted: MiWebhook;
|
||||||
webhookUpdated: MiWebhook;
|
webhookUpdated: MiWebhook;
|
||||||
|
systemWebhookCreated: MiSystemWebhook;
|
||||||
|
systemWebhookDeleted: MiSystemWebhook;
|
||||||
|
systemWebhookUpdated: MiSystemWebhook;
|
||||||
antennaCreated: MiAntenna;
|
antennaCreated: MiAntenna;
|
||||||
antennaDeleted: MiAntenna;
|
antennaDeleted: MiAntenna;
|
||||||
antennaUpdated: MiAntenna;
|
antennaUpdated: MiAntenna;
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class LoggerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
public getLogger(domain: string, color?: KEYWORD | undefined) {
|
||||||
return new Logger(domain, color, store);
|
return new Logger(domain, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.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 { HashtagService } from '@/core/HashtagService.js';
|
||||||
import { AntennaService } from '@/core/AntennaService.js';
|
import { AntennaService } from '@/core/AntennaService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
@ -205,7 +204,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private hashtagService: HashtagService,
|
private hashtagService: HashtagService,
|
||||||
private antennaService: AntennaService,
|
private antennaService: AntennaService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: UserWebhookService,
|
||||||
private featuredService: FeaturedService,
|
private featuredService: FeaturedService,
|
||||||
private remoteUserResolveService: RemoteUserResolveService,
|
private remoteUserResolveService: RemoteUserResolveService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
|
@ -606,7 +605,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'note', {
|
this.queueService.userWebhookDeliver(webhook, 'note', {
|
||||||
note: noteObj,
|
note: noteObj,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -633,7 +632,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'reply', {
|
this.queueService.userWebhookDeliver(webhook, 'reply', {
|
||||||
note: noteObj,
|
note: noteObj,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -656,7 +655,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'renote', {
|
this.queueService.userWebhookDeliver(webhook, 'renote', {
|
||||||
note: noteObj,
|
note: noteObj,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -788,7 +787,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'mention', {
|
this.queueService.userWebhookDeliver(webhook, 'mention', {
|
||||||
note: detailPackedNote,
|
note: detailPackedNote,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -839,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
const mentions = extractMentions(tokens);
|
const mentions = extractMentions(tokens);
|
||||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||||
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
||||||
))).filter(isNotNull);
|
))).filter(x => x != null);
|
||||||
|
|
||||||
// Drop duplicate users
|
// Drop duplicate users
|
||||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||||
|
|
|
@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
|
||||||
import { allSettled } from '@/misc/promise-tracker.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 { Provider } from '@nestjs/common';
|
||||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
|
||||||
|
|
||||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||||
|
@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||||
export type DbQueue = Bull.Queue;
|
export type DbQueue = Bull.Queue;
|
||||||
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||||
export type ObjectStorageQueue = Bull.Queue;
|
export type ObjectStorageQueue = Bull.Queue;
|
||||||
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||||
|
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||||
|
|
||||||
const $system: Provider = {
|
const $system: Provider = {
|
||||||
provide: 'queue:system',
|
provide: 'queue:system',
|
||||||
|
@ -63,9 +71,15 @@ const $objectStorage: Provider = {
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $webhookDeliver: Provider = {
|
const $userWebhookDeliver: Provider = {
|
||||||
provide: 'queue:webhookDeliver',
|
provide: 'queue:userWebhookDeliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
|
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],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
|
||||||
$db,
|
$db,
|
||||||
$relationship,
|
$relationship,
|
||||||
$objectStorage,
|
$objectStorage,
|
||||||
$webhookDeliver,
|
$userWebhookDeliver,
|
||||||
|
$systemWebhookDeliver,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
$system,
|
$system,
|
||||||
|
@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
|
||||||
$db,
|
$db,
|
||||||
$relationship,
|
$relationship,
|
||||||
$objectStorage,
|
$objectStorage,
|
||||||
$webhookDeliver,
|
$userWebhookDeliver,
|
||||||
|
$systemWebhookDeliver,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class QueueModule implements OnApplicationShutdown {
|
export class QueueModule implements OnApplicationShutdown {
|
||||||
|
@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||||
|
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
|
@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
this.dbQueue.close(),
|
this.dbQueue.close(),
|
||||||
this.relationshipQueue.close(),
|
this.relationshipQueue.close(),
|
||||||
this.objectStorageQueue.close(),
|
this.objectStorageQueue.close(),
|
||||||
this.webhookDeliverQueue.close(),
|
this.userWebhookDeliverQueue.close(),
|
||||||
|
this.systemWebhookDeliverQueue.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.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 { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.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 httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -31,7 +49,8 @@ export class QueueService {
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@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', {
|
this.systemQueue.add('tickCharts', {
|
||||||
}, {
|
}, {
|
||||||
|
@ -431,9 +450,13 @@ export class QueueService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UserWebhookDeliverJobData
|
||||||
|
* @see WebhookDeliverProcessorService
|
||||||
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||||
const data = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
webhookId: webhook.id,
|
webhookId: webhook.id,
|
||||||
|
@ -444,7 +467,33 @@ export class QueueService {
|
||||||
eventId: randomUUID(),
|
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,
|
attempts: 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
|
||||||
const FALLBACK = '\u2764';
|
const FALLBACK = '\u2764';
|
||||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||||
|
@ -117,11 +118,16 @@ export class ReactionService {
|
||||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if note is Renote
|
||||||
|
if (isRenote(note) && !isQuote(note)) {
|
||||||
|
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
|
||||||
|
}
|
||||||
|
|
||||||
let reaction = _reaction ?? FALLBACK;
|
let reaction = _reaction ?? FALLBACK;
|
||||||
|
|
||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '\u2764';
|
reaction = '\u2764';
|
||||||
} else if (_reaction) {
|
} else if (_reaction != null) {
|
||||||
const custom = reaction.match(isCustomEmojiRegexp);
|
const custom = reaction.match(isCustomEmojiRegexp);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||||
|
|
|
@ -53,11 +53,11 @@ export class RelayService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addRelay(inbox: string): Promise<MiRelay> {
|
public async addRelay(inbox: string): Promise<MiRelay> {
|
||||||
const relay = await this.relaysRepository.insert({
|
const relay = await this.relaysRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
inbox,
|
inbox,
|
||||||
status: 'requesting',
|
status: 'requesting',
|
||||||
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
const relayActor = await this.getRelayActor();
|
const relayActor = await this.getRelayActor();
|
||||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||||
|
|
|
@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
|
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
|
||||||
const game = await this.reversiGamesRepository.insert({
|
const game = await this.reversiGamesRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
user1Id: parentId,
|
user1Id: parentId,
|
||||||
user2Id: childId,
|
user2Id: childId,
|
||||||
|
@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
bw: 'random',
|
bw: 'random',
|
||||||
isLlotheo: false,
|
isLlotheo: false,
|
||||||
noIrregularRules: options.noIrregularRules,
|
noIrregularRules: options.noIrregularRules,
|
||||||
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
}, { relations: ['user1', 'user2'] });
|
||||||
where: { id: x.identifiers[0].id },
|
|
||||||
relations: ['user1', 'user2'],
|
|
||||||
}));
|
|
||||||
this.cacheGame(game);
|
this.cacheGame(game);
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game);
|
const packed = await this.reversiGameEntityService.packDetail(game);
|
||||||
|
|
|
@ -410,14 +410,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
|
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
const moderatorRoles = includeAdmins
|
||||||
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
? roles.filter(r => r.isModerator || r.isAdministrator)
|
||||||
roleId: In(moderatorRoles.map(r => r.id)),
|
: roles.filter(r => r.isModerator);
|
||||||
}) : [];
|
|
||||||
// TODO: isRootなアカウントも含める
|
// 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
|
@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),
|
id: this.idService.gen(now),
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
this.rolesRepository.update(roleId, {
|
this.rolesRepository.update(roleId, {
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
|
@ -558,7 +576,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
|
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const created = await this.rolesRepository.insert({
|
const created = await this.rolesRepository.insertOne({
|
||||||
id: this.idService.gen(date.getTime()),
|
id: this.idService.gen(date.getTime()),
|
||||||
updatedAt: date,
|
updatedAt: date,
|
||||||
lastUsedAt: date,
|
lastUsedAt: date,
|
||||||
|
@ -576,7 +594,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
canEditMembersByModerator: values.canEditMembersByModerator,
|
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||||
displayOrder: values.displayOrder,
|
displayOrder: values.displayOrder,
|
||||||
policies: values.policies,
|
policies: values.policies,
|
||||||
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('roleCreated', created);
|
this.globalEventService.publishInternalEvent('roleCreated', created);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
|
private logger: Logger;
|
||||||
|
private activeSystemWebhooksFetched = false;
|
||||||
|
private activeSystemWebhooks: MiSystemWebhook[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
@Inject(DI.systemWebhooksRepository)
|
||||||
|
private systemWebhooksRepository: SystemWebhooksRepository,
|
||||||
|
private idService: IdService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
this.logger = this.loggerService.getLogger('webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async fetchActiveSystemWebhooks() {
|
||||||
|
if (!this.activeSystemWebhooksFetched) {
|
||||||
|
this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
this.activeSystemWebhooksFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeSystemWebhooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook の一覧を取得する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async fetchSystemWebhooks(params?: {
|
||||||
|
ids?: MiSystemWebhook['id'][];
|
||||||
|
isActive?: MiSystemWebhook['isActive'];
|
||||||
|
on?: MiSystemWebhook['on'];
|
||||||
|
}): Promise<MiSystemWebhook[]> {
|
||||||
|
const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
|
||||||
|
if (params) {
|
||||||
|
if (params.ids && params.ids.length > 0) {
|
||||||
|
query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
|
||||||
|
}
|
||||||
|
if (params.isActive !== undefined) {
|
||||||
|
query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
|
||||||
|
}
|
||||||
|
if (params.on && params.on.length > 0) {
|
||||||
|
query.andWhere(':on <@ systemWebhook.on', { on: params.on });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook を作成する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async createSystemWebhook(
|
||||||
|
params: {
|
||||||
|
isActive: MiSystemWebhook['isActive'];
|
||||||
|
name: MiSystemWebhook['name'];
|
||||||
|
on: MiSystemWebhook['on'];
|
||||||
|
url: MiSystemWebhook['url'];
|
||||||
|
secret: MiSystemWebhook['secret'];
|
||||||
|
},
|
||||||
|
updater: MiUser,
|
||||||
|
): Promise<MiSystemWebhook> {
|
||||||
|
const id = this.idService.gen();
|
||||||
|
await this.systemWebhooksRepository.insert({
|
||||||
|
...params,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||||
|
this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'createSystemWebhook', {
|
||||||
|
systemWebhookId: webhook.id,
|
||||||
|
webhook: webhook,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
|
||||||
|
return webhook;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook を更新する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async updateSystemWebhook(
|
||||||
|
params: {
|
||||||
|
id: MiSystemWebhook['id'];
|
||||||
|
isActive: MiSystemWebhook['isActive'];
|
||||||
|
name: MiSystemWebhook['name'];
|
||||||
|
on: MiSystemWebhook['on'];
|
||||||
|
url: MiSystemWebhook['url'];
|
||||||
|
secret: MiSystemWebhook['secret'];
|
||||||
|
},
|
||||||
|
updater: MiUser,
|
||||||
|
): Promise<MiSystemWebhook> {
|
||||||
|
const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
|
||||||
|
await this.systemWebhooksRepository.update(beforeEntity.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isActive: params.isActive,
|
||||||
|
name: params.name,
|
||||||
|
on: params.on,
|
||||||
|
url: params.url,
|
||||||
|
secret: params.secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
|
||||||
|
this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'updateSystemWebhook', {
|
||||||
|
systemWebhookId: beforeEntity.id,
|
||||||
|
before: beforeEntity,
|
||||||
|
after: afterEntity,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
|
||||||
|
return afterEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook を削除する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
|
||||||
|
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||||
|
await this.systemWebhooksRepository.delete(id);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'deleteSystemWebhook', {
|
||||||
|
systemWebhookId: webhook.id,
|
||||||
|
webhook,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook をWebhook配送キューに追加する
|
||||||
|
* @see QueueService.systemWebhookDeliver
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||||
|
const webhookEntity = typeof webhook === 'string'
|
||||||
|
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||||
|
: webhook;
|
||||||
|
if (!webhookEntity || !webhookEntity.isActive) {
|
||||||
|
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!webhookEntity.on.includes(type)) {
|
||||||
|
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
if (obj.channel !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'systemWebhookCreated': {
|
||||||
|
if (body.isActive) {
|
||||||
|
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'systemWebhookUpdated': {
|
||||||
|
if (body.isActive) {
|
||||||
|
const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
|
||||||
|
if (i > -1) {
|
||||||
|
this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
|
||||||
|
} else {
|
||||||
|
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'systemWebhookDeleted': {
|
||||||
|
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.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 { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: UserWebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private loggerService: LoggerService,
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||||
user: packed,
|
user: packed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: UserWebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'follow', {
|
this.queueService.userWebhookDeliver(webhook, 'follow', {
|
||||||
user: packed,
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'followed', {
|
this.queueService.userWebhookDeliver(webhook, 'followed', {
|
||||||
user: packed,
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||||
user: packed,
|
user: packed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -517,7 +517,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const followRequest = await this.followRequestsRepository.insert({
|
const followRequest = await this.followRequestsRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followeeHost: followee.host,
|
followeeHost: followee.host,
|
||||||
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
||||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
||||||
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
|
|
||||||
// Publish receiveRequest event
|
// Publish receiveRequest event
|
||||||
if (this.userEntityService.isLocalUser(followee)) {
|
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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||||
user: packedFollowee,
|
user: packedFollowee,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||||
const currentCount = await this.userListMembershipsRepository.countBy({
|
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||||
throw new UserListService.TooManyUsersError();
|
throw new UserListService.TooManyUsersError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import type { WebhooksRepository } from '@/models/_.js';
|
||||||
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserWebhookService implements OnApplicationShutdown {
|
||||||
|
private activeWebhooksFetched = false;
|
||||||
|
private activeWebhooks: MiWebhook[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
@Inject(DI.webhooksRepository)
|
||||||
|
private webhooksRepository: WebhooksRepository,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getActiveWebhooks() {
|
||||||
|
if (!this.activeWebhooksFetched) {
|
||||||
|
this.activeWebhooks = await this.webhooksRepository.findBy({
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
this.activeWebhooksFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeWebhooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
if (obj.channel !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'webhookCreated': {
|
||||||
|
if (body.active) {
|
||||||
|
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'webhookUpdated': {
|
||||||
|
if (body.active) {
|
||||||
|
const i = this.activeWebhooks.findIndex(a => a.id === body.id);
|
||||||
|
if (i > -1) {
|
||||||
|
this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'webhookDeleted': {
|
||||||
|
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,97 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WebhookService implements OnApplicationShutdown {
|
|
||||||
private webhooksFetched = false;
|
|
||||||
private webhooks: MiWebhook[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.redisForSub)
|
|
||||||
private redisForSub: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.webhooksRepository)
|
|
||||||
private webhooksRepository: WebhooksRepository,
|
|
||||||
) {
|
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
|
||||||
this.redisForSub.on('message', this.onMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getActiveWebhooks() {
|
|
||||||
if (!this.webhooksFetched) {
|
|
||||||
this.webhooks = await this.webhooksRepository.findBy({
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
this.webhooksFetched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.webhooks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
|
||||||
const obj = JSON.parse(data);
|
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
|
||||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
|
||||||
switch (type) {
|
|
||||||
case 'webhookCreated':
|
|
||||||
if (body.active) {
|
|
||||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
|
||||||
...body,
|
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
|
||||||
user: null, // joinなカラムは通常取ってこないので
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'webhookUpdated':
|
|
||||||
if (body.active) {
|
|
||||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
|
||||||
if (i > -1) {
|
|
||||||
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
|
||||||
...body,
|
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
|
||||||
user: null, // joinなカラムは通常取ってこないので
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
|
||||||
...body,
|
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
|
||||||
user: null, // joinなカラムは通常取ってこないので
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'webhookDeleted':
|
|
||||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.redisForSub.off('message', this.onMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
|
||||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import { concat, unique } from '@/misc/prelude/array.js';
|
import { concat, unique } from '@/misc/prelude/array.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { getApIds } from './type.js';
|
import { getApIds } from './type.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import type { ApObject } from './type.js';
|
import type { ApObject } from './type.js';
|
||||||
|
@ -41,7 +40,7 @@ export class ApAudienceService {
|
||||||
const limit = promiseLimit<MiUser | null>(2);
|
const limit = promiseLimit<MiUser | null>(2);
|
||||||
const mentionedUsers = (await Promise.all(
|
const mentionedUsers = (await Promise.all(
|
||||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||||
)).filter(isNotNull);
|
)).filter(x => x != null);
|
||||||
|
|
||||||
if (toGroups.public.length > 0) {
|
if (toGroups.public.length > 0) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -27,8 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.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 { 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 { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
|
@ -57,9 +57,6 @@ export class ApInboxService {
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.abuseUserReportsRepository)
|
|
||||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
|
@ -68,6 +65,7 @@ export class ApInboxService {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private abuseReportService: AbuseReportService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private apAudienceService: ApAudienceService,
|
private apAudienceService: ApAudienceService,
|
||||||
private reactionService: ReactionService,
|
private reactionService: ReactionService,
|
||||||
|
@ -539,20 +537,19 @@ export class ApInboxService {
|
||||||
const userIds = uris
|
const userIds = uris
|
||||||
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||||
.map(uri => uri.split('/').at(-1))
|
.map(uri => uri.split('/').at(-1))
|
||||||
.filter(isNotNull);
|
.filter(x => x != null);
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.usersRepository.findBy({
|
||||||
id: In(userIds),
|
id: In(userIds),
|
||||||
});
|
});
|
||||||
if (users.length < 1) return 'skip';
|
if (users.length < 1) return 'skip';
|
||||||
|
|
||||||
await this.abuseUserReportsRepository.insert({
|
await this.abuseReportService.report([{
|
||||||
id: this.idService.gen(),
|
|
||||||
targetUserId: users[0].id,
|
targetUserId: users[0].id,
|
||||||
targetUserHost: users[0].host,
|
targetUserHost: users[0].host,
|
||||||
reporterId: actor.id,
|
reporterId: actor.id,
|
||||||
reporterHost: actor.host,
|
reporterHost: actor.host,
|
||||||
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
|
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
|
||||||
});
|
}]);
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
|
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { JsonLdService } from './JsonLdService.js';
|
import { JsonLdService } from './JsonLdService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
|
@ -317,7 +316,7 @@ export class ApRendererService {
|
||||||
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||||
return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
|
return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
|
||||||
};
|
};
|
||||||
|
|
||||||
let inReplyTo;
|
let inReplyTo;
|
||||||
|
@ -686,7 +685,7 @@ export class ApRendererService {
|
||||||
if (names.length === 0) return [];
|
if (names.length === 0) return [];
|
||||||
|
|
||||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
|
||||||
|
|
||||||
return emojis;
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
|
||||||
import type { MiUser } from '@/models/_.js';
|
import type { MiUser } from '@/models/_.js';
|
||||||
import { toArray, unique } from '@/misc/prelude/array.js';
|
import { toArray, unique } from '@/misc/prelude/array.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { isMention } from '../type.js';
|
import { isMention } from '../type.js';
|
||||||
import { Resolver } from '../ApResolverService.js';
|
import { Resolver } from '../ApResolverService.js';
|
||||||
import { ApPersonService } from './ApPersonService.js';
|
import { ApPersonService } from './ApPersonService.js';
|
||||||
|
@ -28,7 +27,7 @@ export class ApMentionService {
|
||||||
const limit = promiseLimit<MiUser | null>(2);
|
const limit = promiseLimit<MiUser | null>(2);
|
||||||
const mentionedUsers = (await Promise.all(
|
const mentionedUsers = (await Promise.all(
|
||||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||||
)).filter(isNotNull);
|
)).filter(x => x != null);
|
||||||
|
|
||||||
return mentionedUsers;
|
return mentionedUsers;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
|
@ -253,7 +252,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
|
const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
|
||||||
const results = await Promise.all(uris.map(tryResolveNote));
|
const results = await Promise.all(uris.map(tryResolveNote));
|
||||||
|
|
||||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||||
|
@ -407,7 +406,7 @@ export class ApNoteService {
|
||||||
|
|
||||||
this.logger.info(`register emoji host=${host}, name=${name}`);
|
this.logger.info(`register emoji host=${host}, name=${name}`);
|
||||||
|
|
||||||
return await this.emojisRepository.insert({
|
return await this.emojisRepository.insertOne({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
|
@ -416,7 +415,7 @@ export class ApNoteService {
|
||||||
publicUrl: tag.icon.url,
|
publicUrl: tag.icon.url,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
aliases: [],
|
aliases: [],
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -637,7 +636,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
// とりあえずidを別の時間で生成して順番を維持
|
// とりあえずidを別の時間で生成して順番を維持
|
||||||
let td = 0;
|
let td = 0;
|
||||||
for (const note of featuredNotes.filter(isNotNull)) {
|
for (const note of featuredNotes.filter(x => x != null)) {
|
||||||
td -= 1000;
|
td -= 1000;
|
||||||
transactionalEntityManager.insert(MiUserNotePining, {
|
transactionalEntityManager.insert(MiUserNotePining, {
|
||||||
id: this.idService.gen(Date.now() + td),
|
id: this.idService.gen(Date.now() + td),
|
||||||
|
|
|
@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { isQuestion } from '../type.js';
|
import { isQuestion } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
|
@ -52,7 +51,7 @@ export class ApQuestionService {
|
||||||
|
|
||||||
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
||||||
?.map((x) => x.name)
|
?.map((x) => x.name)
|
||||||
.filter(isNotNull)
|
.filter(x => x != null)
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
|
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { toArray } from '@/misc/prelude/array.js';
|
import { toArray } from '@/misc/prelude/array.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { isHashtag } from '../type.js';
|
import { isHashtag } from '../type.js';
|
||||||
import type { IObject, IApHashtag } from '../type.js';
|
import type { IObject, IApHashtag } from '../type.js';
|
||||||
|
|
||||||
|
@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
|
||||||
return hashtags.map(tag => {
|
return hashtags.map(tag => {
|
||||||
const m = tag.name.match(/^#(.+)/);
|
const m = tag.name.match(/^#(.+)/);
|
||||||
return m ? m[1] : null;
|
return m ? m[1] : null;
|
||||||
}).filter(isNotNull);
|
}).filter(x => x != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
|
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
|
||||||
|
|
|
@ -14,6 +14,6 @@ export class ChartLoggerService {
|
||||||
constructor(
|
constructor(
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
|
this.logger = this.loggerService.getLogger('chart', 'white');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
|
|
||||||
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
||||||
.select('instance.host')
|
.select('instance.host')
|
||||||
.where('instance.isSuspended = true');
|
.where('instance.suspensionState != \'none\'');
|
||||||
|
|
||||||
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
|
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
|
||||||
.select('f.followerHost')
|
.select('f.followerHost')
|
||||||
|
@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.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')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
|
@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.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')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
|
|
|
@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm';
|
||||||
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
|
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.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 COLUMN_PREFIX = '___' as const;
|
||||||
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
|
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
|
||||||
|
@ -145,10 +146,10 @@ export default abstract class Chart<T extends Schema> {
|
||||||
group: string | null;
|
group: string | null;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
// ↓にしたいけどfindOneとかで型エラーになる
|
// ↓にしたいけどfindOneとかで型エラーになる
|
||||||
//private repositoryForHour: Repository<RawRecord<T>>;
|
//private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
|
||||||
//private repositoryForDay: Repository<RawRecord<T>>;
|
//private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
|
||||||
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
|
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; }>;
|
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
||||||
|
@ -211,6 +212,10 @@ export default abstract class Chart<T extends Schema> {
|
||||||
} {
|
} {
|
||||||
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
|
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
|
||||||
name:
|
name:
|
||||||
|
span === 'hour' ? `ChartX${name}` :
|
||||||
|
span === 'day' ? `ChartDayX${name}` :
|
||||||
|
new Error('not happen') as never,
|
||||||
|
tableName:
|
||||||
span === 'hour' ? `__chart__${camelToSnake(name)}` :
|
span === 'hour' ? `__chart__${camelToSnake(name)}` :
|
||||||
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
|
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
|
||||||
new Error('not happen') as never,
|
new Error('not happen') as never,
|
||||||
|
@ -271,8 +276,8 @@ export default abstract class Chart<T extends Schema> {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
||||||
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
|
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);
|
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -387,11 +392,11 @@ export default abstract class Chart<T extends Schema> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新規ログ挿入
|
// 新規ログ挿入
|
||||||
log = await repository.insert({
|
log = await repository.insertOne({
|
||||||
date: date,
|
date: date,
|
||||||
...(group ? { group: group } : {}),
|
...(group ? { group: group } : {}),
|
||||||
...columns,
|
...columns,
|
||||||
}).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord<T>;
|
}) as RawRecord<T>;
|
||||||
|
|
||||||
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
|
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
@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<string, Packed<'UserLite'>>,
|
||||||
|
webhooks: Map<string, Packed<'SystemWebhook'>>,
|
||||||
|
},
|
||||||
|
): Promise<Packed<'AbuseReportNotificationRecipient'>> {
|
||||||
|
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<Packed<'AbuseReportNotificationRecipient'>[]> {
|
||||||
|
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(x => x != null);
|
||||||
|
const users: Map<string, Packed<'UserLite'>> = (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(x => x != null);
|
||||||
|
const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -26,6 +27,11 @@ export class AbuseUserReportEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiAbuseUserReport['id'] | MiAbuseUserReport,
|
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 });
|
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
@ -37,13 +43,13 @@ export class AbuseUserReportEntityService {
|
||||||
reporterId: report.reporterId,
|
reporterId: report.reporterId,
|
||||||
targetUserId: report.targetUserId,
|
targetUserId: report.targetUserId,
|
||||||
assigneeId: report.assigneeId,
|
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',
|
schema: 'UserDetailedNotMe',
|
||||||
}),
|
}),
|
||||||
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
|
targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
|
||||||
schema: 'UserDetailedNotMe',
|
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',
|
schema: 'UserDetailedNotMe',
|
||||||
}) : null,
|
}) : null,
|
||||||
forwarded: report.forwarded,
|
forwarded: report.forwarded,
|
||||||
|
@ -51,9 +57,24 @@ export class AbuseUserReportEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
reports: any[],
|
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(x => x != null);
|
||||||
|
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 });
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ export class AntennaEntityService {
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
isActive: antenna.isActive,
|
isActive: antenna.isActive,
|
||||||
hasUnreadNote: false, // TODO
|
hasUnreadNote: false, // TODO
|
||||||
|
notify: false, // 後方互換性のため
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,9 @@ export class BlockingEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiBlocking['id'] | MiBlocking,
|
src: MiBlocking['id'] | MiBlocking,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hint?: {
|
||||||
|
blockee?: Packed<'UserDetailedNotMe'>,
|
||||||
|
},
|
||||||
): Promise<Packed<'Blocking'>> {
|
): Promise<Packed<'Blocking'>> {
|
||||||
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
|
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
@ -36,17 +39,20 @@ export class BlockingEntityService {
|
||||||
id: blocking.id,
|
id: blocking.id,
|
||||||
createdAt: this.idService.parse(blocking.id).date.toISOString(),
|
createdAt: this.idService.parse(blocking.id).date.toISOString(),
|
||||||
blockeeId: blocking.blockeeId,
|
blockeeId: blocking.blockeeId,
|
||||||
blockee: this.userEntityService.pack(blocking.blockeeId, me, {
|
blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
|
||||||
schema: 'UserDetailedNotMe',
|
schema: 'UserDetailedNotMe',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
blockings: any[],
|
blockings: MiBlocking[],
|
||||||
me: { id: MiUser['id'] },
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,9 @@ export class ClipEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiClip['id'] | MiClip,
|
src: MiClip['id'] | MiClip,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hint?: {
|
||||||
|
packedUser?: Packed<'UserLite'>
|
||||||
|
},
|
||||||
): Promise<Packed<'Clip'>> {
|
): Promise<Packed<'Clip'>> {
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
|
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
|
||||||
|
@ -44,22 +47,25 @@ export class ClipEntityService {
|
||||||
createdAt: this.idService.parse(clip.id).date.toISOString(),
|
createdAt: this.idService.parse(clip.id).date.toISOString(),
|
||||||
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
|
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
|
||||||
userId: clip.userId,
|
userId: clip.userId,
|
||||||
user: this.userEntityService.pack(clip.user ?? clip.userId),
|
user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
|
||||||
name: clip.name,
|
name: clip.name,
|
||||||
description: clip.description,
|
description: clip.description,
|
||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||||
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
|
notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
clips: MiClip[],
|
clips: MiClip[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
import { deepClone } from '@/misc/clone.js';
|
import { deepClone } from '@/misc/clone.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { UtilityService } from '../UtilityService.js';
|
import { UtilityService } from '../UtilityService.js';
|
||||||
import { VideoProcessingService } from '../VideoProcessingService.js';
|
import { VideoProcessingService } from '../VideoProcessingService.js';
|
||||||
|
@ -222,6 +221,9 @@ export class DriveFileEntityService {
|
||||||
public async packNullable(
|
public async packNullable(
|
||||||
src: MiDriveFile['id'] | MiDriveFile,
|
src: MiDriveFile['id'] | MiDriveFile,
|
||||||
options?: PackOptions,
|
options?: PackOptions,
|
||||||
|
hint?: {
|
||||||
|
packedUser?: Packed<'UserLite'>
|
||||||
|
},
|
||||||
): Promise<Packed<'DriveFile'> | null> {
|
): Promise<Packed<'DriveFile'> | null> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
detail: false,
|
detail: false,
|
||||||
|
@ -249,7 +251,7 @@ export class DriveFileEntityService {
|
||||||
detail: true,
|
detail: true,
|
||||||
}) : null,
|
}) : null,
|
||||||
userId: file.userId,
|
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,8 +260,11 @@ export class DriveFileEntityService {
|
||||||
files: MiDriveFile[],
|
files: MiDriveFile[],
|
||||||
options?: PackOptions,
|
options?: PackOptions,
|
||||||
): Promise<Packed<'DriveFile'>[]> {
|
): Promise<Packed<'DriveFile'>[]> {
|
||||||
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
|
const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
|
||||||
return items.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(x => x != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -284,6 +289,6 @@ export class DriveFileEntityService {
|
||||||
): Promise<Packed<'DriveFile'>[]> {
|
): Promise<Packed<'DriveFile'>[]> {
|
||||||
if (fileIds.length === 0) return [];
|
if (fileIds.length === 0) return [];
|
||||||
const filesMap = await this.packManyByIdsMap(fileIds, options);
|
const filesMap = await this.packManyByIdsMap(fileIds, options);
|
||||||
return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
|
return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,9 @@ export class FlashEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiFlash['id'] | MiFlash,
|
src: MiFlash['id'] | MiFlash,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hint?: {
|
||||||
|
packedUser?: Packed<'UserLite'>
|
||||||
|
},
|
||||||
): Promise<Packed<'Flash'>> {
|
): Promise<Packed<'Flash'>> {
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
|
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(),
|
createdAt: this.idService.parse(flash.id).date.toISOString(),
|
||||||
updatedAt: flash.updatedAt.toISOString(),
|
updatedAt: flash.updatedAt.toISOString(),
|
||||||
userId: flash.userId,
|
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,
|
title: flash.title,
|
||||||
summary: flash.summary,
|
summary: flash.summary,
|
||||||
script: flash.script,
|
script: flash.script,
|
||||||
|
@ -52,11 +55,14 @@ export class FlashEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
flashs: MiFlash[],
|
flashes: MiFlash[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiFollowRequest } from '@/models/FollowRequest.js';
|
import type { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -26,14 +27,36 @@ export class FollowRequestEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiFollowRequest['id'] | MiFollowRequest,
|
src: MiFollowRequest['id'] | MiFollowRequest,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
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 });
|
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
follower: await this.userEntityService.pack(request.followerId, me),
|
follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
|
||||||
followee: await this.userEntityService.pack(request.followeeId, 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 });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,10 @@ export class FollowingEntityService {
|
||||||
populateFollowee?: boolean;
|
populateFollowee?: boolean;
|
||||||
populateFollower?: boolean;
|
populateFollower?: boolean;
|
||||||
},
|
},
|
||||||
|
hint?: {
|
||||||
|
packedFollowee?: Packed<'UserDetailedNotMe'>,
|
||||||
|
packedFollower?: Packed<'UserDetailedNotMe'>,
|
||||||
|
},
|
||||||
): Promise<Packed<'Following'>> {
|
): Promise<Packed<'Following'>> {
|
||||||
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
|
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(),
|
createdAt: this.idService.parse(following.id).date.toISOString(),
|
||||||
followeeId: following.followeeId,
|
followeeId: following.followeeId,
|
||||||
followerId: following.followerId,
|
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',
|
schema: 'UserDetailedNotMe',
|
||||||
}) : undefined,
|
}) : 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',
|
schema: 'UserDetailedNotMe',
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
followings: any[],
|
followings: MiFollowing[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
opts?: {
|
opts?: {
|
||||||
populateFollowee?: boolean;
|
populateFollowee?: boolean;
|
||||||
populateFollower?: 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 });
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,9 @@ export class GalleryPostEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiGalleryPost['id'] | MiGalleryPost,
|
src: MiGalleryPost['id'] | MiGalleryPost,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hint?: {
|
||||||
|
packedUser?: Packed<'UserLite'>
|
||||||
|
},
|
||||||
): Promise<Packed<'GalleryPost'>> {
|
): Promise<Packed<'GalleryPost'>> {
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
|
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(),
|
createdAt: this.idService.parse(post.id).date.toISOString(),
|
||||||
updatedAt: post.updatedAt.toISOString(),
|
updatedAt: post.updatedAt.toISOString(),
|
||||||
userId: post.userId,
|
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,
|
title: post.title,
|
||||||
description: post.description,
|
description: post.description,
|
||||||
fileIds: post.fileIds,
|
fileIds: post.fileIds,
|
||||||
|
@ -58,11 +61,14 @@ export class GalleryPostEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
posts: MiGalleryPost[],
|
posts: MiGalleryPost[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,10 @@ export class InviteCodeEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
|
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hints?: {
|
||||||
|
packedCreatedBy?: Packed<'UserLite'>,
|
||||||
|
packedUsedBy?: Packed<'UserLite'>,
|
||||||
|
},
|
||||||
): Promise<Packed<'InviteCode'>> {
|
): Promise<Packed<'InviteCode'>> {
|
||||||
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
|
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
|
@ -42,18 +46,28 @@ export class InviteCodeEntityService {
|
||||||
code: target.code,
|
code: target.code,
|
||||||
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
|
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
|
||||||
createdAt: this.idService.parse(target.id).date.toISOString(),
|
createdAt: this.idService.parse(target.id).date.toISOString(),
|
||||||
createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
|
createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
|
||||||
usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
|
usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
|
||||||
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
|
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
|
||||||
used: !!target.usedAt,
|
used: !!target.usedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
targets: any[],
|
tickets: MiRegistrationTicket[],
|
||||||
me: { id: MiUser['id'] },
|
me: { id: MiUser['id'] },
|
||||||
) {
|
) {
|
||||||
return Promise.all(targets.map(x => this.pack(x, me)));
|
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
|
||||||
|
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
|
||||||
|
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 });
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ export class MetaEntityService {
|
||||||
feedbackUrl: instance.feedbackUrl,
|
feedbackUrl: instance.feedbackUrl,
|
||||||
impressumUrl: instance.impressumUrl,
|
impressumUrl: instance.impressumUrl,
|
||||||
privacyPolicyUrl: instance.privacyPolicyUrl,
|
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||||
|
inquiryUrl: instance.inquiryUrl,
|
||||||
disableRegistration: instance.disableRegistration,
|
disableRegistration: instance.disableRegistration,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
|
|
|
@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { ModerationLogsRepository } from '@/models/_.js';
|
import type { ModerationLogsRepository } from '@/models/_.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { } from '@/models/Blocking.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 { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -27,6 +28,9 @@ export class ModerationLogEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiModerationLog['id'] | MiModerationLog,
|
src: MiModerationLog['id'] | MiModerationLog,
|
||||||
|
hint?: {
|
||||||
|
packedUser?: Packed<'UserDetailedNotMe'>,
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
|
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
@ -36,17 +40,20 @@ export class ModerationLogEntityService {
|
||||||
type: log.type,
|
type: log.type,
|
||||||
info: log.info,
|
info: log.info,
|
||||||
userId: log.userId,
|
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',
|
schema: 'UserDetailedNotMe',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
reports: any[],
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,9 @@ export class MutingEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiMuting['id'] | MiMuting,
|
src: MiMuting['id'] | MiMuting,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hints?: {
|
||||||
|
packedMutee?: Packed<'UserDetailedNotMe'>,
|
||||||
|
},
|
||||||
): Promise<Packed<'Muting'>> {
|
): Promise<Packed<'Muting'>> {
|
||||||
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
|
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(),
|
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
||||||
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
|
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
|
||||||
muteeId: muting.muteeId,
|
muteeId: muting.muteeId,
|
||||||
mutee: this.userEntityService.pack(muting.muteeId, me, {
|
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
|
||||||
schema: 'UserDetailedNotMe',
|
schema: 'UserDetailedNotMe',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
mutings: any[],
|
mutings: MiMuting[],
|
||||||
me: { id: MiUser['id'] },
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { DebounceLoader } from '@/misc/loader.js';
|
import { DebounceLoader } from '@/misc/loader.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -276,7 +275,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
packedFiles.set(k, v);
|
packedFiles.set(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
|
return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -290,6 +289,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
_hint_?: {
|
_hint_?: {
|
||||||
myReactions: Map<MiNote['id'], string | null>;
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||||
|
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
): Promise<Packed<'Note'>> {
|
): Promise<Packed<'Note'>> {
|
||||||
|
@ -319,12 +319,13 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||||
const packedFiles = options?._hint_?.packedFiles;
|
const packedFiles = options?._hint_?.packedFiles;
|
||||||
|
const packedUsers = options?._hint_?.packedUsers;
|
||||||
|
|
||||||
const packed: Packed<'Note'> = await awaitAll({
|
const packed: Packed<'Note'> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
createdAt: this.idService.parse(note.id).date.toISOString(),
|
createdAt: this.idService.parse(note.id).date.toISOString(),
|
||||||
userId: note.userId,
|
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,
|
text: text,
|
||||||
cw: note.cw,
|
cw: note.cw,
|
||||||
visibility: note.visibility,
|
visibility: note.visibility,
|
||||||
|
@ -447,14 +448,22 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
||||||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
|
||||||
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
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(x => x != null),
|
||||||
|
...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
|
||||||
|
];
|
||||||
|
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, {
|
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||||
...options,
|
...options,
|
||||||
_hint_: {
|
_hint_: {
|
||||||
myReactions: myReactionsMap,
|
myReactions: myReactionsMap,
|
||||||
packedFiles,
|
packedFiles,
|
||||||
|
packedUsers,
|
||||||
},
|
},
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
|
||||||
options?: {
|
options?: {
|
||||||
withNote: boolean;
|
withNote: boolean;
|
||||||
},
|
},
|
||||||
|
hints?: {
|
||||||
|
packedUser?: Packed<'UserLite'>
|
||||||
|
},
|
||||||
): Promise<Packed<'NoteReaction'>> {
|
): Promise<Packed<'NoteReaction'>> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
withNote: false,
|
withNote: false,
|
||||||
|
@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
|
||||||
return {
|
return {
|
||||||
id: reaction.id,
|
id: reaction.id,
|
||||||
createdAt: this.idService.parse(reaction.id).date.toISOString(),
|
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),
|
type: this.reactionService.convertLegacyReaction(reaction.reaction),
|
||||||
...(opts.withNote ? {
|
...(opts.withNote ? {
|
||||||
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
|
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
|
||||||
|
@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
withNote: false,
|
withNote: false,
|
||||||
}, options);
|
}, options);
|
||||||
|
const _users = reactions.map(({ user, userId }) => user ?? userId);
|
||||||
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
|
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RoleEntityService } from './RoleEntityService.js';
|
import { RoleEntityService } from './RoleEntityService.js';
|
||||||
|
@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
user,
|
user,
|
||||||
reaction: reaction.reaction,
|
reaction: reaction.reaction,
|
||||||
};
|
};
|
||||||
}))).filter(r => isNotNull(r.user));
|
}))).filter(r => r.user != null);
|
||||||
// if all users have been deleted, don't show this notification
|
// if all users have been deleted, don't show this notification
|
||||||
if (reactions.length === 0) {
|
if (reactions.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.userEntityService.pack(userId, { id: meId });
|
return this.userEntityService.pack(userId, { id: meId });
|
||||||
}))).filter(isNotNull);
|
}))).filter(x => x != null);
|
||||||
// if all users have been deleted, don't show this notification
|
// if all users have been deleted, don't show this notification
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
|
|
||||||
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
|
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
|
||||||
|
|
||||||
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
|
||||||
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||||
where: { id: In(noteIds) },
|
where: { id: In(noteIds) },
|
||||||
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
||||||
|
@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await Promise.all(packPromises)).filter(isNotNull);
|
return (await Promise.all(packPromises)).filter(x => x != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
|
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
|
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
|
||||||
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
|
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
|
||||||
where: { id: In(notifierIds) },
|
where: { id: In(notifierIds) },
|
||||||
}) : [];
|
}) : [];
|
||||||
|
@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
|
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
|
||||||
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
|
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
|
||||||
return isValid ? notification : null;
|
return isValid ? notification : null;
|
||||||
}))) as [T | null] ).filter(isNotNull);
|
}))) as [T | null] ).filter(x => x != null);
|
||||||
|
|
||||||
return filteredNotifications;
|
return filteredNotifications;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
|
|
||||||
|
@ -40,6 +39,9 @@ export class PageEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiPage['id'] | MiPage,
|
src: MiPage['id'] | MiPage,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hint?: {
|
||||||
|
packedUser?: Packed<'UserLite'>
|
||||||
|
},
|
||||||
): Promise<Packed<'Page'>> {
|
): Promise<Packed<'Page'>> {
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
|
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
|
||||||
|
@ -91,7 +93,7 @@ export class PageEntityService {
|
||||||
createdAt: this.idService.parse(page.id).date.toISOString(),
|
createdAt: this.idService.parse(page.id).date.toISOString(),
|
||||||
updatedAt: page.updatedAt.toISOString(),
|
updatedAt: page.updatedAt.toISOString(),
|
||||||
userId: page.userId,
|
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,
|
content: page.content,
|
||||||
variables: page.variables,
|
variables: page.variables,
|
||||||
title: page.title,
|
title: page.title,
|
||||||
|
@ -103,18 +105,21 @@ export class PageEntityService {
|
||||||
script: page.script,
|
script: page.script,
|
||||||
eyeCatchingImageId: page.eyeCatchingImageId,
|
eyeCatchingImageId: page.eyeCatchingImageId,
|
||||||
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
||||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
|
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
|
||||||
likedCount: page.likedCount,
|
likedCount: page.likedCount,
|
||||||
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
|
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
pages: MiPage[],
|
pages: MiPage[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiRenoteMuting['id'] | MiRenoteMuting,
|
src: MiRenoteMuting['id'] | MiRenoteMuting,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
|
hints?: {
|
||||||
|
packedMutee?: Packed<'UserDetailedNotMe'>
|
||||||
|
},
|
||||||
): Promise<Packed<'RenoteMuting'>> {
|
): Promise<Packed<'RenoteMuting'>> {
|
||||||
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
|
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
|
||||||
id: muting.id,
|
id: muting.id,
|
||||||
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
||||||
muteeId: muting.muteeId,
|
muteeId: muting.muteeId,
|
||||||
mutee: this.userEntityService.pack(muting.muteeId, me, {
|
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
|
||||||
schema: 'UserDetailedNotMe',
|
schema: 'UserDetailedNotMe',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public async packMany(
|
||||||
mutings: any[],
|
mutings: MiRenoteMuting[],
|
||||||
me: { id: MiUser['id'] },
|
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) })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,15 @@ export class ReversiGameEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async packDetail(
|
public async packDetail(
|
||||||
src: MiReversiGame['id'] | MiReversiGame,
|
src: MiReversiGame['id'] | MiReversiGame,
|
||||||
|
hint?: {
|
||||||
|
packedUser1?: Packed<'UserLite'>,
|
||||||
|
packedUser2?: Packed<'UserLite'>,
|
||||||
|
},
|
||||||
): Promise<Packed<'ReversiGameDetailed'>> {
|
): Promise<Packed<'ReversiGameDetailed'>> {
|
||||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const users = await Promise.all([
|
const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
|
||||||
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
|
||||||
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
|
@ -49,10 +51,10 @@ export class ReversiGameEntityService {
|
||||||
user2Ready: game.user2Ready,
|
user2Ready: game.user2Ready,
|
||||||
user1Id: game.user1Id,
|
user1Id: game.user1Id,
|
||||||
user2Id: game.user2Id,
|
user2Id: game.user2Id,
|
||||||
user1: users[0],
|
user1,
|
||||||
user2: users[1],
|
user2,
|
||||||
winnerId: game.winnerId,
|
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,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
timeoutUserId: game.timeoutUserId,
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
|
@ -68,22 +70,35 @@ export class ReversiGameEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packDetailMany(
|
public async packDetailMany(
|
||||||
xs: MiReversiGame[],
|
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
|
@bindThis
|
||||||
public async packLite(
|
public async packLite(
|
||||||
src: MiReversiGame['id'] | MiReversiGame,
|
src: MiReversiGame['id'] | MiReversiGame,
|
||||||
|
hint?: {
|
||||||
|
packedUser1?: Packed<'UserLite'>,
|
||||||
|
packedUser2?: Packed<'UserLite'>,
|
||||||
|
},
|
||||||
): Promise<Packed<'ReversiGameLite'>> {
|
): Promise<Packed<'ReversiGameLite'>> {
|
||||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const users = await Promise.all([
|
const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
|
||||||
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
|
||||||
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
|
@ -94,10 +109,10 @@ export class ReversiGameEntityService {
|
||||||
isEnded: game.isEnded,
|
isEnded: game.isEnded,
|
||||||
user1Id: game.user1Id,
|
user1Id: game.user1Id,
|
||||||
user2Id: game.user2Id,
|
user2Id: game.user2Id,
|
||||||
user1: users[0],
|
user1,
|
||||||
user2: users[1],
|
user2,
|
||||||
winnerId: game.winnerId,
|
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,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
timeoutUserId: game.timeoutUserId,
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
|
@ -111,10 +126,21 @@ export class ReversiGameEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packLiteMany(
|
public async packLiteMany(
|
||||||
xs: MiReversiGame[],
|
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),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<string, MiSystemWebhook>
|
||||||
|
},
|
||||||
|
): Promise<Packed<'SystemWebhook'>> {
|
||||||
|
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<Packed<'SystemWebhook'>[]> {
|
||||||
|
if (src.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhooks = Array.of<MiSystemWebhook>();
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
|
@ -514,7 +513,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
||||||
alsoKnownAs: user.alsoKnownAs
|
alsoKnownAs: user.alsoKnownAs
|
||||||
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
|
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
|
||||||
.then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
|
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
|
||||||
: null,
|
: null,
|
||||||
createdAt: this.idService.parse(user.id).date.toISOString(),
|
createdAt: this.idService.parse(user.id).date.toISOString(),
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
|
|
|
@ -50,11 +50,14 @@ export class UserListEntityService {
|
||||||
public async packMembershipsMany(
|
public async packMembershipsMany(
|
||||||
memberships: MiUserListMembership[],
|
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 => ({
|
return Promise.all(memberships.map(async x => ({
|
||||||
id: x.id,
|
id: x.id,
|
||||||
createdAt: this.idService.parse(x.id).date.toISOString(),
|
createdAt: this.idService.parse(x.id).date.toISOString(),
|
||||||
userId: x.userId,
|
userId: x.userId,
|
||||||
user: await this.userEntityService.pack(x.userId),
|
user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
|
||||||
withReplies: x.withReplies,
|
withReplies: x.withReplies,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ export const DI = {
|
||||||
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
||||||
hashtagsRepository: Symbol('hashtagsRepository'),
|
hashtagsRepository: Symbol('hashtagsRepository'),
|
||||||
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
|
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
|
||||||
|
abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
|
||||||
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
|
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
|
||||||
authSessionsRepository: Symbol('authSessionsRepository'),
|
authSessionsRepository: Symbol('authSessionsRepository'),
|
||||||
accessTokensRepository: Symbol('accessTokensRepository'),
|
accessTokensRepository: Symbol('accessTokensRepository'),
|
||||||
|
@ -70,6 +71,7 @@ export const DI = {
|
||||||
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
||||||
registryItemsRepository: Symbol('registryItemsRepository'),
|
registryItemsRepository: Symbol('registryItemsRepository'),
|
||||||
webhooksRepository: Symbol('webhooksRepository'),
|
webhooksRepository: Symbol('webhooksRepository'),
|
||||||
|
systemWebhooksRepository: Symbol('systemWebhooksRepository'),
|
||||||
adsRepository: Symbol('adsRepository'),
|
adsRepository: Symbol('adsRepository'),
|
||||||
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
|
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
|
||||||
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
|
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
|
||||||
|
|
|
@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
private context: Context;
|
private context: Context;
|
||||||
private parentLogger: Logger | null = null;
|
private parentLogger: Logger | null = null;
|
||||||
private store: boolean;
|
|
||||||
|
|
||||||
constructor(context: string, color?: KEYWORD, store = true) {
|
constructor(context: string, color?: KEYWORD) {
|
||||||
this.context = {
|
this.context = {
|
||||||
name: context,
|
name: context,
|
||||||
color: color,
|
color: color,
|
||||||
};
|
};
|
||||||
this.store = store;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
|
public createSubLogger(context: string, color?: KEYWORD): Logger {
|
||||||
const logger = new Logger(context, color, store);
|
const logger = new Logger(context, color);
|
||||||
logger.parentLogger = this;
|
logger.parentLogger = this;
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = [], store = true): void {
|
private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = []): void {
|
||||||
if (envOption.quiet) return;
|
if (envOption.quiet) return;
|
||||||
if (!this.store) store = false;
|
|
||||||
if (level === 'debug') store = false;
|
|
||||||
|
|
||||||
if (this.parentLogger) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
|
|
||||||
return input != null;
|
|
||||||
}
|
|
|
@ -4,12 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
packedUserLiteSchema,
|
|
||||||
packedUserDetailedNotMeOnlySchema,
|
|
||||||
packedMeDetailedOnlySchema,
|
packedMeDetailedOnlySchema,
|
||||||
packedUserDetailedNotMeSchema,
|
|
||||||
packedMeDetailedSchema,
|
packedMeDetailedSchema,
|
||||||
|
packedUserDetailedNotMeOnlySchema,
|
||||||
|
packedUserDetailedNotMeSchema,
|
||||||
packedUserDetailedSchema,
|
packedUserDetailedSchema,
|
||||||
|
packedUserLiteSchema,
|
||||||
packedUserSchema,
|
packedUserSchema,
|
||||||
} from '@/models/json-schema/user.js';
|
} from '@/models/json-schema/user.js';
|
||||||
import { packedNoteSchema } from '@/models/json-schema/note.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 { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||||
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
|
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
|
||||||
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.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 { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||||
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
||||||
import { packedAntennaSchema } from '@/models/json-schema/antenna.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 { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||||
import {
|
import {
|
||||||
packedRoleLiteSchema,
|
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||||
packedRoleSchema,
|
|
||||||
packedRolePoliciesSchema,
|
|
||||||
packedRoleCondFormulaLogicsSchema,
|
packedRoleCondFormulaLogicsSchema,
|
||||||
packedRoleCondFormulaValueNot,
|
|
||||||
packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
|
||||||
packedRoleCondFormulaValueAssignedRoleSchema,
|
packedRoleCondFormulaValueAssignedRoleSchema,
|
||||||
packedRoleCondFormulaValueCreatedSchema,
|
packedRoleCondFormulaValueCreatedSchema,
|
||||||
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
||||||
|
packedRoleCondFormulaValueNot,
|
||||||
packedRoleCondFormulaValueSchema,
|
packedRoleCondFormulaValueSchema,
|
||||||
packedRoleCondFormulaValueUserSettingBooleanSchema,
|
packedRoleCondFormulaValueUserSettingBooleanSchema,
|
||||||
|
packedRoleLiteSchema,
|
||||||
|
packedRolePoliciesSchema,
|
||||||
|
packedRoleSchema,
|
||||||
} from '@/models/json-schema/role.js';
|
} from '@/models/json-schema/role.js';
|
||||||
import { packedAdSchema } from '@/models/json-schema/ad.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 {
|
import {
|
||||||
packedMetaLiteSchema,
|
|
||||||
packedMetaDetailedOnlySchema,
|
packedMetaDetailedOnlySchema,
|
||||||
packedMetaDetailedSchema,
|
packedMetaDetailedSchema,
|
||||||
|
packedMetaLiteSchema,
|
||||||
} from '@/models/json-schema/meta.js';
|
} 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 = {
|
export const refs = {
|
||||||
UserLite: packedUserLiteSchema,
|
UserLite: packedUserLiteSchema,
|
||||||
|
@ -111,6 +113,8 @@ export const refs = {
|
||||||
MetaLite: packedMetaLiteSchema,
|
MetaLite: packedMetaLiteSchema,
|
||||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||||
MetaDetailed: packedMetaDetailedSchema,
|
MetaDetailed: packedMetaDetailedSchema,
|
||||||
|
SystemWebhook: packedSystemWebhookSchema,
|
||||||
|
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||||
|
|
|
@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
|
||||||
return Math.max(...xs);
|
return Math.max(...xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits an array based on the equivalence relation.
|
|
||||||
* The concatenation of the result is equal to the argument.
|
|
||||||
*/
|
|
||||||
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
|
||||||
const groups = [] as T[][];
|
|
||||||
for (const x of xs) {
|
|
||||||
const lastGroup = groups.at(-1);
|
|
||||||
if (lastGroup !== undefined && f(lastGroup[0], x)) {
|
|
||||||
lastGroup.push(x);
|
|
||||||
} else {
|
|
||||||
groups.push([x]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits an array based on the equivalence relation induced by the function.
|
|
||||||
* The concatenation of the result is equal to the argument.
|
|
||||||
*/
|
|
||||||
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
|
||||||
return groupBy((a, b) => f(a) === f(b), xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
|
|
||||||
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
|
||||||
const key = keySelector(item);
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
||||||
obj[key] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
obj[key].push(item);
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two arrays by lexicographical order
|
* Compare two arrays by lexicographical order
|
||||||
*/
|
*/
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue