Merge branch 'develop' into feature/default-post-target-detect-from-path

This commit is contained in:
かっこかり 2024-07-04 19:37:42 +09:00 committed by GitHub
commit 1ca4d5fc65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
368 changed files with 15296 additions and 6116 deletions

View File

@ -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:

View File

@ -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": [

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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 }}

4
.gitignore vendored
View File

@ -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

View File

@ -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`のみに

View File

@ -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`.

View File

@ -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

View File

@ -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.

View File

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

View File

@ -1,5 +1,3 @@
version: "3"
services: services:
web: web:
build: . build: .

114
locales/index.d.ts vendored
View File

@ -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;
/** /**

View File

@ -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) => {

View File

@ -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:

View File

@ -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: "向右移动"

View File

@ -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: "向右移動"

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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"`);
}
}

View File

@ -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"
} }
} }

View File

@ -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') {

View File

@ -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 {
/** /**

View File

@ -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

View File

@ -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');

View File

@ -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) {

View File

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

View File

@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { QueueService } from '@/core/QueueService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { IdService } from './IdService.js';
@Injectable()
export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) {
}
/**
* DBに記録し.
* - Redisイベント
* - EMailmetaテーブルに設定されているメールアドレス
* - SystemWebhook
*
* @param params .
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async report(params: {
targetUserId: MiAbuseUserReport['targetUserId'],
targetUserHost: MiAbuseUserReport['targetUserHost'],
reporterId: MiAbuseUserReport['reporterId'],
reporterHost: MiAbuseUserReport['reporterHost'],
comment: string,
}[]) {
const entities = params.map(param => {
return {
id: this.idService.gen(),
targetUserId: param.targetUserId,
targetUserHost: param.targetUserHost,
reporterId: param.reporterId,
reporterHost: param.reporterHost,
comment: param.comment,
};
});
const reports = Array.of<MiAbuseUserReport>();
for (const entity of entities) {
const report = await this.abuseUserReportsRepository.insertOne(entity);
reports.push(report);
}
return Promise.all([
this.abuseReportNotificationService.notifyAdminStream(reports),
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
this.abuseReportNotificationService.notifyMail(reports),
]);
}
/**
* .
* - SystemWebhook
*
* @param params .
* @param operator
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
forward: boolean;
}[],
operator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
id: In(params.map(it => it.reportId)),
});
for (const report of reports) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ps = paramsMap.get(report.id)!;
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: operator.id,
forwarded: ps.forward && report.targetUserHost !== null,
});
if (ps.forward && report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
// eslint-disable-next-line
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
const contextAssignedFlag = this.apRendererService.addContext(flag);
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
}
this.moderationLogService
.log(operator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost !== null,
})
.then();
}
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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();
} }

View File

@ -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,

View File

@ -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;

View File

@ -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)) {

View File

@ -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');
} }

View File

@ -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);

View File

@ -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;

View File

@ -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');

View File

@ -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;

View File

@ -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);
} }
} }

View File

@ -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) =>

View File

@ -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(),
]); ]);
} }

View File

@ -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',

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

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

View File

@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { 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,
}); });
} }

View File

@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js'; import 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,
}); });
} }

View File

@ -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();
} }

View File

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

View File

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

View File

@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import 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 {

View File

@ -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';
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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])); });
})); }));
} }
} }

View File

@ -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),

View File

@ -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);

View File

@ -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[] {

View File

@ -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');
} }
} }

View File

@ -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)),

View File

@ -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`);

View File

@ -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)));
}
}

View File

@ -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 });
}),
);
} }
} }

View File

@ -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, // 後方互換性のため
}; };
} }
} }

View File

@ -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) })));
} }
} }

View File

@ -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) })));
} }
} }

View File

@ -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);
} }
} }

View File

@ -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) })));
} }
} }

View File

@ -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 });
}),
);
}
} }

View File

@ -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 });
}),
);
} }
} }

View File

@ -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) })));
} }
} }

View File

@ -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 });
}),
);
} }
} }

View File

@ -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,

View File

@ -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) })));
} }
} }

View File

@ -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) })));
} }
} }

View File

@ -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,
}, },
}))); })));
} }

View File

@ -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) })));
} }
} }

View File

@ -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;
} }

View File

@ -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) })));
} }
} }

View File

@ -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) })));
} }
} }

View File

@ -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),
});
}),
);
} }
} }

View File

@ -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)));
}
}

View File

@ -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,

View File

@ -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,
}))); })));
} }

View File

@ -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'),

View File

@ -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;
} }

View File

@ -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;
}

View File

@ -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]>;

View File

@ -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