Merge branch 'develop' into fix-7311
This commit is contained in:
commit
a5424f4134
|
@ -1,13 +1,15 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||||
|
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||||
- Fix: センシティブな画像をアイコン・バナーに指定できないように
|
- Fix: センシティブな画像をアイコン・バナーに指定できないように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: アイコン画像・バナー画像を外せるように
|
- Enhance: アイコン画像・バナー画像を外せるように
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
|
||||||
|
|
||||||
|
|
||||||
## 2024.5.0
|
## 2024.5.0
|
||||||
|
|
|
@ -9314,6 +9314,10 @@ export interface Locale extends ILocale {
|
||||||
* Webhookを作成
|
* Webhookを作成
|
||||||
*/
|
*/
|
||||||
"createWebhook": string;
|
"createWebhook": string;
|
||||||
|
/**
|
||||||
|
* Webhookを編集
|
||||||
|
*/
|
||||||
|
"modifyWebhook": string;
|
||||||
/**
|
/**
|
||||||
* 名前
|
* 名前
|
||||||
*/
|
*/
|
||||||
|
@ -9360,6 +9364,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": {
|
||||||
/**
|
/**
|
||||||
|
@ -9506,6 +9576,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": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2470,6 +2470,7 @@ _drivecleaner:
|
||||||
|
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
createWebhook: "Webhookを作成"
|
createWebhook: "Webhookを作成"
|
||||||
|
modifyWebhook: "Webhookを編集"
|
||||||
name: "名前"
|
name: "名前"
|
||||||
secret: "シークレット"
|
secret: "シークレット"
|
||||||
events: "Webhookを実行するタイミング"
|
events: "Webhookを実行するタイミング"
|
||||||
|
@ -2482,6 +2483,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: "ロールを作成"
|
||||||
|
@ -2520,6 +2541,12 @@ _moderationLogTypes:
|
||||||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||||
unsetUserAvatar: "ユーザーのアイコンを解除"
|
unsetUserAvatar: "ユーザーのアイコンを解除"
|
||||||
unsetUserBanner: "ユーザーのバナーを解除"
|
unsetUserBanner: "ユーザーのバナーを解除"
|
||||||
|
createSystemWebhook: "SystemWebhookを作成"
|
||||||
|
updateSystemWebhook: "SystemWebhookを更新"
|
||||||
|
deleteSystemWebhook: "SystemWebhookを削除"
|
||||||
|
createAbuseReportNotificationRecipient: "通報の通知先を作成"
|
||||||
|
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
|
||||||
|
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "ファイルの詳細"
|
title: "ファイルの詳細"
|
||||||
|
|
|
@ -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";
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const nestLogger = logger.createSubLogger('nest', 'green', false);
|
const nestLogger = logger.createSubLogger('nest', 'green');
|
||||||
|
|
||||||
export class NestLogger implements LoggerService {
|
export class NestLogger implements LoggerService {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
|
||||||
EventEmitter.defaultMaxListeners = 128;
|
EventEmitter.defaultMaxListeners = 128;
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
const clusterLogger = logger.createSubLogger('cluster', 'orange');
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
//#region Events
|
//#region Events
|
||||||
|
|
|
@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
|
||||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const logger = new Logger('core', 'cyan');
|
||||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||||
|
|
||||||
const themeColor = chalk.hex('#86b300');
|
const themeColor = chalk.hex('#86b300');
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
|
import { loadConfig } from '@/config.js';
|
||||||
import { jobQueue, server } from './common.js';
|
import { jobQueue, server } from './common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init worker process
|
* Init worker process
|
||||||
*/
|
*/
|
||||||
export async function workerMain() {
|
export async function workerMain() {
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.init({
|
||||||
|
integrations: [
|
||||||
|
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Performance Monitoring
|
||||||
|
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||||
|
|
||||||
|
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||||
|
profilesSampleRate: 1.0,
|
||||||
|
|
||||||
|
maxBreadcrumbs: 0,
|
||||||
|
|
||||||
|
...config.sentryForBackend.options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (envOption.onlyServer) {
|
if (envOption.onlyServer) {
|
||||||
await server();
|
await server();
|
||||||
} else if (envOption.onlyQueue) {
|
} else if (envOption.onlyQueue) {
|
||||||
|
|
|
@ -0,0 +1,406 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import { Brackets, In, IsNull, Not } from 'typeorm';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
|
import type {
|
||||||
|
AbuseReportNotificationRecipientRepository,
|
||||||
|
MiAbuseReportNotificationRecipient,
|
||||||
|
MiAbuseUserReport,
|
||||||
|
MiUser,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { IdService } from './IdService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.abuseReportNotificationRecipientRepository)
|
||||||
|
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
private idService: IdService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private emailService: EmailService,
|
||||||
|
private metaService: MetaService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||||
|
* 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
|
||||||
|
*
|
||||||
|
* @see RoleService.getModeratorIds
|
||||||
|
* @see GlobalEventService.publishAdminStream
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
|
||||||
|
if (abuseReports.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moderatorIds = await this.roleService.getModeratorIds(true, true);
|
||||||
|
|
||||||
|
for (const moderatorId of moderatorIds) {
|
||||||
|
for (const abuseReport of abuseReports) {
|
||||||
|
this.globalEventService.publishAdminStream(
|
||||||
|
moderatorId,
|
||||||
|
'newAbuseUserReport',
|
||||||
|
{
|
||||||
|
id: abuseReport.id,
|
||||||
|
targetUserId: abuseReport.targetUserId,
|
||||||
|
reporterId: abuseReport.reporterId,
|
||||||
|
comment: abuseReport.comment,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||||
|
* メールアドレスの送信先は以下の通り.
|
||||||
|
* - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
|
||||||
|
* - metaテーブルに設定されているメールアドレス
|
||||||
|
*
|
||||||
|
* @see EmailService.sendEmail
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async notifyMail(abuseReports: MiAbuseUserReport[]) {
|
||||||
|
if (abuseReports.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
|
||||||
|
.filter(it => it.isActive && it.userProfile?.emailVerified)
|
||||||
|
.map(it => it.userProfile?.email)
|
||||||
|
.filter(isNotNull),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 送信先の鮮度を保つため、毎回取得する
|
||||||
|
const meta = await this.metaService.fetch(true);
|
||||||
|
recipientEMailAddresses.push(
|
||||||
|
...(meta.email ? [meta.email] : []),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recipientEMailAddresses.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mailAddress of recipientEMailAddresses) {
|
||||||
|
await Promise.all(
|
||||||
|
abuseReports.map(it => {
|
||||||
|
// TODO: 送信処理はJobQueue化したい
|
||||||
|
return this.emailService.sendEmail(
|
||||||
|
mailAddress,
|
||||||
|
'New Abuse Report',
|
||||||
|
sanitizeHtml(it.comment),
|
||||||
|
sanitizeHtml(it.comment),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||||
|
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
|
||||||
|
*
|
||||||
|
* @see SystemWebhookService.enqueueSystemWebhook
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async notifySystemWebhook(
|
||||||
|
abuseReports: MiAbuseUserReport[],
|
||||||
|
type: 'abuseReport' | 'abuseReportResolved',
|
||||||
|
) {
|
||||||
|
if (abuseReports.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientWebhookIds = await this.fetchWebhookRecipients()
|
||||||
|
.then(it => it
|
||||||
|
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||||
|
.map(it => it.systemWebhookId)
|
||||||
|
.filter(isNotNull));
|
||||||
|
for (const webhookId of recipientWebhookIds) {
|
||||||
|
await Promise.all(
|
||||||
|
abuseReports.map(it => {
|
||||||
|
return this.systemWebhookService.enqueueSystemWebhook(
|
||||||
|
webhookId,
|
||||||
|
type,
|
||||||
|
it,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通報の通知先一覧を取得する.
|
||||||
|
*
|
||||||
|
* @param {Object} [params] クエリの取得条件
|
||||||
|
* @param {Object} [params.method] 取得する通知先の通知方法
|
||||||
|
* @param {Object} [opts] 動作時の詳細なオプション
|
||||||
|
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||||
|
* @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
|
||||||
|
* @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
|
||||||
|
* @see removeUnauthorizedRecipientUsers
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async fetchRecipients(
|
||||||
|
params?: {
|
||||||
|
ids?: MiAbuseReportNotificationRecipient['id'][],
|
||||||
|
method?: RecipientMethod[],
|
||||||
|
},
|
||||||
|
opts?: {
|
||||||
|
removeUnauthorized?: boolean,
|
||||||
|
joinUser?: boolean,
|
||||||
|
joinSystemWebhook?: boolean,
|
||||||
|
},
|
||||||
|
): Promise<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(isNotNull));
|
||||||
|
if (recipientUserIds.size <= 0) {
|
||||||
|
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
// モデレータ権限の有無で通知先設定を振り分ける
|
||||||
|
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
|
||||||
|
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||||
|
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||||
|
for (const recipient of userRecipients) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (authorizedUserIds.includes(recipient.userId!)) {
|
||||||
|
authorizedUserRecipients.push(recipient);
|
||||||
|
} else {
|
||||||
|
unauthorizedUserRecipients.push(recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// モデレータ権限を持たない通知先をDBから削除する
|
||||||
|
if (unauthorizedUserRecipients.length > 0) {
|
||||||
|
await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
|
||||||
|
}
|
||||||
|
const nonUserRecipients = recipients.filter(it => it.userId === null);
|
||||||
|
return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
if (obj.channel !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'roleUpdated':
|
||||||
|
case 'roleDeleted':
|
||||||
|
case 'userRoleUnassigned': {
|
||||||
|
// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
|
||||||
|
process.nextTick(async () => {
|
||||||
|
const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
|
||||||
|
userId: Not(IsNull()),
|
||||||
|
});
|
||||||
|
await this.removeUnauthorizedRecipientUsers(recipients);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { IdService } from './IdService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AbuseReportService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.abuseUserReportsRepository)
|
||||||
|
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
private idService: IdService,
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
private instanceActorService: InstanceActorService,
|
||||||
|
private apRendererService: ApRendererService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
|
||||||
|
* - 管理者用Redisイベント
|
||||||
|
* - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
|
||||||
|
* - SystemWebhook
|
||||||
|
*
|
||||||
|
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||||
|
* @see AbuseReportNotificationService.notify
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async report(params: {
|
||||||
|
targetUserId: MiAbuseUserReport['targetUserId'],
|
||||||
|
targetUserHost: MiAbuseUserReport['targetUserHost'],
|
||||||
|
reporterId: MiAbuseUserReport['reporterId'],
|
||||||
|
reporterHost: MiAbuseUserReport['reporterHost'],
|
||||||
|
comment: string,
|
||||||
|
}[]) {
|
||||||
|
const entities = params.map(param => {
|
||||||
|
return {
|
||||||
|
id: this.idService.gen(),
|
||||||
|
targetUserId: param.targetUserId,
|
||||||
|
targetUserHost: param.targetUserHost,
|
||||||
|
reporterId: param.reporterId,
|
||||||
|
reporterHost: param.reporterHost,
|
||||||
|
comment: param.comment,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const reports = Array.of<MiAbuseUserReport>();
|
||||||
|
for (const entity of entities) {
|
||||||
|
const report = await this.abuseUserReportsRepository.insertOne(entity);
|
||||||
|
reports.push(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
this.abuseReportNotificationService.notifyAdminStream(reports),
|
||||||
|
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
|
||||||
|
this.abuseReportNotificationService.notifyMail(reports),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通報を解決し、その内容を下記の手段で管理者各位に通知する.
|
||||||
|
* - SystemWebhook
|
||||||
|
*
|
||||||
|
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||||
|
* @param operator 通報を処理したユーザ
|
||||||
|
* @see AbuseReportNotificationService.notify
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async resolve(
|
||||||
|
params: {
|
||||||
|
reportId: string;
|
||||||
|
forward: boolean;
|
||||||
|
}[],
|
||||||
|
operator: MiUser,
|
||||||
|
) {
|
||||||
|
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||||
|
const reports = await this.abuseUserReportsRepository.findBy({
|
||||||
|
id: In(params.map(it => it.reportId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const report of reports) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const ps = paramsMap.get(report.id)!;
|
||||||
|
|
||||||
|
await this.abuseUserReportsRepository.update(report.id, {
|
||||||
|
resolved: true,
|
||||||
|
assigneeId: operator.id,
|
||||||
|
forwarded: ps.forward && report.targetUserHost !== null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ps.forward && report.targetUserHost != null) {
|
||||||
|
const actor = await this.instanceActorService.getInstanceActor();
|
||||||
|
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||||
|
const contextAssignedFlag = this.apRendererService.addContext(flag);
|
||||||
|
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.moderationLogService
|
||||||
|
.log(operator, 'resolveAbuseReport', {
|
||||||
|
reportId: report.id,
|
||||||
|
report: report,
|
||||||
|
forwarded: ps.forward && report.targetUserHost !== null,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||||
|
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
import type { MiSignin } from '@/models/Signin.js';
|
import type { MiSignin } from '@/models/Signin.js';
|
||||||
import type { MiPage } from '@/models/Page.js';
|
import type { MiPage } from '@/models/Page.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
@ -227,6 +228,9 @@ export interface InternalEventTypes {
|
||||||
webhookCreated: MiWebhook;
|
webhookCreated: MiWebhook;
|
||||||
webhookDeleted: MiWebhook;
|
webhookDeleted: MiWebhook;
|
||||||
webhookUpdated: MiWebhook;
|
webhookUpdated: MiWebhook;
|
||||||
|
systemWebhookCreated: MiSystemWebhook;
|
||||||
|
systemWebhookDeleted: MiSystemWebhook;
|
||||||
|
systemWebhookUpdated: MiSystemWebhook;
|
||||||
antennaCreated: MiAntenna;
|
antennaCreated: MiAntenna;
|
||||||
antennaDeleted: MiAntenna;
|
antennaDeleted: MiAntenna;
|
||||||
antennaUpdated: MiAntenna;
|
antennaUpdated: MiAntenna;
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class LoggerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
public getLogger(domain: string, color?: KEYWORD | undefined) {
|
||||||
return new Logger(domain, color, store);
|
return new Logger(domain, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { HashtagService } from '@/core/HashtagService.js';
|
import { HashtagService } from '@/core/HashtagService.js';
|
||||||
import { AntennaService } from '@/core/AntennaService.js';
|
import { AntennaService } from '@/core/AntennaService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
@ -205,7 +205,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 +606,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 +633,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 +656,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 +788,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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
|
||||||
import { allSettled } from '@/misc/promise-tracker.js';
|
import { allSettled } from '@/misc/promise-tracker.js';
|
||||||
|
import {
|
||||||
|
DeliverJobData,
|
||||||
|
EndedPollNotificationJobData,
|
||||||
|
InboxJobData,
|
||||||
|
RelationshipJobData,
|
||||||
|
UserWebhookDeliverJobData,
|
||||||
|
SystemWebhookDeliverJobData,
|
||||||
|
} from '../queue/types.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
|
||||||
|
|
||||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||||
|
@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||||
export type DbQueue = Bull.Queue;
|
export type DbQueue = Bull.Queue;
|
||||||
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||||
export type ObjectStorageQueue = Bull.Queue;
|
export type ObjectStorageQueue = Bull.Queue;
|
||||||
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||||
|
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||||
|
|
||||||
const $system: Provider = {
|
const $system: Provider = {
|
||||||
provide: 'queue:system',
|
provide: 'queue:system',
|
||||||
|
@ -63,9 +71,15 @@ const $objectStorage: Provider = {
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $webhookDeliver: Provider = {
|
const $userWebhookDeliver: Provider = {
|
||||||
provide: 'queue:webhookDeliver',
|
provide: 'queue:userWebhookDeliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
|
||||||
|
inject: [DI.config],
|
||||||
|
};
|
||||||
|
|
||||||
|
const $systemWebhookDeliver: Provider = {
|
||||||
|
provide: 'queue:systemWebhookDeliver',
|
||||||
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
|
||||||
$db,
|
$db,
|
||||||
$relationship,
|
$relationship,
|
||||||
$objectStorage,
|
$objectStorage,
|
||||||
$webhookDeliver,
|
$userWebhookDeliver,
|
||||||
|
$systemWebhookDeliver,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
$system,
|
$system,
|
||||||
|
@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
|
||||||
$db,
|
$db,
|
||||||
$relationship,
|
$relationship,
|
||||||
$objectStorage,
|
$objectStorage,
|
||||||
$webhookDeliver,
|
$userWebhookDeliver,
|
||||||
|
$systemWebhookDeliver,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class QueueModule implements OnApplicationShutdown {
|
export class QueueModule implements OnApplicationShutdown {
|
||||||
|
@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||||
|
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
|
@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
this.dbQueue.close(),
|
this.dbQueue.close(),
|
||||||
this.relationshipQueue.close(),
|
this.relationshipQueue.close(),
|
||||||
this.objectStorageQueue.close(),
|
this.objectStorageQueue.close(),
|
||||||
this.webhookDeliverQueue.close(),
|
this.userWebhookDeliverQueue.close(),
|
||||||
|
this.systemWebhookDeliverQueue.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||||
|
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
import type {
|
||||||
|
DbJobData,
|
||||||
|
DeliverJobData,
|
||||||
|
RelationshipJobData,
|
||||||
|
SystemWebhookDeliverJobData,
|
||||||
|
ThinUser,
|
||||||
|
UserWebhookDeliverJobData,
|
||||||
|
} from '../queue/types.js';
|
||||||
|
import type {
|
||||||
|
DbQueue,
|
||||||
|
DeliverQueue,
|
||||||
|
EndedPollNotificationQueue,
|
||||||
|
InboxQueue,
|
||||||
|
ObjectStorageQueue,
|
||||||
|
RelationshipQueue,
|
||||||
|
SystemQueue,
|
||||||
|
UserWebhookDeliverQueue,
|
||||||
|
SystemWebhookDeliverQueue,
|
||||||
|
} from './QueueModule.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -31,7 +49,8 @@ export class QueueService {
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||||
|
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||||
) {
|
) {
|
||||||
this.systemQueue.add('tickCharts', {
|
this.systemQueue.add('tickCharts', {
|
||||||
}, {
|
}, {
|
||||||
|
@ -431,9 +450,13 @@ export class QueueService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UserWebhookDeliverJobData
|
||||||
|
* @see WebhookDeliverProcessorService
|
||||||
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||||
const data = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
webhookId: webhook.id,
|
webhookId: webhook.id,
|
||||||
|
@ -444,7 +467,33 @@ export class QueueService {
|
||||||
eventId: randomUUID(),
|
eventId: randomUUID(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.webhookDeliverQueue.add(webhook.id, data, {
|
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
|
attempts: 4,
|
||||||
|
backoff: {
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see SystemWebhookDeliverJobData
|
||||||
|
* @see WebhookDeliverProcessorService
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
||||||
|
const data: SystemWebhookDeliverJobData = {
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
webhookId: webhook.id,
|
||||||
|
to: webhook.url,
|
||||||
|
secret: webhook.secret,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
eventId: randomUUID(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
|
private logger: Logger;
|
||||||
|
private activeSystemWebhooksFetched = false;
|
||||||
|
private activeSystemWebhooks: MiSystemWebhook[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
@Inject(DI.systemWebhooksRepository)
|
||||||
|
private systemWebhooksRepository: SystemWebhooksRepository,
|
||||||
|
private idService: IdService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
this.logger = this.loggerService.getLogger('webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async fetchActiveSystemWebhooks() {
|
||||||
|
if (!this.activeSystemWebhooksFetched) {
|
||||||
|
this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
this.activeSystemWebhooksFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeSystemWebhooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook の一覧を取得する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async fetchSystemWebhooks(params?: {
|
||||||
|
ids?: MiSystemWebhook['id'][];
|
||||||
|
isActive?: MiSystemWebhook['isActive'];
|
||||||
|
on?: MiSystemWebhook['on'];
|
||||||
|
}): Promise<MiSystemWebhook[]> {
|
||||||
|
const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
|
||||||
|
if (params) {
|
||||||
|
if (params.ids && params.ids.length > 0) {
|
||||||
|
query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
|
||||||
|
}
|
||||||
|
if (params.isActive !== undefined) {
|
||||||
|
query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
|
||||||
|
}
|
||||||
|
if (params.on && params.on.length > 0) {
|
||||||
|
query.andWhere(':on <@ systemWebhook.on', { on: params.on });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook を作成する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async createSystemWebhook(
|
||||||
|
params: {
|
||||||
|
isActive: MiSystemWebhook['isActive'];
|
||||||
|
name: MiSystemWebhook['name'];
|
||||||
|
on: MiSystemWebhook['on'];
|
||||||
|
url: MiSystemWebhook['url'];
|
||||||
|
secret: MiSystemWebhook['secret'];
|
||||||
|
},
|
||||||
|
updater: MiUser,
|
||||||
|
): Promise<MiSystemWebhook> {
|
||||||
|
const id = this.idService.gen();
|
||||||
|
await this.systemWebhooksRepository.insert({
|
||||||
|
...params,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||||
|
this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'createSystemWebhook', {
|
||||||
|
systemWebhookId: webhook.id,
|
||||||
|
webhook: webhook,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
|
||||||
|
return webhook;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook を更新する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async updateSystemWebhook(
|
||||||
|
params: {
|
||||||
|
id: MiSystemWebhook['id'];
|
||||||
|
isActive: MiSystemWebhook['isActive'];
|
||||||
|
name: MiSystemWebhook['name'];
|
||||||
|
on: MiSystemWebhook['on'];
|
||||||
|
url: MiSystemWebhook['url'];
|
||||||
|
secret: MiSystemWebhook['secret'];
|
||||||
|
},
|
||||||
|
updater: MiUser,
|
||||||
|
): Promise<MiSystemWebhook> {
|
||||||
|
const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
|
||||||
|
await this.systemWebhooksRepository.update(beforeEntity.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isActive: params.isActive,
|
||||||
|
name: params.name,
|
||||||
|
on: params.on,
|
||||||
|
url: params.url,
|
||||||
|
secret: params.secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
|
||||||
|
this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'updateSystemWebhook', {
|
||||||
|
systemWebhookId: beforeEntity.id,
|
||||||
|
before: beforeEntity,
|
||||||
|
after: afterEntity,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
|
||||||
|
return afterEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook を削除する.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
|
||||||
|
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||||
|
await this.systemWebhooksRepository.delete(id);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
|
||||||
|
this.moderationLogService
|
||||||
|
.log(updater, 'deleteSystemWebhook', {
|
||||||
|
systemWebhookId: webhook.id,
|
||||||
|
webhook,
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemWebhook をWebhook配送キューに追加する
|
||||||
|
* @see QueueService.systemWebhookDeliver
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||||
|
const webhookEntity = typeof webhook === 'string'
|
||||||
|
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||||
|
: webhook;
|
||||||
|
if (!webhookEntity || !webhookEntity.isActive) {
|
||||||
|
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!webhookEntity.on.includes(type)) {
|
||||||
|
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
if (obj.channel !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'systemWebhookCreated': {
|
||||||
|
if (body.isActive) {
|
||||||
|
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'systemWebhookUpdated': {
|
||||||
|
if (body.isActive) {
|
||||||
|
const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
|
||||||
|
if (i > -1) {
|
||||||
|
this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
|
||||||
|
} else {
|
||||||
|
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'systemWebhookDeleted': {
|
||||||
|
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: UserWebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
|
@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||||
user: packed,
|
user: packed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: UserWebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
|
@ -331,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'follow', {
|
this.queueService.userWebhookDeliver(webhook, 'follow', {
|
||||||
user: packed,
|
user: packed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -345,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'followed', {
|
this.queueService.userWebhookDeliver(webhook, 'followed', {
|
||||||
user: packed,
|
user: packed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -398,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||||
user: packed,
|
user: packed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import type { WebhooksRepository } from '@/models/_.js';
|
||||||
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserWebhookService implements OnApplicationShutdown {
|
||||||
|
private activeWebhooksFetched = false;
|
||||||
|
private activeWebhooks: MiWebhook[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
@Inject(DI.webhooksRepository)
|
||||||
|
private webhooksRepository: WebhooksRepository,
|
||||||
|
) {
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getActiveWebhooks() {
|
||||||
|
if (!this.activeWebhooksFetched) {
|
||||||
|
this.activeWebhooks = await this.webhooksRepository.findBy({
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
this.activeWebhooksFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeWebhooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
if (obj.channel !== 'internal') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'webhookCreated': {
|
||||||
|
if (body.active) {
|
||||||
|
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'webhookUpdated': {
|
||||||
|
if (body.active) {
|
||||||
|
const i = this.activeWebhooks.findIndex(a => a.id === body.id);
|
||||||
|
if (i > -1) {
|
||||||
|
this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'webhookDeleted': {
|
||||||
|
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,97 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WebhookService implements OnApplicationShutdown {
|
|
||||||
private webhooksFetched = false;
|
|
||||||
private webhooks: MiWebhook[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.redisForSub)
|
|
||||||
private redisForSub: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.webhooksRepository)
|
|
||||||
private webhooksRepository: WebhooksRepository,
|
|
||||||
) {
|
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
|
||||||
this.redisForSub.on('message', this.onMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getActiveWebhooks() {
|
|
||||||
if (!this.webhooksFetched) {
|
|
||||||
this.webhooks = await this.webhooksRepository.findBy({
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
this.webhooksFetched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.webhooks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
|
||||||
const obj = JSON.parse(data);
|
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
|
||||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
|
||||||
switch (type) {
|
|
||||||
case 'webhookCreated':
|
|
||||||
if (body.active) {
|
|
||||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
|
||||||
...body,
|
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
|
||||||
user: null, // joinなカラムは通常取ってこないので
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'webhookUpdated':
|
|
||||||
if (body.active) {
|
|
||||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
|
||||||
if (i > -1) {
|
|
||||||
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
|
||||||
...body,
|
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
|
||||||
user: null, // joinなカラムは通常取ってこないので
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
|
||||||
...body,
|
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
|
||||||
user: null, // joinなカラムは通常取ってこないので
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'webhookDeleted':
|
|
||||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public dispose(): void {
|
|
||||||
this.redisForSub.off('message', this.onMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -29,6 +29,7 @@ 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 { 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 +58,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 +66,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,
|
||||||
|
@ -545,14 +544,13 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,6 @@ export class ChartLoggerService {
|
||||||
constructor(
|
constructor(
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
|
this.logger = this.loggerService.getLogger('chart', 'white');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
|
|
||||||
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
||||||
.select('instance.host')
|
.select('instance.host')
|
||||||
.where('instance.isSuspended = true');
|
.where('instance.suspensionState != \'none\'');
|
||||||
|
|
||||||
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
|
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
|
||||||
.select('f.followerHost')
|
.select('f.followerHost')
|
||||||
|
@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere('instance.isSuspended = false')
|
.andWhere('instance.suspensionState = \'none\'')
|
||||||
.andWhere('instance.isNotResponding = false')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
|
@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||||
.andWhere('instance.isSuspended = false')
|
.andWhere('instance.suspensionState = \'none\'')
|
||||||
.andWhere('instance.isNotResponding = false')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||||
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AbuseReportNotificationRecipientEntityService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.abuseReportNotificationRecipientRepository)
|
||||||
|
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
private systemWebhookEntityService: SystemWebhookEntityService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async pack(
|
||||||
|
src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
|
||||||
|
opts?: {
|
||||||
|
users: Map<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(isNotNull);
|
||||||
|
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(isNotNull);
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ export const DI = {
|
||||||
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
||||||
hashtagsRepository: Symbol('hashtagsRepository'),
|
hashtagsRepository: Symbol('hashtagsRepository'),
|
||||||
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
|
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
|
||||||
|
abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
|
||||||
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
|
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
|
||||||
authSessionsRepository: Symbol('authSessionsRepository'),
|
authSessionsRepository: Symbol('authSessionsRepository'),
|
||||||
accessTokensRepository: Symbol('accessTokensRepository'),
|
accessTokensRepository: Symbol('accessTokensRepository'),
|
||||||
|
@ -70,6 +71,7 @@ export const DI = {
|
||||||
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
||||||
registryItemsRepository: Symbol('registryItemsRepository'),
|
registryItemsRepository: Symbol('registryItemsRepository'),
|
||||||
webhooksRepository: Symbol('webhooksRepository'),
|
webhooksRepository: Symbol('webhooksRepository'),
|
||||||
|
systemWebhooksRepository: Symbol('systemWebhooksRepository'),
|
||||||
adsRepository: Symbol('adsRepository'),
|
adsRepository: Symbol('adsRepository'),
|
||||||
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
|
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
|
||||||
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
|
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
|
||||||
|
|
|
@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
private context: Context;
|
private context: Context;
|
||||||
private parentLogger: Logger | null = null;
|
private parentLogger: Logger | null = null;
|
||||||
private store: boolean;
|
|
||||||
|
|
||||||
constructor(context: string, color?: KEYWORD, store = true) {
|
constructor(context: string, color?: KEYWORD) {
|
||||||
this.context = {
|
this.context = {
|
||||||
name: context,
|
name: context,
|
||||||
color: color,
|
color: color,
|
||||||
};
|
};
|
||||||
this.store = store;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
|
public createSubLogger(context: string, color?: KEYWORD): Logger {
|
||||||
const logger = new Logger(context, color, store);
|
const logger = new Logger(context, color);
|
||||||
logger.parentLogger = this;
|
logger.parentLogger = this;
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = [], store = true): void {
|
private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = []): void {
|
||||||
if (envOption.quiet) return;
|
if (envOption.quiet) return;
|
||||||
if (!this.store) store = false;
|
|
||||||
if (level === 'debug') store = false;
|
|
||||||
|
|
||||||
if (this.parentLogger) {
|
if (this.parentLogger) {
|
||||||
this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store);
|
this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]>;
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||||
|
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||||
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
|
import { id } from './util/id.js';
|
||||||
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通報受信時に通知を送信する方法.
|
||||||
|
*/
|
||||||
|
export type RecipientMethod = 'email' | 'webhook';
|
||||||
|
|
||||||
|
@Entity('abuse_report_notification_recipient')
|
||||||
|
export class MiAbuseReportNotificationRecipient {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有効かどうか.
|
||||||
|
*/
|
||||||
|
@Index()
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public isActive: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日時.
|
||||||
|
*/
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知設定名.
|
||||||
|
*/
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 255,
|
||||||
|
})
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知方法.
|
||||||
|
*/
|
||||||
|
@Index()
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 64,
|
||||||
|
})
|
||||||
|
public method: RecipientMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先のユーザID.
|
||||||
|
*/
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public userId: MiUser['id'] | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先のユーザ.
|
||||||
|
*/
|
||||||
|
@ManyToOne(type => MiUser, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
|
||||||
|
public user: MiUser | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先のユーザプロフィール.
|
||||||
|
*/
|
||||||
|
@ManyToOne(type => MiUserProfile, {})
|
||||||
|
@JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
|
||||||
|
public userProfile: MiUserProfile | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先のシステムWebhookId.
|
||||||
|
*/
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public systemWebhookId: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知先のシステムWebhook.
|
||||||
|
*/
|
||||||
|
@ManyToOne(type => MiSystemWebhook, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public systemWebhook: MiSystemWebhook | null;
|
||||||
|
}
|
|
@ -3,11 +3,83 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Provider } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MiRepository, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, miRepository } from './_.js';
|
import {
|
||||||
|
MiAbuseReportNotificationRecipient,
|
||||||
|
MiAbuseUserReport,
|
||||||
|
MiAccessToken,
|
||||||
|
MiAd,
|
||||||
|
MiAnnouncement,
|
||||||
|
MiAnnouncementRead,
|
||||||
|
MiAntenna,
|
||||||
|
MiApp,
|
||||||
|
MiAuthSession,
|
||||||
|
MiAvatarDecoration,
|
||||||
|
MiBlocking,
|
||||||
|
MiBubbleGameRecord,
|
||||||
|
MiChannel,
|
||||||
|
MiChannelFavorite,
|
||||||
|
MiChannelFollowing,
|
||||||
|
MiClip,
|
||||||
|
MiClipFavorite,
|
||||||
|
MiClipNote,
|
||||||
|
MiDriveFile,
|
||||||
|
MiDriveFolder,
|
||||||
|
MiEmoji,
|
||||||
|
MiFlash,
|
||||||
|
MiFlashLike,
|
||||||
|
MiFollowing,
|
||||||
|
MiFollowRequest,
|
||||||
|
MiGalleryLike,
|
||||||
|
MiGalleryPost,
|
||||||
|
MiHashtag,
|
||||||
|
MiInstance,
|
||||||
|
MiMeta,
|
||||||
|
MiModerationLog,
|
||||||
|
MiMuting,
|
||||||
|
MiNote,
|
||||||
|
MiNoteFavorite,
|
||||||
|
MiNoteReaction,
|
||||||
|
MiNoteThreadMuting,
|
||||||
|
MiNoteUnread,
|
||||||
|
MiPage,
|
||||||
|
MiPageLike,
|
||||||
|
MiPasswordResetRequest,
|
||||||
|
MiPoll,
|
||||||
|
MiPollVote,
|
||||||
|
MiPromoNote,
|
||||||
|
MiPromoRead,
|
||||||
|
MiRegistrationTicket,
|
||||||
|
MiRegistryItem,
|
||||||
|
MiRelay,
|
||||||
|
MiRenoteMuting,
|
||||||
|
MiRepository,
|
||||||
|
miRepository,
|
||||||
|
MiRetentionAggregation,
|
||||||
|
MiReversiGame,
|
||||||
|
MiRole,
|
||||||
|
MiRoleAssignment,
|
||||||
|
MiSignin,
|
||||||
|
MiSwSubscription,
|
||||||
|
MiSystemWebhook,
|
||||||
|
MiUsedUsername,
|
||||||
|
MiUser,
|
||||||
|
MiUserIp,
|
||||||
|
MiUserKeypair,
|
||||||
|
MiUserList,
|
||||||
|
MiUserListFavorite,
|
||||||
|
MiUserListMembership,
|
||||||
|
MiUserMemo,
|
||||||
|
MiUserNotePining,
|
||||||
|
MiUserPending,
|
||||||
|
MiUserProfile,
|
||||||
|
MiUserPublickey,
|
||||||
|
MiUserSecurityKey,
|
||||||
|
MiWebhook
|
||||||
|
} from './_.js';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
const $usersRepository: Provider = {
|
const $usersRepository: Provider = {
|
||||||
provide: DI.usersRepository,
|
provide: DI.usersRepository,
|
||||||
|
@ -225,6 +297,12 @@ const $abuseUserReportsRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $abuseReportNotificationRecipientRepository: Provider = {
|
||||||
|
provide: DI.abuseReportNotificationRecipientRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $registrationTicketsRepository: Provider = {
|
const $registrationTicketsRepository: Provider = {
|
||||||
provide: DI.registrationTicketsRepository,
|
provide: DI.registrationTicketsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
|
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
|
||||||
|
@ -351,6 +429,12 @@ const $webhooksRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $systemWebhooksRepository: Provider = {
|
||||||
|
provide: DI.systemWebhooksRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $adsRepository: Provider = {
|
const $adsRepository: Provider = {
|
||||||
provide: DI.adsRepository,
|
provide: DI.adsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
|
useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
|
||||||
|
@ -412,8 +496,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [],
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
$usersRepository,
|
$usersRepository,
|
||||||
$notesRepository,
|
$notesRepository,
|
||||||
|
@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
$swSubscriptionsRepository,
|
$swSubscriptionsRepository,
|
||||||
$hashtagsRepository,
|
$hashtagsRepository,
|
||||||
$abuseUserReportsRepository,
|
$abuseUserReportsRepository,
|
||||||
|
$abuseReportNotificationRecipientRepository,
|
||||||
$registrationTicketsRepository,
|
$registrationTicketsRepository,
|
||||||
$authSessionsRepository,
|
$authSessionsRepository,
|
||||||
$accessTokensRepository,
|
$accessTokensRepository,
|
||||||
|
@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
$channelFavoritesRepository,
|
$channelFavoritesRepository,
|
||||||
$registryItemsRepository,
|
$registryItemsRepository,
|
||||||
$webhooksRepository,
|
$webhooksRepository,
|
||||||
|
$systemWebhooksRepository,
|
||||||
$adsRepository,
|
$adsRepository,
|
||||||
$passwordResetRequestsRepository,
|
$passwordResetRequestsRepository,
|
||||||
$retentionAggregationsRepository,
|
$retentionAggregationsRepository,
|
||||||
|
@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
$swSubscriptionsRepository,
|
$swSubscriptionsRepository,
|
||||||
$hashtagsRepository,
|
$hashtagsRepository,
|
||||||
$abuseUserReportsRepository,
|
$abuseUserReportsRepository,
|
||||||
|
$abuseReportNotificationRecipientRepository,
|
||||||
$registrationTicketsRepository,
|
$registrationTicketsRepository,
|
||||||
$authSessionsRepository,
|
$authSessionsRepository,
|
||||||
$accessTokensRepository,
|
$accessTokensRepository,
|
||||||
|
@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
$channelFavoritesRepository,
|
$channelFavoritesRepository,
|
||||||
$registryItemsRepository,
|
$registryItemsRepository,
|
||||||
$webhooksRepository,
|
$webhooksRepository,
|
||||||
|
$systemWebhooksRepository,
|
||||||
$adsRepository,
|
$adsRepository,
|
||||||
$passwordResetRequestsRepository,
|
$passwordResetRequestsRepository,
|
||||||
$retentionAggregationsRepository,
|
$retentionAggregationsRepository,
|
||||||
|
@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
|
||||||
$reversiGamesRepository,
|
$reversiGamesRepository,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class RepositoryModule {}
|
export class RepositoryModule {
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
import { Serialized } from '@/types.js';
|
||||||
|
import { id } from './util/id.js';
|
||||||
|
|
||||||
|
export const systemWebhookEventTypes = [
|
||||||
|
// ユーザからの通報を受けたとき
|
||||||
|
'abuseReport',
|
||||||
|
// 通報を処理したとき
|
||||||
|
'abuseReportResolved',
|
||||||
|
] as const;
|
||||||
|
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
|
||||||
|
|
||||||
|
@Entity('system_webhook')
|
||||||
|
export class MiSystemWebhook {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有効かどうか.
|
||||||
|
*/
|
||||||
|
@Index('IDX_system_webhook_isActive', { synchronize: false })
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public isActive: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日時.
|
||||||
|
*/
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最後に送信された日時.
|
||||||
|
*/
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public latestSentAt: Date | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最後に送信されたステータスコード
|
||||||
|
*/
|
||||||
|
@Column('integer', {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public latestStatus: number | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知設定名.
|
||||||
|
*/
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 255,
|
||||||
|
})
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* イベント種別.
|
||||||
|
*/
|
||||||
|
@Index('IDX_system_webhook_on', { synchronize: false })
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
array: true,
|
||||||
|
default: '{}',
|
||||||
|
})
|
||||||
|
public on: SystemWebhookEventType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook送信先のURL.
|
||||||
|
*/
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
})
|
||||||
|
public url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook検証用の値.
|
||||||
|
*/
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
})
|
||||||
|
public secret: string;
|
||||||
|
|
||||||
|
static deserialize(obj: Serialized<MiSystemWebhook>): MiSystemWebhook {
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
updatedAt: new Date(obj.updatedAt),
|
||||||
|
latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transfor
|
||||||
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
|
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
|
||||||
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
|
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
|
||||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
|
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||||
import { MiAd } from '@/models/Ad.js';
|
import { MiAd } from '@/models/Ad.js';
|
||||||
import { MiAnnouncement } from '@/models/Announcement.js';
|
import { MiAnnouncement } from '@/models/Announcement.js';
|
||||||
|
@ -68,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||||
import { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||||
import { MiChannel } from '@/models/Channel.js';
|
import { MiChannel } from '@/models/Channel.js';
|
||||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||||
import { MiRole } from '@/models/Role.js';
|
import { MiRole } from '@/models/Role.js';
|
||||||
|
@ -144,6 +146,7 @@ export const miRepository = {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MiAbuseUserReport,
|
MiAbuseUserReport,
|
||||||
|
MiAbuseReportNotificationRecipient,
|
||||||
MiAccessToken,
|
MiAccessToken,
|
||||||
MiAd,
|
MiAd,
|
||||||
MiAnnouncement,
|
MiAnnouncement,
|
||||||
|
@ -201,6 +204,7 @@ export {
|
||||||
MiUserPublickey,
|
MiUserPublickey,
|
||||||
MiUserSecurityKey,
|
MiUserSecurityKey,
|
||||||
MiWebhook,
|
MiWebhook,
|
||||||
|
MiSystemWebhook,
|
||||||
MiChannel,
|
MiChannel,
|
||||||
MiRetentionAggregation,
|
MiRetentionAggregation,
|
||||||
MiRole,
|
MiRole,
|
||||||
|
@ -213,6 +217,7 @@ export {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||||
|
export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>;
|
||||||
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
|
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
|
||||||
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
|
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
|
||||||
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
|
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
|
||||||
|
@ -270,6 +275,7 @@ export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<Mi
|
||||||
export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
|
export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
|
||||||
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
|
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
|
||||||
export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
|
export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
|
||||||
|
export type SystemWebhooksRepository = Repository<MiSystemWebhook> & MiRepository<MiWebhook>;
|
||||||
export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
|
export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
|
||||||
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
|
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
|
||||||
export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
|
export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const packedAbuseReportNotificationRecipientSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
method: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['email', 'webhook'],
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
ref: 'UserLite',
|
||||||
|
},
|
||||||
|
systemWebhookId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
systemWebhook: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
ref: 'SystemWebhook',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||||
|
|
||||||
|
export const packedSystemWebhookSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
latestSentAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
latestStatus: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: systemWebhookEventTypes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
|
@ -5,13 +5,12 @@
|
||||||
|
|
||||||
// https://github.com/typeorm/typeorm/issues/2400
|
// https://github.com/typeorm/typeorm/issues/2400
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
pg.types.setTypeParser(20, Number);
|
|
||||||
|
|
||||||
import { DataSource, Logger } from 'typeorm';
|
import { DataSource, Logger } from 'typeorm';
|
||||||
import * as highlight from 'cli-highlight';
|
import * as highlight from 'cli-highlight';
|
||||||
import { entities as charts } from '@/core/chart/entities.js';
|
import { entities as charts } from '@/core/chart/entities.js';
|
||||||
|
|
||||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
|
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||||
import { MiAd } from '@/models/Ad.js';
|
import { MiAd } from '@/models/Ad.js';
|
||||||
import { MiAnnouncement } from '@/models/Announcement.js';
|
import { MiAnnouncement } from '@/models/Announcement.js';
|
||||||
|
@ -69,6 +68,7 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||||
import { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
|
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||||
import { MiChannel } from '@/models/Channel.js';
|
import { MiChannel } from '@/models/Channel.js';
|
||||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||||
import { MiRole } from '@/models/Role.js';
|
import { MiRole } from '@/models/Role.js';
|
||||||
|
@ -83,9 +83,11 @@ import { Config } from '@/config.js';
|
||||||
import MisskeyLogger from '@/logger.js';
|
import MisskeyLogger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
pg.types.setTypeParser(20, Number);
|
||||||
|
|
||||||
export const dbLogger = new MisskeyLogger('db');
|
export const dbLogger = new MisskeyLogger('db');
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
|
||||||
|
|
||||||
class MyCustomLogger implements Logger {
|
class MyCustomLogger implements Logger {
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -167,6 +169,7 @@ export const entities = [
|
||||||
MiHashtag,
|
MiHashtag,
|
||||||
MiSwSubscription,
|
MiSwSubscription,
|
||||||
MiAbuseUserReport,
|
MiAbuseUserReport,
|
||||||
|
MiAbuseReportNotificationRecipient,
|
||||||
MiRegistrationTicket,
|
MiRegistrationTicket,
|
||||||
MiSignin,
|
MiSignin,
|
||||||
MiModerationLog,
|
MiModerationLog,
|
||||||
|
@ -185,6 +188,7 @@ export const entities = [
|
||||||
MiPasswordResetRequest,
|
MiPasswordResetRequest,
|
||||||
MiUserPending,
|
MiUserPending,
|
||||||
MiWebhook,
|
MiWebhook,
|
||||||
|
MiSystemWebhook,
|
||||||
MiUserIp,
|
MiUserIp,
|
||||||
MiRetentionAggregation,
|
MiRetentionAggregation,
|
||||||
MiRole,
|
MiRole,
|
||||||
|
|
|
@ -11,7 +11,8 @@ import { QueueProcessorService } from './QueueProcessorService.js';
|
||||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
|
@ -71,7 +72,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
DeleteFileProcessorService,
|
DeleteFileProcessorService,
|
||||||
CleanRemoteFilesProcessorService,
|
CleanRemoteFilesProcessorService,
|
||||||
RelationshipProcessorService,
|
RelationshipProcessorService,
|
||||||
WebhookDeliverProcessorService,
|
UserWebhookDeliverProcessorService,
|
||||||
|
SystemWebhookDeliverProcessorService,
|
||||||
EndedPollNotificationProcessorService,
|
EndedPollNotificationProcessorService,
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
InboxProcessorService,
|
InboxProcessorService,
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
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 type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||||
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
|
@ -75,7 +77,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private dbQueueWorker: Bull.Worker;
|
private dbQueueWorker: Bull.Worker;
|
||||||
private deliverQueueWorker: Bull.Worker;
|
private deliverQueueWorker: Bull.Worker;
|
||||||
private inboxQueueWorker: Bull.Worker;
|
private inboxQueueWorker: Bull.Worker;
|
||||||
private webhookDeliverQueueWorker: Bull.Worker;
|
private userWebhookDeliverQueueWorker: Bull.Worker;
|
||||||
|
private systemWebhookDeliverQueueWorker: Bull.Worker;
|
||||||
private relationshipQueueWorker: Bull.Worker;
|
private relationshipQueueWorker: Bull.Worker;
|
||||||
private objectStorageQueueWorker: Bull.Worker;
|
private objectStorageQueueWorker: Bull.Worker;
|
||||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||||
|
@ -85,7 +88,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
|
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||||
|
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||||
private deliverProcessorService: DeliverProcessorService,
|
private deliverProcessorService: DeliverProcessorService,
|
||||||
private inboxProcessorService: InboxProcessorService,
|
private inboxProcessorService: InboxProcessorService,
|
||||||
|
@ -135,7 +139,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region system
|
//#region system
|
||||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
{
|
||||||
|
const processer = (job: Bull.Job) => {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case 'tickCharts': return this.tickChartsProcessorService.process();
|
case 'tickCharts': return this.tickChartsProcessorService.process();
|
||||||
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
||||||
|
@ -145,23 +150,41 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'clean': return this.cleanProcessorService.process();
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
|
||||||
|
} else {
|
||||||
|
return processer(job);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.SYSTEM),
|
...baseQueueOptions(this.config, QUEUE.SYSTEM),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const systemLogger = this.logger.createSubLogger('system');
|
const logger = this.logger.createSubLogger('system');
|
||||||
|
|
||||||
this.systemQueueWorker
|
this.systemQueueWorker
|
||||||
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
.on('failed', (job, err: Error) => {
|
||||||
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||||
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region db
|
//#region db
|
||||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
{
|
||||||
|
const processer = (job: Bull.Job) => {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||||
|
@ -184,23 +207,47 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
|
||||||
|
} else {
|
||||||
|
return processer(job);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.DB),
|
...baseQueueOptions(this.config, QUEUE.DB),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbLogger = this.logger.createSubLogger('db');
|
const logger = this.logger.createSubLogger('db');
|
||||||
|
|
||||||
this.dbQueueWorker
|
this.dbQueueWorker
|
||||||
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
.on('failed', (job, err) => {
|
||||||
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||||
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region deliver
|
//#region deliver
|
||||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
|
{
|
||||||
|
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
|
||||||
|
} else {
|
||||||
|
return this.deliverProcessorService.process(job);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.DELIVER),
|
...baseQueueOptions(this.config, QUEUE.DELIVER),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
concurrency: this.config.deliverJobConcurrency ?? 128,
|
concurrency: this.config.deliverJobConcurrency ?? 128,
|
||||||
|
@ -213,18 +260,34 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const deliverLogger = this.logger.createSubLogger('deliver');
|
const logger = this.logger.createSubLogger('deliver');
|
||||||
|
|
||||||
this.deliverQueueWorker
|
this.deliverQueueWorker
|
||||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
|
.on('failed', (job, err) => {
|
||||||
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||||
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region inbox
|
//#region inbox
|
||||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
|
{
|
||||||
|
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
|
||||||
|
} else {
|
||||||
|
return this.inboxProcessorService.process(job);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.INBOX),
|
...baseQueueOptions(this.config, QUEUE.INBOX),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
concurrency: this.config.inboxJobConcurrency ?? 16,
|
concurrency: this.config.inboxJobConcurrency ?? 16,
|
||||||
|
@ -237,19 +300,35 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const inboxLogger = this.logger.createSubLogger('inbox');
|
const logger = this.logger.createSubLogger('inbox');
|
||||||
|
|
||||||
this.inboxQueueWorker
|
this.inboxQueueWorker
|
||||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
|
||||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
|
.on('failed', (job, err) => {
|
||||||
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
|
||||||
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region webhook deliver
|
//#region user-webhook deliver
|
||||||
this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
|
{
|
||||||
...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
|
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
|
||||||
|
} else {
|
||||||
|
return this.userWebhookDeliverProcessorService.process(job);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
concurrency: 64,
|
concurrency: 64,
|
||||||
limiter: {
|
limiter: {
|
||||||
|
@ -261,18 +340,68 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const webhookLogger = this.logger.createSubLogger('webhook');
|
const logger = this.logger.createSubLogger('user-webhook');
|
||||||
|
|
||||||
this.webhookDeliverQueueWorker
|
this.userWebhookDeliverQueueWorker
|
||||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
|
.on('failed', (job, err) => {
|
||||||
.on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||||
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region system-webhook deliver
|
||||||
|
{
|
||||||
|
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
|
||||||
|
} else {
|
||||||
|
return this.systemWebhookDeliverProcessorService.process(job);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
|
||||||
|
autorun: false,
|
||||||
|
concurrency: 16,
|
||||||
|
limiter: {
|
||||||
|
max: 16,
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
backoffStrategy: httpRelatedBackoff,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = this.logger.createSubLogger('system-webhook');
|
||||||
|
|
||||||
|
this.systemWebhookDeliverQueueWorker
|
||||||
|
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
|
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
|
.on('failed', (job, err) => {
|
||||||
|
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||||
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region relationship
|
//#region relationship
|
||||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
{
|
||||||
|
const processer = (job: Bull.Job) => {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case 'follow': return this.relationshipProcessorService.processFollow(job);
|
case 'follow': return this.relationshipProcessorService.processFollow(job);
|
||||||
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
|
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
|
||||||
|
@ -280,6 +409,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
|
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
|
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
|
||||||
|
} else {
|
||||||
|
return processer(job);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
|
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
|
@ -290,44 +427,79 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const relationshipLogger = this.logger.createSubLogger('relationship');
|
const logger = this.logger.createSubLogger('relationship');
|
||||||
|
|
||||||
this.relationshipQueueWorker
|
this.relationshipQueueWorker
|
||||||
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
.on('failed', (job, err) => {
|
||||||
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||||
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region object storage
|
//#region object storage
|
||||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
{
|
||||||
|
const processer = (job: Bull.Job) => {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case 'deleteFile': return this.deleteFileProcessorService.process(job);
|
case 'deleteFile': return this.deleteFileProcessorService.process(job);
|
||||||
case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
|
case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
|
default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
|
||||||
|
} else {
|
||||||
|
return processer(job);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
|
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
concurrency: 16,
|
concurrency: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
const logger = this.logger.createSubLogger('objectStorage');
|
||||||
|
|
||||||
this.objectStorageQueueWorker
|
this.objectStorageQueueWorker
|
||||||
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
.on('failed', (job, err) => {
|
||||||
.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
|
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||||
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
|
if (config.sentryForBackend) {
|
||||||
|
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
extra: { job, err },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region ended poll notification
|
//#region ended poll notification
|
||||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
|
{
|
||||||
|
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
|
||||||
|
} else {
|
||||||
|
return this.endedPollNotificationProcessorService.process(job);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +510,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
this.dbQueueWorker.run(),
|
this.dbQueueWorker.run(),
|
||||||
this.deliverQueueWorker.run(),
|
this.deliverQueueWorker.run(),
|
||||||
this.inboxQueueWorker.run(),
|
this.inboxQueueWorker.run(),
|
||||||
this.webhookDeliverQueueWorker.run(),
|
this.userWebhookDeliverQueueWorker.run(),
|
||||||
|
this.systemWebhookDeliverQueueWorker.run(),
|
||||||
this.relationshipQueueWorker.run(),
|
this.relationshipQueueWorker.run(),
|
||||||
this.objectStorageQueueWorker.run(),
|
this.objectStorageQueueWorker.run(),
|
||||||
this.endedPollNotificationQueueWorker.run(),
|
this.endedPollNotificationQueueWorker.run(),
|
||||||
|
@ -352,7 +525,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
this.dbQueueWorker.close(),
|
this.dbQueueWorker.close(),
|
||||||
this.deliverQueueWorker.close(),
|
this.deliverQueueWorker.close(),
|
||||||
this.inboxQueueWorker.close(),
|
this.inboxQueueWorker.close(),
|
||||||
this.webhookDeliverQueueWorker.close(),
|
this.userWebhookDeliverQueueWorker.close(),
|
||||||
|
this.systemWebhookDeliverQueueWorker.close(),
|
||||||
this.relationshipQueueWorker.close(),
|
this.relationshipQueueWorker.close(),
|
||||||
this.objectStorageQueueWorker.close(),
|
this.objectStorageQueueWorker.close(),
|
||||||
this.endedPollNotificationQueueWorker.close(),
|
this.endedPollNotificationQueueWorker.close(),
|
||||||
|
|
|
@ -14,7 +14,8 @@ export const QUEUE = {
|
||||||
DB: 'db',
|
DB: 'db',
|
||||||
RELATIONSHIP: 'relationship',
|
RELATIONSHIP: 'relationship',
|
||||||
OBJECT_STORAGE: 'objectStorage',
|
OBJECT_STORAGE: 'objectStorage',
|
||||||
WEBHOOK_DELIVER: 'webhookDeliver',
|
USER_WEBHOOK_DELIVER: 'userWebhookDeliver',
|
||||||
|
SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { SystemWebhooksRepository } from '@/models/_.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import { SystemWebhookDeliverJobData } from '../types.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemWebhookDeliverProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.systemWebhooksRepository)
|
||||||
|
private systemWebhooksRepository: SystemWebhooksRepository,
|
||||||
|
|
||||||
|
private httpRequestService: HttpRequestService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<SystemWebhookDeliverJobData>): Promise<string> {
|
||||||
|
try {
|
||||||
|
this.logger.debug(`delivering ${job.data.webhookId}`);
|
||||||
|
|
||||||
|
const res = await this.httpRequestService.send(job.data.to, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Misskey-Hooks',
|
||||||
|
'X-Misskey-Host': this.config.host,
|
||||||
|
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||||
|
'X-Misskey-Hook-Secret': job.data.secret,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
server: this.config.url,
|
||||||
|
hookId: job.data.webhookId,
|
||||||
|
eventId: job.data.eventId,
|
||||||
|
createdAt: job.data.createdAt,
|
||||||
|
type: job.data.type,
|
||||||
|
body: job.data.content,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
|
||||||
|
latestSentAt: new Date(),
|
||||||
|
latestStatus: res.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return 'Success';
|
||||||
|
} catch (res) {
|
||||||
|
this.logger.error(res as Error);
|
||||||
|
|
||||||
|
this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
|
||||||
|
latestSentAt: new Date(),
|
||||||
|
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res instanceof StatusError) {
|
||||||
|
// 4xx
|
||||||
|
if (!res.isRetryable) {
|
||||||
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5xx etc.
|
||||||
|
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||||
|
} else {
|
||||||
|
// DNS error, socket error, timeout ...
|
||||||
|
throw res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,10 +13,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { WebhookDeliverJobData } from '../types.js';
|
import { UserWebhookDeliverJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebhookDeliverProcessorService {
|
export class UserWebhookDeliverProcessorService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -33,7 +33,7 @@ export class WebhookDeliverProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> {
|
public async process(job: Bull.Job<UserWebhookDeliverJobData>): Promise<string> {
|
||||||
try {
|
try {
|
||||||
this.logger.debug(`delivering ${job.data.webhookId}`);
|
this.logger.debug(`delivering ${job.data.webhookId}`);
|
||||||
|
|
|
@ -106,7 +106,17 @@ export type EndedPollNotificationJobData = {
|
||||||
noteId: MiNote['id'];
|
noteId: MiNote['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebhookDeliverJobData = {
|
export type SystemWebhookDeliverJobData = {
|
||||||
|
type: string;
|
||||||
|
content: unknown;
|
||||||
|
webhookId: MiWebhook['id'];
|
||||||
|
to: string;
|
||||||
|
secret: string;
|
||||||
|
createdAt: number;
|
||||||
|
eventId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserWebhookDeliverJobData = {
|
||||||
type: string;
|
type: string;
|
||||||
content: unknown;
|
content: unknown;
|
||||||
webhookId: MiWebhook['id'];
|
webhookId: MiWebhook['id'];
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class FileServerService {
|
||||||
private internalStorageService: InternalStorageService,
|
private internalStorageService: InternalStorageService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('server', 'gray', false);
|
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||||
|
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private oauth2ProviderService: OAuth2ProviderService,
|
private oauth2ProviderService: OAuth2ProviderService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('server', 'gray', false);
|
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -93,7 +93,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#onExecError(ep: IEndpoint, data: any, err: Error): void {
|
#onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void {
|
||||||
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
||||||
throw err;
|
throw err;
|
||||||
} else {
|
} else {
|
||||||
|
@ -108,10 +108,13 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
id: errId,
|
id: errId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.error(err, errId);
|
|
||||||
|
|
||||||
if (this.config.sentryForBackend) {
|
if (this.config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||||
|
level: 'error',
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
extra: {
|
extra: {
|
||||||
ep: ep.name,
|
ep: ep.name,
|
||||||
ps: data,
|
ps: data,
|
||||||
|
@ -410,9 +413,13 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// API invoking
|
// API invoking
|
||||||
if (this.config.sentryForBackend) {
|
if (this.config.sentryForBackend) {
|
||||||
return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
|
return await Sentry.startSpan({
|
||||||
|
name: 'API: ' + ep.name,
|
||||||
|
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||||
|
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||||
} else {
|
} else {
|
||||||
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
|
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
||||||
|
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,13 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
|
||||||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||||
|
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||||
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
||||||
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
||||||
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
|
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
|
||||||
|
@ -82,6 +87,11 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
|
||||||
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
|
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
|
||||||
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
||||||
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
||||||
|
import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
|
||||||
|
import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
|
||||||
|
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
||||||
|
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
||||||
|
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
|
@ -381,6 +391,11 @@ import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
|
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
|
||||||
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
|
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
|
||||||
|
const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default };
|
||||||
|
const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default };
|
||||||
|
const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default };
|
||||||
|
const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default };
|
||||||
|
const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default };
|
||||||
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
|
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
|
||||||
const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
|
const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
|
||||||
const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
|
const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
|
||||||
|
@ -455,6 +470,11 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla
|
||||||
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
|
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
|
||||||
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
|
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
|
||||||
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
|
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
|
||||||
|
const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default };
|
||||||
|
const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default };
|
||||||
|
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
|
||||||
|
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
|
||||||
|
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
|
||||||
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||||
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
|
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
|
||||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
||||||
|
@ -758,6 +778,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
ApiLoggerService,
|
ApiLoggerService,
|
||||||
$admin_meta,
|
$admin_meta,
|
||||||
$admin_abuseUserReports,
|
$admin_abuseUserReports,
|
||||||
|
$admin_abuseReport_notificationRecipient_list,
|
||||||
|
$admin_abuseReport_notificationRecipient_show,
|
||||||
|
$admin_abuseReport_notificationRecipient_create,
|
||||||
|
$admin_abuseReport_notificationRecipient_update,
|
||||||
|
$admin_abuseReport_notificationRecipient_delete,
|
||||||
$admin_accounts_create,
|
$admin_accounts_create,
|
||||||
$admin_accounts_delete,
|
$admin_accounts_delete,
|
||||||
$admin_accounts_findByEmail,
|
$admin_accounts_findByEmail,
|
||||||
|
@ -832,6 +857,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_roles_unassign,
|
$admin_roles_unassign,
|
||||||
$admin_roles_updateDefaultPolicies,
|
$admin_roles_updateDefaultPolicies,
|
||||||
$admin_roles_users,
|
$admin_roles_users,
|
||||||
|
$admin_systemWebhook_create,
|
||||||
|
$admin_systemWebhook_delete,
|
||||||
|
$admin_systemWebhook_list,
|
||||||
|
$admin_systemWebhook_show,
|
||||||
|
$admin_systemWebhook_update,
|
||||||
$announcements,
|
$announcements,
|
||||||
$announcements_show,
|
$announcements_show,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
|
@ -1129,6 +1159,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
exports: [
|
exports: [
|
||||||
$admin_meta,
|
$admin_meta,
|
||||||
$admin_abuseUserReports,
|
$admin_abuseUserReports,
|
||||||
|
$admin_abuseReport_notificationRecipient_list,
|
||||||
|
$admin_abuseReport_notificationRecipient_show,
|
||||||
|
$admin_abuseReport_notificationRecipient_create,
|
||||||
|
$admin_abuseReport_notificationRecipient_update,
|
||||||
|
$admin_abuseReport_notificationRecipient_delete,
|
||||||
$admin_accounts_create,
|
$admin_accounts_create,
|
||||||
$admin_accounts_delete,
|
$admin_accounts_delete,
|
||||||
$admin_accounts_findByEmail,
|
$admin_accounts_findByEmail,
|
||||||
|
@ -1203,6 +1238,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_roles_unassign,
|
$admin_roles_unassign,
|
||||||
$admin_roles_updateDefaultPolicies,
|
$admin_roles_updateDefaultPolicies,
|
||||||
$admin_roles_users,
|
$admin_roles_users,
|
||||||
|
$admin_systemWebhook_create,
|
||||||
|
$admin_systemWebhook_delete,
|
||||||
|
$admin_systemWebhook_list,
|
||||||
|
$admin_systemWebhook_show,
|
||||||
|
$admin_systemWebhook_update,
|
||||||
$announcements,
|
$announcements,
|
||||||
$announcements_show,
|
$announcements_show,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
|
|
|
@ -6,8 +6,18 @@
|
||||||
import { permissions } from 'misskey-js';
|
import { permissions } from 'misskey-js';
|
||||||
import type { KeyOf, Schema } from '@/misc/json-schema.js';
|
import type { KeyOf, Schema } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
import * as ep___admin_abuseReport_notificationRecipient_list
|
||||||
|
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_show
|
||||||
|
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_create
|
||||||
|
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_update
|
||||||
|
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
|
||||||
|
import * as ep___admin_abuseReport_notificationRecipient_delete
|
||||||
|
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
|
||||||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||||
|
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||||
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
||||||
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
||||||
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
|
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
|
||||||
|
@ -44,7 +54,8 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c
|
||||||
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
||||||
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
||||||
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
||||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
import * as ep___admin_federation_refreshRemoteInstanceMetadata
|
||||||
|
from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||||
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
||||||
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
|
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
|
||||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||||
|
@ -82,6 +93,11 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
|
||||||
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
|
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
|
||||||
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
|
||||||
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
|
||||||
|
import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
|
||||||
|
import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
|
||||||
|
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
||||||
|
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
||||||
|
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
|
@ -379,6 +395,11 @@ import * as ep___reversi_verify from './endpoints/reversi/verify.js';
|
||||||
const eps = [
|
const eps = [
|
||||||
['admin/meta', ep___admin_meta],
|
['admin/meta', ep___admin_meta],
|
||||||
['admin/abuse-user-reports', ep___admin_abuseUserReports],
|
['admin/abuse-user-reports', ep___admin_abuseUserReports],
|
||||||
|
['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
|
||||||
|
['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
|
||||||
|
['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
|
||||||
|
['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
|
||||||
|
['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
|
||||||
['admin/accounts/create', ep___admin_accounts_create],
|
['admin/accounts/create', ep___admin_accounts_create],
|
||||||
['admin/accounts/delete', ep___admin_accounts_delete],
|
['admin/accounts/delete', ep___admin_accounts_delete],
|
||||||
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
|
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
|
||||||
|
@ -453,6 +474,11 @@ const eps = [
|
||||||
['admin/roles/unassign', ep___admin_roles_unassign],
|
['admin/roles/unassign', ep___admin_roles_unassign],
|
||||||
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
|
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
|
||||||
['admin/roles/users', ep___admin_roles_users],
|
['admin/roles/users', ep___admin_roles_users],
|
||||||
|
['admin/system-webhook/create', ep___admin_systemWebhook_create],
|
||||||
|
['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
|
||||||
|
['admin/system-webhook/list', ep___admin_systemWebhook_list],
|
||||||
|
['admin/system-webhook/show', ep___admin_systemWebhook_show],
|
||||||
|
['admin/system-webhook/update', ep___admin_systemWebhook_update],
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['announcements/show', ep___announcements_show],
|
['announcements/show', ep___announcements_show],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
|
@ -873,8 +899,12 @@ export interface IEndpoint {
|
||||||
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
|
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
get meta() { return ep.meta ?? {}; },
|
get meta() {
|
||||||
get params() { return ep.paramDef; },
|
return ep.meta ?? {};
|
||||||
|
},
|
||||||
|
get params() {
|
||||||
|
return ep.paramDef;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import {
|
||||||
|
AbuseReportNotificationRecipientEntityService,
|
||||||
|
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { UserProfilesRepository } from '@/models/_.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'abuse-report', 'notification-recipient'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:abuse-report:notification-recipient',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'AbuseReportNotificationRecipient',
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
correlationCheckEmail: {
|
||||||
|
message: 'If "method" is email, "userId" must be set.',
|
||||||
|
code: 'CORRELATION_CHECK_EMAIL',
|
||||||
|
id: '348bb8ae-575a-6fe9-4327-5811999def8f',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
correlationCheckWebhook: {
|
||||||
|
message: 'If "method" is webhook, "systemWebhookId" must be set.',
|
||||||
|
code: 'CORRELATION_CHECK_WEBHOOK',
|
||||||
|
id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
emailAddressNotSet: {
|
||||||
|
message: 'Email address is not set.',
|
||||||
|
code: 'EMAIL_ADDRESS_NOT_SET',
|
||||||
|
id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 255,
|
||||||
|
},
|
||||||
|
method: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['email', 'webhook'],
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
systemWebhookId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'isActive',
|
||||||
|
'name',
|
||||||
|
'method',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
if (ps.method === 'email') {
|
||||||
|
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
|
||||||
|
if (!ps.userId || !userProfile) {
|
||||||
|
throw new ApiError(meta.errors.correlationCheckEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userProfile.email || !userProfile.emailVerified) {
|
||||||
|
throw new ApiError(meta.errors.emailAddressNotSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.method === 'webhook' && !ps.systemWebhookId) {
|
||||||
|
throw new ApiError(meta.errors.correlationCheckWebhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = ps.method === 'email' ? ps.userId : null;
|
||||||
|
const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
|
||||||
|
const result = await this.abuseReportNotificationService.createRecipient(
|
||||||
|
{
|
||||||
|
isActive: ps.isActive,
|
||||||
|
name: ps.name,
|
||||||
|
method: ps.method,
|
||||||
|
userId: userId ?? null,
|
||||||
|
systemWebhookId: systemWebhookId ?? null,
|
||||||
|
},
|
||||||
|
me,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.abuseReportNotificationRecipientEntityService.pack(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'abuse-report', 'notification-recipient'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:abuse-report:notification-recipient',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'id',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
await this.abuseReportNotificationService.deleteRecipient(
|
||||||
|
ps.id,
|
||||||
|
me,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import {
|
||||||
|
AbuseReportNotificationRecipientEntityService,
|
||||||
|
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'abuse-report', 'notification-recipient'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'read:admin:abuse-report:notification-recipient',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'AbuseReportNotificationRecipient',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
method: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['email', 'webhook'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps) => {
|
||||||
|
const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method });
|
||||||
|
return this.abuseReportNotificationRecipientEntityService.packMany(recipients);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import {
|
||||||
|
AbuseReportNotificationRecipientEntityService,
|
||||||
|
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'abuse-report', 'notification-recipient'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'read:admin:abuse-report:notification-recipient',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'AbuseReportNotificationRecipient',
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchRecipient: {
|
||||||
|
message: 'No such recipient.',
|
||||||
|
code: 'NO_SUCH_RECIPIENT',
|
||||||
|
id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4',
|
||||||
|
kind: 'server',
|
||||||
|
httpStatusCode: 404,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps) => {
|
||||||
|
const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] });
|
||||||
|
if (recipients.length === 0) {
|
||||||
|
throw new ApiError(meta.errors.noSuchRecipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import {
|
||||||
|
AbuseReportNotificationRecipientEntityService,
|
||||||
|
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { UserProfilesRepository } from '@/models/_.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'abuse-report', 'notification-recipient'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:abuse-report:notification-recipient',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'AbuseReportNotificationRecipient',
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
correlationCheckEmail: {
|
||||||
|
message: 'If "method" is email, "userId" must be set.',
|
||||||
|
code: 'CORRELATION_CHECK_EMAIL',
|
||||||
|
id: '348bb8ae-575a-6fe9-4327-5811999def8f',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
correlationCheckWebhook: {
|
||||||
|
message: 'If "method" is webhook, "systemWebhookId" must be set.',
|
||||||
|
code: 'CORRELATION_CHECK_WEBHOOK',
|
||||||
|
id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
emailAddressNotSet: {
|
||||||
|
message: 'Email address is not set.',
|
||||||
|
code: 'EMAIL_ADDRESS_NOT_SET',
|
||||||
|
id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 255,
|
||||||
|
},
|
||||||
|
method: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['email', 'webhook'],
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
systemWebhookId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'id',
|
||||||
|
'isActive',
|
||||||
|
'name',
|
||||||
|
'method',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||||
|
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
if (ps.method === 'email') {
|
||||||
|
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
|
||||||
|
if (!ps.userId || !userProfile) {
|
||||||
|
throw new ApiError(meta.errors.correlationCheckEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userProfile.email || !userProfile.emailVerified) {
|
||||||
|
throw new ApiError(meta.errors.emailAddressNotSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.method === 'webhook' && !ps.systemWebhookId) {
|
||||||
|
throw new ApiError(meta.errors.correlationCheckWebhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = ps.method === 'email' ? ps.userId : null;
|
||||||
|
const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
|
||||||
|
const result = await this.abuseReportNotificationService.updateRecipient(
|
||||||
|
{
|
||||||
|
id: ps.id,
|
||||||
|
isActive: ps.isActive,
|
||||||
|
name: ps.name,
|
||||||
|
method: ps.method,
|
||||||
|
userId: userId ?? null,
|
||||||
|
systemWebhookId: systemWebhookId ?? null,
|
||||||
|
},
|
||||||
|
me,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.abuseReportNotificationRecipientEntityService.pack(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
|
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -53,7 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@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,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const deliverJobCounts = await this.deliverQueue.getJobCounts();
|
const deliverJobCounts = await this.deliverQueue.getJobCounts();
|
||||||
|
|
|
@ -5,12 +5,10 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js';
|
import type { AbuseUserReportsRepository } from '@/models/_.js';
|
||||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -18,6 +16,16 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
kind: 'write:admin:resolve-abuse-user-report',
|
kind: 'write:admin:resolve-abuse-user-report',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchAbuseReport: {
|
||||||
|
message: 'No such abuse report.',
|
||||||
|
code: 'NO_SUCH_ABUSE_REPORT',
|
||||||
|
id: 'ac3794dd-2ce4-d878-e546-73c60c06b398',
|
||||||
|
kind: 'server',
|
||||||
|
httpStatusCode: 404,
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -29,47 +37,20 @@ export const paramDef = {
|
||||||
required: ['reportId'],
|
required: ['reportId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.abuseUserReportsRepository)
|
@Inject(DI.abuseUserReportsRepository)
|
||||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||||
|
private abuseReportService: AbuseReportService,
|
||||||
private queueService: QueueService,
|
|
||||||
private instanceActorService: InstanceActorService,
|
|
||||||
private apRendererService: ApRendererService,
|
|
||||||
private moderationLogService: ModerationLogService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
|
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
|
||||||
|
if (!report) {
|
||||||
if (report == null) {
|
throw new ApiError(meta.errors.noSuchAbuseReport);
|
||||||
throw new Error('report not found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.forward && report.targetUserHost != null) {
|
await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
|
||||||
const actor = await this.instanceActorService.getInstanceActor();
|
|
||||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
|
||||||
|
|
||||||
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.abuseUserReportsRepository.update(report.id, {
|
|
||||||
resolved: true,
|
|
||||||
assigneeId: me.id,
|
|
||||||
forwarded: ps.forward && report.targetUserHost != null,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.moderationLogService.log(me, 'resolveAbuseReport', {
|
|
||||||
reportId: report.id,
|
|
||||||
report: report,
|
|
||||||
forwarded: ps.forward && report.targetUserHost != null,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||||
|
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'system-webhook'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:system-webhook',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'SystemWebhook',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 255,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: systemWebhookEventTypes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 1024,
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'isActive',
|
||||||
|
'name',
|
||||||
|
'on',
|
||||||
|
'url',
|
||||||
|
'secret',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private systemWebhookEntityService: SystemWebhookEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const result = await this.systemWebhookService.createSystemWebhook(
|
||||||
|
{
|
||||||
|
isActive: ps.isActive,
|
||||||
|
name: ps.name,
|
||||||
|
on: ps.on,
|
||||||
|
url: ps.url,
|
||||||
|
secret: ps.secret,
|
||||||
|
},
|
||||||
|
me,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.systemWebhookEntityService.pack(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'system-webhook'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:system-webhook',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'id',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
await this.systemWebhookService.deleteSystemWebhook(
|
||||||
|
ps.id,
|
||||||
|
me,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||||
|
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'system-webhook'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:system-webhook',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'SystemWebhook',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: systemWebhookEventTypes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private systemWebhookEntityService: SystemWebhookEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps) => {
|
||||||
|
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({
|
||||||
|
isActive: ps.isActive,
|
||||||
|
on: ps.on,
|
||||||
|
});
|
||||||
|
return this.systemWebhookEntityService.packMany(webhooks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'system-webhook'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:system-webhook',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'SystemWebhook',
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchSystemWebhook: {
|
||||||
|
message: 'No such SystemWebhook.',
|
||||||
|
code: 'NO_SUCH_SYSTEM_WEBHOOK',
|
||||||
|
id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d',
|
||||||
|
kind: 'server',
|
||||||
|
httpStatusCode: 404,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private systemWebhookEntityService: SystemWebhookEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps) => {
|
||||||
|
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] });
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
throw new ApiError(meta.errors.noSuchSystemWebhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.systemWebhookEntityService.pack(webhooks[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||||
|
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin', 'system-webhook'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
secure: true,
|
||||||
|
kind: 'write:admin:system-webhook',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'SystemWebhook',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'misskey:id',
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 255,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: systemWebhookEventTypes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 1024,
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'id',
|
||||||
|
'isActive',
|
||||||
|
'name',
|
||||||
|
'on',
|
||||||
|
'url',
|
||||||
|
'secret',
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private systemWebhookService: SystemWebhookService,
|
||||||
|
private systemWebhookEntityService: SystemWebhookEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const result = await this.systemWebhookService.updateSystemWebhook(
|
||||||
|
{
|
||||||
|
id: ps.id,
|
||||||
|
isActive: ps.isActive,
|
||||||
|
name: ps.name,
|
||||||
|
on: ps.on,
|
||||||
|
url: ps.url,
|
||||||
|
secret: ps.secret,
|
||||||
|
},
|
||||||
|
me,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.systemWebhookEntityService.pack(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
if (typeof ps.suspended === 'boolean') {
|
if (typeof ps.suspended === 'boolean') {
|
||||||
if (ps.suspended) {
|
if (ps.suspended) {
|
||||||
query.andWhere('instance.isSuspended = TRUE');
|
query.andWhere('instance.suspensionState != \'none\'');
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('instance.isSuspended = FALSE');
|
query.andWhere('instance.suspensionState = \'none\'');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { AbuseUserReportsRepository } from '@/models/_.js';
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { EmailService } from '@/core/EmailService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -57,60 +51,32 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.abuseUserReportsRepository)
|
|
||||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
|
||||||
|
|
||||||
private idService: IdService,
|
|
||||||
private metaService: MetaService,
|
|
||||||
private emailService: EmailService,
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private globalEventService: GlobalEventService,
|
private abuseReportService: AbuseReportService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Lookup user
|
// Lookup user
|
||||||
const user = await this.getterService.getUser(ps.userId).catch(err => {
|
const targetUser = await this.getterService.getUser(ps.userId).catch(err => {
|
||||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user.id === me.id) {
|
if (targetUser.id === me.id) {
|
||||||
throw new ApiError(meta.errors.cannotReportYourself);
|
throw new ApiError(meta.errors.cannotReportYourself);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.roleService.isAdministrator(user)) {
|
if (await this.roleService.isAdministrator(targetUser)) {
|
||||||
throw new ApiError(meta.errors.cannotReportAdmin);
|
throw new ApiError(meta.errors.cannotReportAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
const report = await this.abuseUserReportsRepository.insertOne({
|
await this.abuseReportService.report([{
|
||||||
id: this.idService.gen(),
|
targetUserId: targetUser.id,
|
||||||
targetUserId: user.id,
|
targetUserHost: targetUser.host,
|
||||||
targetUserHost: user.host,
|
|
||||||
reporterId: me.id,
|
reporterId: me.id,
|
||||||
reporterHost: null,
|
reporterHost: null,
|
||||||
comment: ps.comment,
|
comment: ps.comment,
|
||||||
});
|
}]);
|
||||||
|
|
||||||
// Publish event to moderators
|
|
||||||
setImmediate(async () => {
|
|
||||||
const moderators = await this.roleService.getModerators();
|
|
||||||
|
|
||||||
for (const moderator of moderators) {
|
|
||||||
this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
|
|
||||||
id: report.id,
|
|
||||||
targetUserId: report.targetUserId,
|
|
||||||
reporterId: report.reporterId,
|
|
||||||
comment: report.comment,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
if (meta.email) {
|
|
||||||
this.emailService.sendEmail(meta.email, 'New abuse report',
|
|
||||||
sanitizeHtml(ps.comment),
|
|
||||||
sanitizeHtml(ps.comment));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
|
import type {
|
||||||
|
DbQueue,
|
||||||
|
DeliverQueue,
|
||||||
|
EndedPollNotificationQueue,
|
||||||
|
InboxQueue,
|
||||||
|
ObjectStorageQueue,
|
||||||
|
SystemQueue,
|
||||||
|
UserWebhookDeliverQueue,
|
||||||
|
SystemWebhookDeliverQueue,
|
||||||
|
} from '@/core/QueueModule.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||||
|
@ -111,7 +120,8 @@ export class ClientServerService {
|
||||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||||
@Inject('queue:db') public dbQueue: DbQueue,
|
@Inject('queue:db') public dbQueue: DbQueue,
|
||||||
@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.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -239,7 +249,8 @@ export class ClientServerService {
|
||||||
this.inboxQueue,
|
this.inboxQueue,
|
||||||
this.dbQueue,
|
this.dbQueue,
|
||||||
this.objectStorageQueue,
|
this.objectStorageQueue,
|
||||||
this.webhookDeliverQueue,
|
this.userWebhookDeliverQueue,
|
||||||
|
this.systemWebhookDeliverQueue,
|
||||||
].map(q => new BullMQAdapter(q)),
|
].map(q => new BullMQAdapter(q)),
|
||||||
serverAdapter,
|
serverAdapter,
|
||||||
});
|
});
|
||||||
|
|
|
@ -90,6 +90,12 @@ export const moderationLogTypes = [
|
||||||
'deleteAvatarDecoration',
|
'deleteAvatarDecoration',
|
||||||
'unsetUserAvatar',
|
'unsetUserAvatar',
|
||||||
'unsetUserBanner',
|
'unsetUserBanner',
|
||||||
|
'createSystemWebhook',
|
||||||
|
'updateSystemWebhook',
|
||||||
|
'deleteSystemWebhook',
|
||||||
|
'createAbuseReportNotificationRecipient',
|
||||||
|
'updateAbuseReportNotificationRecipient',
|
||||||
|
'deleteAbuseReportNotificationRecipient',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -282,6 +288,32 @@ export type ModerationLogPayloads = {
|
||||||
userHost: string | null;
|
userHost: string | null;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
};
|
};
|
||||||
|
createSystemWebhook: {
|
||||||
|
systemWebhookId: string;
|
||||||
|
webhook: any;
|
||||||
|
};
|
||||||
|
updateSystemWebhook: {
|
||||||
|
systemWebhookId: string;
|
||||||
|
before: any;
|
||||||
|
after: any;
|
||||||
|
};
|
||||||
|
deleteSystemWebhook: {
|
||||||
|
systemWebhookId: string;
|
||||||
|
webhook: any;
|
||||||
|
};
|
||||||
|
createAbuseReportNotificationRecipient: {
|
||||||
|
recipientId: string;
|
||||||
|
recipient: any;
|
||||||
|
};
|
||||||
|
updateAbuseReportNotificationRecipient: {
|
||||||
|
recipientId: string;
|
||||||
|
before: any;
|
||||||
|
after: any;
|
||||||
|
};
|
||||||
|
deleteAbuseReportNotificationRecipient: {
|
||||||
|
recipientId: string;
|
||||||
|
recipient: any;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Serialized<T> = {
|
export type Serialized<T> = {
|
||||||
|
|
|
@ -0,0 +1,401 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { entities } from 'misskey-js';
|
||||||
|
import { beforeEach, describe, test } from '@jest/globals';
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
|
||||||
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
const WEBHOOK_HOST = 'http://localhost:15080';
|
||||||
|
const WEBHOOK_PORT = 15080;
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
describe('[シナリオ] ユーザ通報', () => {
|
||||||
|
let queue: INestApplicationContext;
|
||||||
|
let admin: entities.SignupResponse;
|
||||||
|
let alice: entities.SignupResponse;
|
||||||
|
let bob: entities.SignupResponse;
|
||||||
|
|
||||||
|
type SystemWebhookPayload = {
|
||||||
|
server: string;
|
||||||
|
hookId: string;
|
||||||
|
eventId: string;
|
||||||
|
createdAt: string;
|
||||||
|
type: string;
|
||||||
|
body: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>): Promise<T> {
|
||||||
|
const fastify = Fastify();
|
||||||
|
|
||||||
|
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||||
|
const result = await new Promise<string>(async (resolve, reject) => {
|
||||||
|
fastify.all('/', async (req, res) => {
|
||||||
|
timeoutHandle && clearTimeout(timeoutHandle);
|
||||||
|
|
||||||
|
const body = JSON.stringify(req.body);
|
||||||
|
res.status(200).send('ok');
|
||||||
|
await fastify.close();
|
||||||
|
resolve(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
await fastify.listen({ port: WEBHOOK_PORT });
|
||||||
|
|
||||||
|
timeoutHandle = setTimeout(async () => {
|
||||||
|
await fastify.close();
|
||||||
|
reject(new Error('timeout'));
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postAction();
|
||||||
|
} catch (e) {
|
||||||
|
await fastify.close();
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await fastify.close();
|
||||||
|
|
||||||
|
return JSON.parse(result) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
|
||||||
|
const res = await api(
|
||||||
|
'admin/system-webhook/create',
|
||||||
|
{
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: WEBHOOK_HOST,
|
||||||
|
secret: randomString(),
|
||||||
|
...args,
|
||||||
|
},
|
||||||
|
credential ?? admin,
|
||||||
|
);
|
||||||
|
return res.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAbuseReportNotificationRecipient(args?: Partial<entities.AdminAbuseReportNotificationRecipientCreateRequest>, credential?: UserToken): Promise<entities.AdminAbuseReportNotificationRecipientCreateResponse> {
|
||||||
|
const res = await api(
|
||||||
|
'admin/abuse-report/notification-recipient/create',
|
||||||
|
{
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
method: 'webhook',
|
||||||
|
...args,
|
||||||
|
},
|
||||||
|
credential ?? admin,
|
||||||
|
);
|
||||||
|
return res.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAbuseReport(args?: Partial<entities.UsersReportAbuseRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
|
||||||
|
const res = await api(
|
||||||
|
'users/report-abuse',
|
||||||
|
{
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
...args,
|
||||||
|
},
|
||||||
|
credential ?? admin,
|
||||||
|
);
|
||||||
|
return res.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAbuseReport(args?: Partial<entities.AdminResolveAbuseUserReportRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
|
||||||
|
const res = await api(
|
||||||
|
'admin/resolve-abuse-user-report',
|
||||||
|
{
|
||||||
|
reportId: admin.id,
|
||||||
|
...args,
|
||||||
|
},
|
||||||
|
credential ?? admin,
|
||||||
|
);
|
||||||
|
return res.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
queue = await startJobQueue();
|
||||||
|
admin = await signup({ username: 'admin' });
|
||||||
|
alice = await signup({ username: 'alice' });
|
||||||
|
bob = await signup({ username: 'bob' });
|
||||||
|
|
||||||
|
await role(admin, { isAdministrator: true });
|
||||||
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await queue.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('SystemWebhook', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const webhooks = await api('admin/system-webhook/list', {}, admin);
|
||||||
|
for (const webhook of webhooks.body) {
|
||||||
|
await api('admin/system-webhook/delete', { id: webhook.id }, admin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> abuseReportが送出される', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: ['abuseReport'],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(webhookBody, null, 2));
|
||||||
|
|
||||||
|
expect(webhookBody.hookId).toBe(webhook.id);
|
||||||
|
expect(webhookBody.type).toBe('abuseReport');
|
||||||
|
expect(webhookBody.body.targetUserId).toBe(alice.id);
|
||||||
|
expect(webhookBody.body.reporterId).toBe(bob.id);
|
||||||
|
expect(webhookBody.body.comment).toBe(abuse.comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが送出される', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: ['abuseReport', 'abuseReportResolved'],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody1 = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(webhookBody1, null, 2));
|
||||||
|
expect(webhookBody1.hookId).toBe(webhook.id);
|
||||||
|
expect(webhookBody1.type).toBe('abuseReport');
|
||||||
|
expect(webhookBody1.body.targetUserId).toBe(alice.id);
|
||||||
|
expect(webhookBody1.body.reporterId).toBe(bob.id);
|
||||||
|
expect(webhookBody1.body.assigneeId).toBeNull();
|
||||||
|
expect(webhookBody1.body.resolved).toBe(false);
|
||||||
|
expect(webhookBody1.body.comment).toBe(abuse.comment);
|
||||||
|
|
||||||
|
// 解決
|
||||||
|
const webhookBody2 = await captureWebhook(async () => {
|
||||||
|
await resolveAbuseReport({
|
||||||
|
reportId: webhookBody1.body.id,
|
||||||
|
forward: false,
|
||||||
|
}, admin);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(webhookBody2, null, 2));
|
||||||
|
expect(webhookBody2.hookId).toBe(webhook.id);
|
||||||
|
expect(webhookBody2.type).toBe('abuseReportResolved');
|
||||||
|
expect(webhookBody2.body.targetUserId).toBe(alice.id);
|
||||||
|
expect(webhookBody2.body.reporterId).toBe(bob.id);
|
||||||
|
expect(webhookBody2.body.assigneeId).toBe(admin.id);
|
||||||
|
expect(webhookBody2.body.resolved).toBe(true);
|
||||||
|
expect(webhookBody2.body.comment).toBe(abuse.comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> abuseReportが未許可の場合は送出されない', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: [],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody).toBe('timeout');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが送出される', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody1 = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody1).toBe('timeout');
|
||||||
|
|
||||||
|
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
|
||||||
|
|
||||||
|
// 解決
|
||||||
|
const webhookBody2 = await captureWebhook(async () => {
|
||||||
|
await resolveAbuseReport({
|
||||||
|
reportId: abuseReportId,
|
||||||
|
forward: false,
|
||||||
|
}, admin);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(webhookBody2, null, 2));
|
||||||
|
expect(webhookBody2.hookId).toBe(webhook.id);
|
||||||
|
expect(webhookBody2.type).toBe('abuseReportResolved');
|
||||||
|
expect(webhookBody2.body.targetUserId).toBe(alice.id);
|
||||||
|
expect(webhookBody2.body.reporterId).toBe(bob.id);
|
||||||
|
expect(webhookBody2.body.assigneeId).toBe(admin.id);
|
||||||
|
expect(webhookBody2.body.resolved).toBe(true);
|
||||||
|
expect(webhookBody2.body.comment).toBe(abuse.comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: ['abuseReport'],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody1 = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(webhookBody1, null, 2));
|
||||||
|
expect(webhookBody1.hookId).toBe(webhook.id);
|
||||||
|
expect(webhookBody1.type).toBe('abuseReport');
|
||||||
|
expect(webhookBody1.body.targetUserId).toBe(alice.id);
|
||||||
|
expect(webhookBody1.body.reporterId).toBe(bob.id);
|
||||||
|
expect(webhookBody1.body.assigneeId).toBeNull();
|
||||||
|
expect(webhookBody1.body.resolved).toBe(false);
|
||||||
|
expect(webhookBody1.body.comment).toBe(abuse.comment);
|
||||||
|
|
||||||
|
// 解決
|
||||||
|
const webhookBody2 = await captureWebhook(async () => {
|
||||||
|
await resolveAbuseReport({
|
||||||
|
reportId: webhookBody1.body.id,
|
||||||
|
forward: false,
|
||||||
|
}, admin);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody2).toBe('timeout');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: [],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody1 = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody1).toBe('timeout');
|
||||||
|
|
||||||
|
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
|
||||||
|
|
||||||
|
// 解決
|
||||||
|
const webhookBody2 = await captureWebhook(async () => {
|
||||||
|
await resolveAbuseReport({
|
||||||
|
reportId: abuseReportId,
|
||||||
|
forward: false,
|
||||||
|
}, admin);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody2).toBe('timeout');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> Webhookが無効の場合は送出されない', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: ['abuseReport', 'abuseReportResolved'],
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody1 = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody1).toBe('timeout');
|
||||||
|
|
||||||
|
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
|
||||||
|
|
||||||
|
// 解決
|
||||||
|
const webhookBody2 = await captureWebhook(async () => {
|
||||||
|
await resolveAbuseReport({
|
||||||
|
reportId: abuseReportId,
|
||||||
|
forward: false,
|
||||||
|
}, admin);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody2).toBe('timeout');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通報を受けた -> 通知設定が無効の場合は送出されない', async () => {
|
||||||
|
const webhook = await createSystemWebhook({
|
||||||
|
on: ['abuseReport', 'abuseReportResolved'],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false });
|
||||||
|
|
||||||
|
// 通報(bob -> alice)
|
||||||
|
const abuse = {
|
||||||
|
userId: alice.id,
|
||||||
|
comment: randomString(),
|
||||||
|
};
|
||||||
|
const webhookBody1 = await captureWebhook(async () => {
|
||||||
|
await createAbuseReport(abuse, bob);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody1).toBe('timeout');
|
||||||
|
|
||||||
|
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
|
||||||
|
|
||||||
|
// 解決
|
||||||
|
const webhookBody2 = await captureWebhook(async () => {
|
||||||
|
await resolveAbuseReport({
|
||||||
|
reportId: abuseReportId,
|
||||||
|
forward: false,
|
||||||
|
}, admin);
|
||||||
|
}).catch(e => e.message);
|
||||||
|
|
||||||
|
expect(webhookBody2).toBe('timeout');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,343 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
|
import {
|
||||||
|
AbuseReportNotificationRecipientRepository,
|
||||||
|
MiAbuseReportNotificationRecipient,
|
||||||
|
MiSystemWebhook,
|
||||||
|
MiUser,
|
||||||
|
SystemWebhooksRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { randomString } from '../utils.js';
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
describe('AbuseReportNotificationService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let service: AbuseReportNotificationService;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
let systemWebhooksRepository: SystemWebhooksRepository;
|
||||||
|
let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository;
|
||||||
|
let idService: IdService;
|
||||||
|
let roleService: jest.Mocked<RoleService>;
|
||||||
|
let emailService: jest.Mocked<EmailService>;
|
||||||
|
let webhookService: jest.Mocked<SystemWebhookService>;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let root: MiUser;
|
||||||
|
let alice: MiUser;
|
||||||
|
let bob: MiUser;
|
||||||
|
let systemWebhook1: MiSystemWebhook;
|
||||||
|
let systemWebhook2: MiSystemWebhook;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
const user = await usersRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
await userProfilesRepository.insert({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWebhook(data: Partial<MiSystemWebhook> = {}) {
|
||||||
|
return systemWebhooksRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRecipient(data: Partial<MiAbuseReportNotificationRecipient> = {}) {
|
||||||
|
return abuseReportNotificationRecipientRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => abuseReportNotificationRecipientRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AbuseReportNotificationService,
|
||||||
|
IdService,
|
||||||
|
{
|
||||||
|
provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||||
|
systemWebhooksRepository = app.get(DI.systemWebhooksRepository);
|
||||||
|
abuseReportNotificationRecipientRepository = app.get(DI.abuseReportNotificationRecipientRepository);
|
||||||
|
|
||||||
|
service = app.get(AbuseReportNotificationService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
roleService = app.get(RoleService) as jest.Mocked<RoleService>;
|
||||||
|
emailService = app.get<EmailService>(EmailService) as jest.Mocked<EmailService>;
|
||||||
|
webhookService = app.get<SystemWebhookService>(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||||
|
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||||
|
bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
|
||||||
|
systemWebhook1 = await createWebhook();
|
||||||
|
systemWebhook2 = await createWebhook();
|
||||||
|
|
||||||
|
roleService.getModeratorIds.mockResolvedValue([root.id, alice.id, bob.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
emailService.sendEmail.mockClear();
|
||||||
|
webhookService.enqueueSystemWebhook.mockClear();
|
||||||
|
|
||||||
|
await usersRepository.delete({});
|
||||||
|
await userProfilesRepository.delete({});
|
||||||
|
await systemWebhooksRepository.delete({});
|
||||||
|
await abuseReportNotificationRecipientRepository.delete({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('createRecipient', () => {
|
||||||
|
test('作成成功1', async () => {
|
||||||
|
const params = {
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
method: 'email' as RecipientMethod,
|
||||||
|
userId: alice.id,
|
||||||
|
systemWebhookId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipient1 = await service.createRecipient(params, root);
|
||||||
|
expect(recipient1).toMatchObject(params);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('作成成功2', async () => {
|
||||||
|
const params = {
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
method: 'webhook' as RecipientMethod,
|
||||||
|
userId: null,
|
||||||
|
systemWebhookId: systemWebhook1.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipient1 = await service.createRecipient(params, root);
|
||||||
|
expect(recipient1).toMatchObject(params);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateRecipient', () => {
|
||||||
|
test('更新成功1', async () => {
|
||||||
|
const recipient1 = await createRecipient({
|
||||||
|
method: 'email',
|
||||||
|
userId: alice.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
id: recipient1.id,
|
||||||
|
isActive: false,
|
||||||
|
name: randomString(),
|
||||||
|
method: 'email' as RecipientMethod,
|
||||||
|
userId: bob.id,
|
||||||
|
systemWebhookId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipient2 = await service.updateRecipient(params, root);
|
||||||
|
expect(recipient2).toMatchObject(params);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('更新成功2', async () => {
|
||||||
|
const recipient1 = await createRecipient({
|
||||||
|
method: 'webhook',
|
||||||
|
systemWebhookId: systemWebhook1.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
id: recipient1.id,
|
||||||
|
isActive: false,
|
||||||
|
name: randomString(),
|
||||||
|
method: 'webhook' as RecipientMethod,
|
||||||
|
userId: null,
|
||||||
|
systemWebhookId: systemWebhook2.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipient2 = await service.updateRecipient(params, root);
|
||||||
|
expect(recipient2).toMatchObject(params);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteRecipient', () => {
|
||||||
|
test('削除成功1', async () => {
|
||||||
|
const recipient1 = await createRecipient({
|
||||||
|
method: 'email',
|
||||||
|
userId: alice.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.deleteRecipient(recipient1.id, root);
|
||||||
|
|
||||||
|
await expect(abuseReportNotificationRecipientRepository.findOneBy({ id: recipient1.id })).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchRecipients', () => {
|
||||||
|
async function create() {
|
||||||
|
const recipient1 = await createRecipient({
|
||||||
|
method: 'email',
|
||||||
|
userId: alice.id,
|
||||||
|
});
|
||||||
|
const recipient2 = await createRecipient({
|
||||||
|
method: 'email',
|
||||||
|
userId: bob.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipient3 = await createRecipient({
|
||||||
|
method: 'webhook',
|
||||||
|
systemWebhookId: systemWebhook1.id,
|
||||||
|
});
|
||||||
|
const recipient4 = await createRecipient({
|
||||||
|
method: 'webhook',
|
||||||
|
systemWebhookId: systemWebhook2.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [recipient1, recipient2, recipient3, recipient4];
|
||||||
|
}
|
||||||
|
|
||||||
|
test('フィルタなし', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({});
|
||||||
|
expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フィルタなし(非モデレータは除外される)', async () => {
|
||||||
|
roleService.getModeratorIds.mockClear();
|
||||||
|
roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]);
|
||||||
|
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({});
|
||||||
|
// aliceはモデレータではないので除外される
|
||||||
|
expect(recipients).toEqual([recipient2, recipient3, recipient4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フィルタなし(非モデレータでも除外されないオプション設定)', async () => {
|
||||||
|
roleService.getModeratorIds.mockClear();
|
||||||
|
roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]);
|
||||||
|
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({}, { removeUnauthorized: false });
|
||||||
|
expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emailのみ', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({ method: ['email'] });
|
||||||
|
expect(recipients).toEqual([recipient1, recipient2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('webhookのみ', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({ method: ['webhook'] });
|
||||||
|
expect(recipients).toEqual([recipient3, recipient4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('すべて', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({ method: ['email', 'webhook'] });
|
||||||
|
expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id] });
|
||||||
|
expect(recipients).toEqual([recipient1, recipient3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定(method=emailではないIDが混ざりこまない)', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['email'] });
|
||||||
|
expect(recipients).toEqual([recipient1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定(method=webhookではないIDが混ざりこまない)', async () => {
|
||||||
|
const [recipient1, recipient2, recipient3, recipient4] = await create();
|
||||||
|
|
||||||
|
const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['webhook'] });
|
||||||
|
expect(recipients).toEqual([recipient3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,8 +3,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
@ -13,7 +11,14 @@ import { Test } from '@nestjs/testing';
|
||||||
import * as lolex from '@sinonjs/fake-timers';
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
|
import {
|
||||||
|
MiRole,
|
||||||
|
MiRoleAssignment,
|
||||||
|
MiUser,
|
||||||
|
RoleAssignmentsRepository,
|
||||||
|
RolesRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { genAidx } from '@/misc/id/aidx.js';
|
import { genAidx } from '@/misc/id/aidx.js';
|
||||||
|
@ -23,6 +28,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { RoleCondFormulaValue } from '@/models/Role.js';
|
import { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { sleep } from '../utils.js';
|
import { sleep } from '../utils.js';
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
import type { MockFunctionMetadata } from 'jest-mock';
|
import type { MockFunctionMetadata } from 'jest-mock';
|
||||||
|
@ -39,27 +45,27 @@ describe('RoleService', () => {
|
||||||
let notificationService: jest.Mocked<NotificationService>;
|
let notificationService: jest.Mocked<NotificationService>;
|
||||||
let clock: lolex.InstalledClock;
|
let clock: lolex.InstalledClock;
|
||||||
|
|
||||||
function createUser(data: Partial<MiUser> = {}) {
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
const un = secureRndstr(16);
|
const un = secureRndstr(16);
|
||||||
return usersRepository.insert({
|
const x = await usersRepository.insert({
|
||||||
id: genAidx(Date.now()),
|
id: genAidx(Date.now()),
|
||||||
username: un,
|
username: un,
|
||||||
usernameLower: un,
|
usernameLower: un,
|
||||||
...data,
|
...data,
|
||||||
})
|
});
|
||||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
return await usersRepository.findOneByOrFail(x.identifiers[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRole(data: Partial<MiRole> = {}) {
|
async function createRole(data: Partial<MiRole> = {}) {
|
||||||
return rolesRepository.insert({
|
const x = await rolesRepository.insert({
|
||||||
id: genAidx(Date.now()),
|
id: genAidx(Date.now()),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
...data,
|
...data,
|
||||||
})
|
});
|
||||||
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
|
return await rolesRepository.findOneByOrFail(x.identifiers[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial<MiRole> = {}) {
|
function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial<MiRole> = {}) {
|
||||||
|
@ -71,6 +77,20 @@ describe('RoleService', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assignRole(args: Partial<MiRoleAssignment>) {
|
||||||
|
const id = genAidx(Date.now());
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + 1);
|
||||||
|
|
||||||
|
await roleAssignmentsRepository.insert({
|
||||||
|
id,
|
||||||
|
expiresAt,
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await roleAssignmentsRepository.findOneByOrFail({ id });
|
||||||
|
}
|
||||||
|
|
||||||
function aidx() {
|
function aidx() {
|
||||||
return genAidx(Date.now());
|
return genAidx(Date.now());
|
||||||
}
|
}
|
||||||
|
@ -265,6 +285,96 @@ describe('RoleService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getModeratorIds', () => {
|
||||||
|
test('includeAdmins = false, excludeExpire = false', async () => {
|
||||||
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds(false, false);
|
||||||
|
expect(result).toEqual([modeUser1.id, modeUser2.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includeAdmins = false, excludeExpire = true', async () => {
|
||||||
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds(false, true);
|
||||||
|
expect(result).toEqual([modeUser1.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includeAdmins = true, excludeExpire = false', async () => {
|
||||||
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds(true, false);
|
||||||
|
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includeAdmins = true, excludeExpire = true', async () => {
|
||||||
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds(true, true);
|
||||||
|
expect(result).toEqual([adminUser1.id, modeUser1.id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('conditional role', () => {
|
describe('conditional role', () => {
|
||||||
test('~かつ~', async () => {
|
test('~かつ~', async () => {
|
||||||
const [user1, user2, user3, user4] = await Promise.all([
|
const [user1, user2, user3, user4] = await Promise.all([
|
||||||
|
|
|
@ -0,0 +1,515 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { MiUser } from '@/models/User.js';
|
||||||
|
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
|
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
|
import { randomString, sleep } from '../utils.js';
|
||||||
|
|
||||||
|
describe('SystemWebhookService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let service: SystemWebhookService;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let systemWebhooksRepository: SystemWebhooksRepository;
|
||||||
|
let idService: IdService;
|
||||||
|
let queueService: jest.Mocked<QueueService>;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let root: MiUser;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
return await usersRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWebhook(data: Partial<MiSystemWebhook> = {}) {
|
||||||
|
return systemWebhooksRepository
|
||||||
|
.insert({
|
||||||
|
id: idService.gen(),
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function beforeAllImpl() {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
SystemWebhookService,
|
||||||
|
IdService,
|
||||||
|
LoggerService,
|
||||||
|
GlobalEventService,
|
||||||
|
{
|
||||||
|
provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
systemWebhooksRepository = app.get(DI.systemWebhooksRepository);
|
||||||
|
|
||||||
|
service = app.get(SystemWebhookService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function afterAllImpl() {
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function beforeEachImpl() {
|
||||||
|
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function afterEachImpl() {
|
||||||
|
await usersRepository.delete({});
|
||||||
|
await systemWebhooksRepository.delete({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('アプリを毎回作り直す必要のないグループ', () => {
|
||||||
|
beforeAll(beforeAllImpl);
|
||||||
|
afterAll(afterAllImpl);
|
||||||
|
beforeEach(beforeEachImpl);
|
||||||
|
afterEach(afterEachImpl);
|
||||||
|
|
||||||
|
describe('fetchSystemWebhooks', () => {
|
||||||
|
test('フィルタなし', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('activeのみ', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchSystemWebhooks({ isActive: true });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('特定のイベントのみ', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'] });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('activeな特定のイベントのみ', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'], isActive: true });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id] });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ID指定(他条件とANDになるか見たい)', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
const webhook3 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
const webhook4 = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSystemWebhook', () => {
|
||||||
|
test('作成成功 ', async () => {
|
||||||
|
const params = {
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'] as SystemWebhookEventType[],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const webhook = await service.createSystemWebhook(params, root);
|
||||||
|
expect(webhook).toMatchObject(params);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateSystemWebhook', () => {
|
||||||
|
test('更新成功', async () => {
|
||||||
|
const webhook = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
id: webhook.id,
|
||||||
|
isActive: false,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'] as SystemWebhookEventType[],
|
||||||
|
url: randomString(),
|
||||||
|
secret: randomString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedWebhook = await service.updateSystemWebhook(params, root);
|
||||||
|
expect(updatedWebhook).toMatchObject(params);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteSystemWebhook', () => {
|
||||||
|
test('削除成功', async () => {
|
||||||
|
const webhook = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.deleteSystemWebhook(webhook.id, root);
|
||||||
|
|
||||||
|
await expect(systemWebhooksRepository.findOneBy({ id: webhook.id })).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('アプリを毎回作り直す必要があるグループ', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeAllImpl();
|
||||||
|
await beforeEachImpl();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await afterEachImpl();
|
||||||
|
await afterAllImpl();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enqueueSystemWebhook', () => {
|
||||||
|
test('キューに追加成功', async () => {
|
||||||
|
const webhook = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
||||||
|
|
||||||
|
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('非アクティブなWebhookはキューに追加されない', async () => {
|
||||||
|
const webhook = await createWebhook({
|
||||||
|
isActive: false,
|
||||||
|
on: ['abuseReport'],
|
||||||
|
});
|
||||||
|
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
||||||
|
|
||||||
|
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => {
|
||||||
|
const webhook1 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: [],
|
||||||
|
});
|
||||||
|
const webhook2 = await createWebhook({
|
||||||
|
isActive: true,
|
||||||
|
on: ['abuseReportResolved'],
|
||||||
|
});
|
||||||
|
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
|
||||||
|
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
|
||||||
|
|
||||||
|
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchActiveSystemWebhooks', () => {
|
||||||
|
describe('systemWebhookCreated', () => {
|
||||||
|
test('ActiveなWebhookが追加された時、キャッシュに追加されている', async () => {
|
||||||
|
const webhook = await service.createSystemWebhook(
|
||||||
|
{
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
},
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('NotActiveなWebhookが追加された時、キャッシュに追加されていない', async () => {
|
||||||
|
const webhook = await service.createSystemWebhook(
|
||||||
|
{
|
||||||
|
isActive: false,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
},
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('systemWebhookUpdated', () => {
|
||||||
|
test('ActiveなWebhookが編集された時、キャッシュに反映されている', async () => {
|
||||||
|
const id = idService.gen();
|
||||||
|
await createWebhook({ id });
|
||||||
|
// キャッシュ作成
|
||||||
|
const webhook1 = await service.fetchActiveSystemWebhooks();
|
||||||
|
// 読み込まれていることをチェック
|
||||||
|
expect(webhook1.length).toEqual(1);
|
||||||
|
expect(webhook1[0].id).toEqual(id);
|
||||||
|
|
||||||
|
const webhook2 = await service.updateSystemWebhook(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
isActive: true,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
},
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('NotActiveなWebhookが編集された時、キャッシュに追加されない', async () => {
|
||||||
|
const id = idService.gen();
|
||||||
|
await createWebhook({ id, isActive: false });
|
||||||
|
// キャッシュ作成
|
||||||
|
const webhook1 = await service.fetchActiveSystemWebhooks();
|
||||||
|
// 読み込まれていないことをチェック
|
||||||
|
expect(webhook1.length).toEqual(0);
|
||||||
|
|
||||||
|
const webhook2 = await service.updateSystemWebhook(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
isActive: false,
|
||||||
|
name: randomString(),
|
||||||
|
on: ['abuseReport'],
|
||||||
|
url: 'https://example.com',
|
||||||
|
secret: randomString(),
|
||||||
|
},
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('NotActiveなWebhookがActiveにされた時、キャッシュに追加されている', async () => {
|
||||||
|
const id = idService.gen();
|
||||||
|
const baseWebhook = await createWebhook({ id, isActive: false });
|
||||||
|
// キャッシュ作成
|
||||||
|
const webhook1 = await service.fetchActiveSystemWebhooks();
|
||||||
|
// 読み込まれていないことをチェック
|
||||||
|
expect(webhook1.length).toEqual(0);
|
||||||
|
|
||||||
|
const webhook2 = await service.updateSystemWebhook(
|
||||||
|
{
|
||||||
|
...baseWebhook,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks).toEqual([webhook2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ActiveなWebhookがNotActiveにされた時、キャッシュから削除されている', async () => {
|
||||||
|
const id = idService.gen();
|
||||||
|
const baseWebhook = await createWebhook({ id, isActive: true });
|
||||||
|
// キャッシュ作成
|
||||||
|
const webhook1 = await service.fetchActiveSystemWebhooks();
|
||||||
|
// 読み込まれていることをチェック
|
||||||
|
expect(webhook1.length).toEqual(1);
|
||||||
|
expect(webhook1[0].id).toEqual(id);
|
||||||
|
|
||||||
|
const webhook2 = await service.updateSystemWebhook(
|
||||||
|
{
|
||||||
|
...baseWebhook,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('systemWebhookDeleted', () => {
|
||||||
|
test('キャッシュから削除されている', async () => {
|
||||||
|
const id = idService.gen();
|
||||||
|
const baseWebhook = await createWebhook({ id, isActive: true });
|
||||||
|
// キャッシュ作成
|
||||||
|
const webhook1 = await service.fetchActiveSystemWebhooks();
|
||||||
|
// 読み込まれていることをチェック
|
||||||
|
expect(webhook1.length).toEqual(1);
|
||||||
|
expect(webhook1[0].id).toEqual(id);
|
||||||
|
|
||||||
|
const webhook2 = await service.deleteSystemWebhook(
|
||||||
|
id,
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
|
||||||
|
// redisでの配信経由で更新されるのでちょっと待つ
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
|
||||||
|
expect(fetchedWebhooks.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -22,6 +22,66 @@ export function abuseUserReport() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl: string | null = 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true'): entities.Channel {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
createdAt: '2016-12-28T22:49:51.000Z',
|
||||||
|
lastNotedAt: '2016-12-28T22:49:51.000Z',
|
||||||
|
name,
|
||||||
|
description: null,
|
||||||
|
userId: null,
|
||||||
|
bannerUrl,
|
||||||
|
pinnedNoteIds: [],
|
||||||
|
color: '#000',
|
||||||
|
isArchived: false,
|
||||||
|
usersCount: 1,
|
||||||
|
notesCount: 1,
|
||||||
|
isSensitive: false,
|
||||||
|
allowRenoteToExternal: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
createdAt: '2016-12-28T22:49:51.000Z',
|
||||||
|
lastClippedAt: null,
|
||||||
|
userId: 'someuserid',
|
||||||
|
user: {
|
||||||
|
id: 'someuserid',
|
||||||
|
name: 'Misskey User',
|
||||||
|
username: 'miskist',
|
||||||
|
host: 'misskey-hub.net',
|
||||||
|
avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
|
||||||
|
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
|
||||||
|
avatarDecorations: [],
|
||||||
|
emojis: {},
|
||||||
|
badgeRoles: [],
|
||||||
|
onlineStatus: 'unknown',
|
||||||
|
},
|
||||||
|
notesCount: undefined,
|
||||||
|
name,
|
||||||
|
description: 'Some clip description',
|
||||||
|
isPublic: false,
|
||||||
|
favoritedCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emojiDetailed(id = 'someemojiid', name = 'some_emoji'): entities.EmojiDetailed {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
aliases: ['alias1', 'alias2'],
|
||||||
|
name,
|
||||||
|
category: 'emojiCategory',
|
||||||
|
host: null,
|
||||||
|
url: '/client-assets/about-icon.png',
|
||||||
|
license: null,
|
||||||
|
isSensitive: false,
|
||||||
|
localOnly: false,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: ['roleId1', 'roleId2'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function galleryPost(isSensitive = false) {
|
export function galleryPost(isSensitive = false) {
|
||||||
return {
|
return {
|
||||||
id: 'somepostid',
|
id: 'somepostid',
|
||||||
|
|
|
@ -397,7 +397,7 @@ function toStories(component: string): Promise<string> {
|
||||||
const globs = await Promise.all([
|
const globs = await Promise.all([
|
||||||
glob('src/components/global/Mk*.vue'),
|
glob('src/components/global/Mk*.vue'),
|
||||||
glob('src/components/global/RouterView.vue'),
|
glob('src/components/global/RouterView.vue'),
|
||||||
glob('src/components/Mk{A,B}*.vue'),
|
glob('src/components/Mk[A-C]*.vue'),
|
||||||
glob('src/components/MkDigitalClock.vue'),
|
glob('src/components/MkDigitalClock.vue'),
|
||||||
glob('src/components/MkGalleryPostPreview.vue'),
|
glob('src/components/MkGalleryPostPreview.vue'),
|
||||||
glob('src/components/MkSignupServerRules.vue'),
|
glob('src/components/MkSignupServerRules.vue'),
|
||||||
|
|
|
@ -15,6 +15,7 @@ const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
staticDirs: [{ from: '../assets', to: '/client-assets' }],
|
||||||
addons: [
|
addons: [
|
||||||
getAbsolutePath('@storybook/addon-essentials'),
|
getAbsolutePath('@storybook/addon-essentials'),
|
||||||
getAbsolutePath('@storybook/addon-interactions'),
|
getAbsolutePath('@storybook/addon-interactions'),
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { FORCE_REMOUNT } from '@storybook/core-events';
|
||||||
import { addons } from '@storybook/preview-api';
|
import { addons } from '@storybook/preview-api';
|
||||||
import { type Preview, setup } from '@storybook/vue3';
|
import { type Preview, setup } from '@storybook/vue3';
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||||
import { userDetailed } from './fakes.js';
|
import { userDetailed } from './fakes.js';
|
||||||
import locale from './locale.js';
|
import locale from './locale.js';
|
||||||
import { commonHandlers, onUnhandledRequest } from './mocks.js';
|
import { commonHandlers, onUnhandledRequest } from './mocks.js';
|
||||||
|
@ -122,7 +122,6 @@ const preview = {
|
||||||
}
|
}
|
||||||
return story;
|
return story;
|
||||||
},
|
},
|
||||||
mswDecorator,
|
|
||||||
(Story, context) => {
|
(Story, context) => {
|
||||||
return {
|
return {
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -137,6 +136,7 @@ const preview = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
loaders: [mswLoader],
|
||||||
parameters: {
|
parameters: {
|
||||||
controls: {
|
controls: {
|
||||||
exclude: /^__/,
|
exclude: /^__/,
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.12.7",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/sanitize-html": "2.11.0",
|
"@types/sanitize-html": "2.11.0",
|
||||||
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/uuid": "9.0.8",
|
"@types/uuid": "9.0.8",
|
||||||
|
@ -128,6 +129,7 @@
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
|
"seedrandom": "3.0.5",
|
||||||
"start-server-and-test": "2.0.3",
|
"start-server-and-test": "2.0.3",
|
||||||
"storybook": "8.0.9",
|
"storybook": "8.0.9",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
|
|
|
@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:type="type"
|
:type="type"
|
||||||
:name="name"
|
:name="name"
|
||||||
:value="value"
|
:value="value"
|
||||||
|
:disabled="disabled"
|
||||||
@click="emit('click', $event)"
|
@click="emit('click', $event)"
|
||||||
@mousedown="onMousedown"
|
@mousedown="onMousedown"
|
||||||
>
|
>
|
||||||
|
@ -55,6 +56,7 @@ const props = defineProps<{
|
||||||
asLike?: boolean;
|
asLike?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { expect, userEvent, within } from '@storybook/test';
|
||||||
|
import { channel } from '../../.storybook/fakes.js';
|
||||||
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
|
import MkChannelFollowButton from './MkChannelFollowButton.vue';
|
||||||
|
import { semaphore } from '@/scripts/test-utils.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = semaphore();
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkChannelFollowButton,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkChannelFollowButton v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
channel: channel(),
|
||||||
|
full: true,
|
||||||
|
},
|
||||||
|
async play({ canvasElement }) {
|
||||||
|
await s.acquire();
|
||||||
|
await sleep(1000);
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
|
||||||
|
await userEvent.click(buttonElement);
|
||||||
|
await sleep(1000);
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
|
||||||
|
await sleep(100);
|
||||||
|
await userEvent.click(buttonElement);
|
||||||
|
s.release();
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...commonHandlers,
|
||||||
|
http.post('/api/channels/follow', async ({ request }) => {
|
||||||
|
action('POST /api/channels/follow')(await request.json());
|
||||||
|
return HttpResponse.json({});
|
||||||
|
}),
|
||||||
|
http.post('/api/channels/unfollow', async ({ request }) => {
|
||||||
|
action('POST /api/channels/unfollow')(await request.json());
|
||||||
|
return HttpResponse.json({});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkChannelFollowButton>;
|
|
@ -26,17 +26,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
channel: Record<string, any>;
|
channel: Misskey.entities.Channel;
|
||||||
full?: boolean;
|
full?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
full: false,
|
full: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFollowing = ref<boolean>(props.channel.isFollowing);
|
const isFollowing = ref(props.channel.isFollowing);
|
||||||
const wait = ref(false);
|
const wait = ref(false);
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { channel } from '../../.storybook/fakes.js';
|
||||||
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
|
import MkChannelList from './MkChannelList.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkChannelList,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkChannelList v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
pagination: {
|
||||||
|
endpoint: 'channels/search',
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
chromatic: {
|
||||||
|
// NOTE: ロードが終わるまで待つ
|
||||||
|
delay: 3000,
|
||||||
|
},
|
||||||
|
layout: 'fullscreen',
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...commonHandlers,
|
||||||
|
http.post('/api/channels/search', async ({ request, params }) => {
|
||||||
|
action('POST /api/channels/search')(await request.json());
|
||||||
|
return HttpResponse.json(params.untilId === 'lastchannel' ? [] : [
|
||||||
|
channel(),
|
||||||
|
channel('lastchannel', 'Last Channel', null),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies StoryObj<typeof MkChannelList>;
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { channel } from '../../.storybook/fakes.js';
|
||||||
|
import MkChannelPreview from './MkChannelPreview.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkChannelPreview,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkChannelPreview v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
channel: channel(),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies StoryObj<typeof MkChannelPreview>;
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { DefaultBodyType, HttpResponse, HttpResponseResolver, JsonBodyType, PathParams, http } from 'msw';
|
||||||
|
import seedrandom from 'seedrandom';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
|
import MkChart from './MkChart.vue';
|
||||||
|
|
||||||
|
function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] {
|
||||||
|
const rng = seedrandom(seed);
|
||||||
|
const max = Math.floor(option?.mul ?? 250 * rng());
|
||||||
|
let accumulation = 0;
|
||||||
|
const array: number[] = [];
|
||||||
|
for (let i = 0; i < limit; i++) {
|
||||||
|
const num = Math.floor((max + 1) * rng());
|
||||||
|
if (option?.accumulate) {
|
||||||
|
accumulation += num;
|
||||||
|
array.unshift(accumulation);
|
||||||
|
} else {
|
||||||
|
array.push(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChartResolver(fields: string[], option?: { accumulate?: boolean, mulMap?: Record<string, number> }): HttpResponseResolver<PathParams, DefaultBodyType, JsonBodyType> {
|
||||||
|
return ({ request }) => {
|
||||||
|
action(`GET ${request.url}`)();
|
||||||
|
const limitParam = new URL(request.url).searchParams.get('limit');
|
||||||
|
const limit = limitParam ? parseInt(limitParam) : 30;
|
||||||
|
const res = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
const layers = field.split('.');
|
||||||
|
let current = res;
|
||||||
|
while (layers.length > 1) {
|
||||||
|
const currentKey = layers.shift()!;
|
||||||
|
if (current[currentKey] == null) current[currentKey] = {};
|
||||||
|
current = current[currentKey];
|
||||||
|
}
|
||||||
|
current[layers[0]] = getChartArray(field, limit, {
|
||||||
|
accumulate: option?.accumulate,
|
||||||
|
mul: option?.mulMap != null && field in option.mulMap ? option.mulMap[field] : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return HttpResponse.json(res);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Base = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkChart,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkChart v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
src: 'federation',
|
||||||
|
span: 'hour',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...commonHandlers,
|
||||||
|
http.get('/api/charts/federation', getChartResolver(
|
||||||
|
['deliveredInstances', 'inboxInstances', 'stalled', 'sub', 'pub', 'pubsub', 'subActive', 'pubActive'],
|
||||||
|
)),
|
||||||
|
http.get('/api/charts/notes', getChartResolver(
|
||||||
|
['local.total', 'remote.total'],
|
||||||
|
{ accumulate: true },
|
||||||
|
)),
|
||||||
|
http.get('/api/charts/drive', getChartResolver(
|
||||||
|
['local.incSize', 'local.decSize', 'remote.incSize', 'remote.decSize'],
|
||||||
|
{ mulMap: { 'local.incSize': 1e7, 'local.decSize': 5e6, 'remote.incSize': 1e6, 'remote.decSize': 5e5 } },
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkChart>;
|
||||||
|
export const FederationChart = {
|
||||||
|
...Base,
|
||||||
|
args: {
|
||||||
|
src: 'federation',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkChart>;
|
||||||
|
export const NotesTotalChart = {
|
||||||
|
...Base,
|
||||||
|
args: {
|
||||||
|
src: 'notes-total',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkChart>;
|
||||||
|
export const DriveChart = {
|
||||||
|
...Base,
|
||||||
|
args: {
|
||||||
|
src: 'drive',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkChart>;
|
|
@ -19,8 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, shallowRef, watch, PropType } from 'vue';
|
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||||
|
@ -34,44 +35,55 @@ import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const props = defineProps({
|
type ChartSrc =
|
||||||
src: {
|
| 'federation'
|
||||||
type: String,
|
| 'ap-request'
|
||||||
required: true,
|
| 'users'
|
||||||
},
|
| 'users-total'
|
||||||
args: {
|
| 'active-users'
|
||||||
type: Object,
|
| 'notes'
|
||||||
required: false,
|
| 'local-notes'
|
||||||
},
|
| 'remote-notes'
|
||||||
limit: {
|
| 'notes-total'
|
||||||
type: Number,
|
| 'drive'
|
||||||
required: false,
|
| 'drive-files'
|
||||||
default: 90,
|
| 'instance-requests'
|
||||||
},
|
| 'instance-users'
|
||||||
span: {
|
| 'instance-users-total'
|
||||||
type: String as PropType<'hour' | 'day'>,
|
| 'instance-notes'
|
||||||
required: true,
|
| 'instance-notes-total'
|
||||||
},
|
| 'instance-ff'
|
||||||
detailed: {
|
| 'instance-ff-total'
|
||||||
type: Boolean,
|
| 'instance-drive-usage'
|
||||||
required: false,
|
| 'instance-drive-usage-total'
|
||||||
default: false,
|
| 'instance-drive-files'
|
||||||
},
|
| 'instance-drive-files-total'
|
||||||
stacked: {
|
| 'per-user-notes'
|
||||||
type: Boolean,
|
| 'per-user-pv'
|
||||||
required: false,
|
| 'per-user-following'
|
||||||
default: false,
|
| 'per-user-followers'
|
||||||
},
|
| 'per-user-drive'
|
||||||
bar: {
|
|
||||||
type: Boolean,
|
const props = withDefaults(defineProps<{
|
||||||
required: false,
|
src: ChartSrc;
|
||||||
default: false,
|
args?: {
|
||||||
},
|
host?: string;
|
||||||
aspectRatio: {
|
user?: Misskey.entities.UserLite;
|
||||||
type: Number,
|
withoutAll?: boolean;
|
||||||
required: false,
|
};
|
||||||
default: null,
|
limit?: number;
|
||||||
},
|
span: 'hour' | 'day';
|
||||||
|
detailed?: boolean;
|
||||||
|
stacked?: boolean;
|
||||||
|
bar?: boolean;
|
||||||
|
aspectRatio?: number | null;
|
||||||
|
}>(), {
|
||||||
|
args: undefined,
|
||||||
|
limit: 90,
|
||||||
|
detailed: false,
|
||||||
|
stacked: false,
|
||||||
|
bar: false,
|
||||||
|
aspectRatio: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
|
const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MkChartLegend from './MkChartLegend.vue';
|
||||||
|
void MkChartLegend;
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MkChartTooltip from './MkChartTooltip.vue';
|
||||||
|
void MkChartTooltip;
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { expect, within } from '@storybook/test';
|
||||||
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
|
import MkClickerGame from './MkClickerGame.vue';
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkClickerGame,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkClickerGame v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async play({ canvasElement }) {
|
||||||
|
await sleep(1000);
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const count = canvas.getByTestId('count');
|
||||||
|
// NOTE: flaky なので N/A も通しておく
|
||||||
|
await expect(count).toHaveTextContent(/^(0|N\/A)$/);
|
||||||
|
// FIXME: flaky
|
||||||
|
// const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
||||||
|
// await userEvent.click(buttonElement);
|
||||||
|
// await expect(count).toHaveTextContent('1');
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...commonHandlers,
|
||||||
|
http.post('/api/i/registry/get', async ({ request }) => {
|
||||||
|
action('POST /api/i/registry/get')(await request.json());
|
||||||
|
return HttpResponse.json({
|
||||||
|
error: {
|
||||||
|
message: 'No such key.',
|
||||||
|
code: 'NO_SUCH_KEY',
|
||||||
|
id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
http.post('/api/i/registry/set', async ({ request }) => {
|
||||||
|
action('POST /api/i/registry/set')(await request.json());
|
||||||
|
return HttpResponse.json(undefined, { status: 204 });
|
||||||
|
}),
|
||||||
|
http.post('/api/i/claim-achievement', async ({ request }) => {
|
||||||
|
action('POST /api/i/claim-achievement')(await request.json());
|
||||||
|
return HttpResponse.json(undefined, { status: 204 });
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkClickerGame>;
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div>
|
<div>
|
||||||
<div v-if="game.ready" :class="$style.game">
|
<div v-if="game.ready" :class="$style.game">
|
||||||
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
|
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
|
||||||
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
<div :class="$style.count" class="" data-testid="count"><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
||||||
<button v-click-anime class="_button" @click="onClick">
|
<button v-click-anime class="_button" @click="onClick">
|
||||||
<img src="/client-assets/cookie.png" :class="$style.img">
|
<img src="/client-assets/cookie.png" :class="$style.img">
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { clip } from '../../.storybook/fakes.js';
|
||||||
|
import MkClipPreview from './MkClipPreview.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkClipPreview,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkClipPreview v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
clip: clip(),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies StoryObj<typeof MkClipPreview>;
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MkCode_core from './MkCode.core.vue';
|
||||||
|
void MkCode_core;
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import MkCode from './MkCode.vue';
|
||||||
|
const code = `for (let i, 100) {
|
||||||
|
<: if (i % 15 == 0) "FizzBuzz"
|
||||||
|
elif (i % 3 == 0) "Fizz"
|
||||||
|
elif (i % 5 == 0) "Buzz"
|
||||||
|
else i
|
||||||
|
}`;
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkCode,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkCode v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
code,
|
||||||
|
lang: 'is',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkCode>;
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import MkCodeEditor from './MkCodeEditor.vue';
|
||||||
|
const code = `for (let i, 100) {
|
||||||
|
<: if (i % 15 == 0) "FizzBuzz"
|
||||||
|
elif (i % 3 == 0) "Fizz"
|
||||||
|
elif (i % 5 == 0) "Buzz"
|
||||||
|
else i
|
||||||
|
}`;
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkCodeEditor,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return {
|
||||||
|
'change': action('change'),
|
||||||
|
'keydown': action('keydown'),
|
||||||
|
'enter': action('enter'),
|
||||||
|
'update:modelValue': action('update:modelValue'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkCodeEditor v-model="code" v-bind="props" v-on="events" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
lang: 'aiscript',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><Suspense><story/></Suspense></div></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies StoryObj<typeof MkCodeEditor>;
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import MkCodeInline from './MkCodeInline.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkCodeInline,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkCodeInline v-bind="props"/>',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
code: '<: "Hello, world!"',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkCodeInline>;
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import MkColorInput from './MkColorInput.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkColorInput,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
color: '#cccccc',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return {
|
||||||
|
'update:modelValue': action('update:modelValue'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkColorInput v-model="color" v-bind="props" v-on="events" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><story/></div></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies StoryObj<typeof MkColorInput>;
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MkContainer from './MkContainer.vue';
|
||||||
|
void MkContainer;
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { userEvent, within } from '@storybook/test';
|
||||||
|
import MkContextMenu from './MkContextMenu.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
export const Empty = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onContextmenu(ev: MouseEvent) {
|
||||||
|
os.contextMenu(args.items, ev);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<div @contextmenu.stop="onContextmenu">Right Click Here</div>',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
async play({ canvasElement }) {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const target = canvas.getByText('Right Click Here');
|
||||||
|
await userEvent.pointer({ keys: '[MouseRight>]', target });
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkContextMenu>;
|
||||||
|
export const SomeTabs = {
|
||||||
|
...Empty,
|
||||||
|
args: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Home',
|
||||||
|
icon: 'ti ti-home',
|
||||||
|
action() {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkContextMenu>;
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { HttpResponse, http } from 'msw';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { file } from '../../.storybook/fakes.js';
|
||||||
|
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||||
|
import MkCropperDialog from './MkCropperDialog.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkCropperDialog,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return {
|
||||||
|
'ok': action('ok'),
|
||||||
|
'cancel': action('cancel'),
|
||||||
|
'closed': action('closed'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkCropperDialog v-bind="props" v-on="events" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
file: file(),
|
||||||
|
aspectRatio: NaN,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
chromatic: {
|
||||||
|
// NOTE: ロードが終わるまで待つ
|
||||||
|
delay: 3000,
|
||||||
|
},
|
||||||
|
layout: 'centered',
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...commonHandlers,
|
||||||
|
http.get('/proxy/image.webp', async ({ request }) => {
|
||||||
|
const url = new URL(request.url).searchParams.get('url');
|
||||||
|
if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') {
|
||||||
|
const image = await (await fetch('client-assets/fedi.jpg')).blob();
|
||||||
|
return new HttpResponse(image, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new HttpResponse(null, { status: 404 });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
http.post('/api/drive/files/create', async ({ request }) => {
|
||||||
|
action('POST /api/drive/files/create')(await request.formData());
|
||||||
|
return HttpResponse.json(file());
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkCropperDialog>;
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { emojiDetailed } from '../../.storybook/fakes.js';
|
||||||
|
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkCustomEmojiDetailedDialog,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkCustomEmojiDetailedDialog v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
emoji: emojiDetailed(),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkCustomEmojiDetailedDialog>;
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-default-export */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { expect, userEvent, within } from '@storybook/test';
|
||||||
|
import { file } from '../../.storybook/fakes.js';
|
||||||
|
import MkCwButton from './MkCwButton.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { semaphore } from '@/scripts/test-utils.js';
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = semaphore();
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkCwButton,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showContent: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return {
|
||||||
|
'update:modelValue': action('update:modelValue'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkCwButton v-model="showContent" v-bind="props" v-on="events" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
text: 'Some CW content',
|
||||||
|
},
|
||||||
|
async play({ canvasElement }) {
|
||||||
|
await s.acquire();
|
||||||
|
await sleep(1000);
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show);
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 }));
|
||||||
|
await userEvent.click(buttonElement);
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide);
|
||||||
|
await userEvent.click(buttonElement);
|
||||||
|
s.release();
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
chromatic: {
|
||||||
|
// NOTE: テストが終わるまで待つ
|
||||||
|
delay: 5000,
|
||||||
|
},
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkCwButton>;
|
||||||
|
export const IncludesTextAndDriveFile = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
text: 'Some CW content',
|
||||||
|
files: [file()],
|
||||||
|
},
|
||||||
|
async play({ canvasElement }) {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 }));
|
||||||
|
await expect(buttonElement).toHaveTextContent(' / ');
|
||||||
|
await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 }));
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkCwButton>;
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="default" :style="[
|
||||||
|
marginTopBottom ? { marginTop: marginTopBottom, marginBottom: marginTopBottom } : {},
|
||||||
|
marginLeftRight ? { marginLeft: marginLeftRight, marginRight: marginLeftRight } : {},
|
||||||
|
borderStyle ? { borderStyle: borderStyle } : {},
|
||||||
|
borderWidth ? { borderWidth: borderWidth } : {},
|
||||||
|
borderColor ? { borderColor: borderColor } : {},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
marginTopBottom?: string;
|
||||||
|
marginLeftRight?: string;
|
||||||
|
borderStyle?: string;
|
||||||
|
borderWidth?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.default {
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@keydown.enter="toggle"
|
@keydown.enter="toggle"
|
||||||
>
|
>
|
||||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||||
<span :class="$style.body">
|
<span v-if="!noBody" :class="$style.body">
|
||||||
<!-- TODO: 無名slotの方は廃止 -->
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
<span :class="$style.label">
|
<span :class="$style.label">
|
||||||
<span @click="toggle">
|
<span @click="toggle">
|
||||||
|
@ -34,16 +34,19 @@ const props = defineProps<{
|
||||||
modelValue: boolean | Ref<boolean>;
|
modelValue: boolean | Ref<boolean>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
|
noBody?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: boolean): void;
|
(ev: 'update:modelValue', v: boolean): void;
|
||||||
|
(ev: 'change', v: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const checked = toRefs(props).modelValue;
|
const checked = toRefs(props).modelValue;
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (props.disabled) return;
|
if (props.disabled) return;
|
||||||
emit('update:modelValue', !checked.value);
|
emit('update:modelValue', !checked.value);
|
||||||
|
emit('change', !checked.value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
|
||||||
|
|
||||||
|
export type MkSystemWebhookEditorProps = {
|
||||||
|
mode: 'create' | 'edit';
|
||||||
|
id?: string;
|
||||||
|
requiredEvents?: SystemWebhookEventType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MkSystemWebhookResult = {
|
||||||
|
id?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
name: string;
|
||||||
|
on: SystemWebhookEventType[];
|
||||||
|
url: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
|
||||||
|
const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => {
|
||||||
|
const res = await os.popup(
|
||||||
|
defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
|
||||||
|
props,
|
||||||
|
{
|
||||||
|
submitted: (ev: MkSystemWebhookResult) => {
|
||||||
|
resolve({ dispose: res.dispose, result: ev });
|
||||||
|
},
|
||||||
|
closed: () => {
|
||||||
|
resolve({ dispose: res.dispose, result: null });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
:width="450"
|
||||||
|
:height="590"
|
||||||
|
:canClose="true"
|
||||||
|
:withOkButton="false"
|
||||||
|
:okButtonDisabled="false"
|
||||||
|
@click="onCancelClicked"
|
||||||
|
@close="onCancelClicked"
|
||||||
|
@closed="onCancelClicked"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
|
||||||
|
</template>
|
||||||
|
<MkSpacer :marginMin="20" :marginMax="28">
|
||||||
|
<MkLoading v-if="loading !== 0"/>
|
||||||
|
<div v-else :class="$style.root" class="_gaps_m">
|
||||||
|
<MkInput v-model="title">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings.name }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="url">
|
||||||
|
<template #label>URL</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="secret">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings.events }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
||||||
|
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkSwitch v-model="isActive">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
|
<div :class="$style.footer" class="_buttonsCenter">
|
||||||
|
<MkButton primary :disabled="disableSubmitButton" @click="onSubmitClicked">
|
||||||
|
<i class="ti ti-check"></i>
|
||||||
|
{{ i18n.ts.ok }}
|
||||||
|
</MkButton>
|
||||||
|
<MkButton @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||||
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import {
|
||||||
|
MkSystemWebhookEditorProps,
|
||||||
|
MkSystemWebhookResult,
|
||||||
|
SystemWebhookEventType,
|
||||||
|
} from '@/components/MkSystemWebhookEditor.impl.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
type EventType = {
|
||||||
|
abuseReport: boolean;
|
||||||
|
abuseReportResolved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'submitted', result: MkSystemWebhookResult): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<MkSystemWebhookEditorProps>();
|
||||||
|
|
||||||
|
const { mode, id, requiredEvents } = toRefs(props);
|
||||||
|
|
||||||
|
const loading = ref<number>(0);
|
||||||
|
|
||||||
|
const title = ref<string>('');
|
||||||
|
const url = ref<string>('');
|
||||||
|
const secret = ref<string>('');
|
||||||
|
const events = ref<EventType>({
|
||||||
|
abuseReport: true,
|
||||||
|
abuseReportResolved: true,
|
||||||
|
});
|
||||||
|
const isActive = ref<boolean>(true);
|
||||||
|
|
||||||
|
const disabledEvents = ref<EventType>({
|
||||||
|
abuseReport: false,
|
||||||
|
abuseReportResolved: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableSubmitButton = computed(() => {
|
||||||
|
if (!title.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!url.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!secret.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmitClicked() {
|
||||||
|
await loadingScope(async () => {
|
||||||
|
const params = {
|
||||||
|
isActive: isActive.value,
|
||||||
|
name: title.value,
|
||||||
|
url: url.value,
|
||||||
|
secret: secret.value,
|
||||||
|
on: Object.keys(events.value).filter(ev => events.value[ev as keyof EventType]) as SystemWebhookEventType[],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (mode.value) {
|
||||||
|
case 'create': {
|
||||||
|
const result = await misskeyApi('admin/system-webhook/create', params);
|
||||||
|
emit('submitted', result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'edit': {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params });
|
||||||
|
emit('submitted', result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (ex: any) {
|
||||||
|
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||||
|
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelClicked() {
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
loading.value++;
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
loading.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadingScope(async () => {
|
||||||
|
switch (mode.value) {
|
||||||
|
case 'edit': {
|
||||||
|
if (!id.value) {
|
||||||
|
throw new Error('id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await misskeyApi('admin/system-webhook/show', { id: id.value });
|
||||||
|
|
||||||
|
title.value = res.name;
|
||||||
|
url.value = res.url;
|
||||||
|
secret.value = res.secret;
|
||||||
|
isActive.value = res.isActive;
|
||||||
|
for (const ev of Object.keys(events.value)) {
|
||||||
|
events.value[ev] = res.on.includes(ev as SystemWebhookEventType);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (ex: any) {
|
||||||
|
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
|
||||||
|
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ev of requiredEvents.value ?? []) {
|
||||||
|
disabledEvents.value[ev] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue