From 27ac3d795e7098124a7eea0dd59f6f1ef8f32394 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 4 Jun 2024 13:14:37 +0900
Subject: [PATCH 01/49] Update about-misskey.vue
---
packages/frontend/src/pages/about-misskey.vue | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index b55ae220d8..629f00689d 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -102,13 +102,16 @@ SPDX-License-Identifier: AGPL-3.0-only
Special thanks
From 43cccaaee9be42fab38eaa9ca04bb5e55b5d8db7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 4 Jun 2024 13:15:35 +0900
Subject: [PATCH 02/49] fix
---
packages/frontend/src/pages/about-misskey.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 629f00689d..cc0394f401 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-

+
From d4a8c63264939c4ec36af628f7e1516d2ae60254 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 09:32:04 +0900
Subject: [PATCH 03/49] enhance(backend): sentry integration for job queues
---
.../src/queue/QueueProcessorService.ts | 389 +++++++++++-------
1 file changed, 231 insertions(+), 158 deletions(-)
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index ce999d9cef..eb1901d069 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
+import * as Sentry from '@sentry/node';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
@@ -135,199 +136,271 @@ export class QueueProcessorService implements OnApplicationShutdown {
}
//#region system
- this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
- switch (job.name) {
- case 'tickCharts': return this.tickChartsProcessorService.process();
- case 'resyncCharts': return this.resyncChartsProcessorService.process();
- case 'cleanCharts': return this.cleanChartsProcessorService.process();
- case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
- case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
- case 'clean': return this.cleanProcessorService.process();
- default: throw new Error(`unrecognized job type ${job.name} for system`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.SYSTEM),
- autorun: false,
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'tickCharts': return this.tickChartsProcessorService.process();
+ case 'resyncCharts': return this.resyncChartsProcessorService.process();
+ case 'cleanCharts': return this.cleanChartsProcessorService.process();
+ case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
+ case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+ case 'clean': return this.cleanProcessorService.process();
+ default: throw new Error(`unrecognized job type ${job.name} for system`);
+ }
+ };
- const systemLogger = this.logger.createSubLogger('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),
+ autorun: false,
+ });
- this.systemQueueWorker
- .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
+ const systemLogger = this.logger.createSubLogger('system');
+
+ this.systemQueueWorker
+ .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region db
- this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
- switch (job.name) {
- case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
- case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
- case 'exportNotes': return this.exportNotesProcessorService.process(job);
- case 'exportClips': return this.exportClipsProcessorService.process(job);
- case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
- case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
- case 'exportMuting': return this.exportMutingProcessorService.process(job);
- case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
- case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
- case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
- case 'importFollowing': return this.importFollowingProcessorService.process(job);
- case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
- case 'importMuting': return this.importMutingProcessorService.process(job);
- case 'importBlocking': return this.importBlockingProcessorService.process(job);
- case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
- case 'importUserLists': return this.importUserListsProcessorService.process(job);
- case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
- case 'importAntennas': return this.importAntennasProcessorService.process(job);
- case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
- default: throw new Error(`unrecognized job type ${job.name} for db`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.DB),
- autorun: false,
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
+ case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
+ case 'exportNotes': return this.exportNotesProcessorService.process(job);
+ case 'exportClips': return this.exportClipsProcessorService.process(job);
+ case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
+ case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
+ case 'exportMuting': return this.exportMutingProcessorService.process(job);
+ case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
+ case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
+ case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
+ case 'importFollowing': return this.importFollowingProcessorService.process(job);
+ case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
+ case 'importMuting': return this.importMutingProcessorService.process(job);
+ case 'importBlocking': return this.importBlockingProcessorService.process(job);
+ case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
+ case 'importUserLists': return this.importUserListsProcessorService.process(job);
+ case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
+ case 'importAntennas': return this.importAntennasProcessorService.process(job);
+ case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
+ default: throw new Error(`unrecognized job type ${job.name} for db`);
+ }
+ };
- const dbLogger = this.logger.createSubLogger('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),
+ autorun: false,
+ });
- this.dbQueueWorker
- .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
+ const dbLogger = this.logger.createSubLogger('db');
+
+ this.dbQueueWorker
+ .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region deliver
- this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.DELIVER),
- autorun: false,
- concurrency: this.config.deliverJobConcurrency ?? 128,
- limiter: {
- max: this.config.deliverJobPerSec ?? 128,
- duration: 1000,
- },
- settings: {
- backoffStrategy: httpRelatedBackoff,
- },
- });
+ {
+ 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),
+ autorun: false,
+ concurrency: this.config.deliverJobConcurrency ?? 128,
+ limiter: {
+ max: this.config.deliverJobPerSec ?? 128,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- const deliverLogger = this.logger.createSubLogger('deliver');
+ const deliverLogger = this.logger.createSubLogger('deliver');
- this.deliverQueueWorker
- .on('active', (job) => deliverLogger.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('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
- .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
+ this.deliverQueueWorker
+ .on('active', (job) => deliverLogger.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('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
+ .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region inbox
- this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.INBOX),
- autorun: false,
- concurrency: this.config.inboxJobConcurrency ?? 16,
- limiter: {
- max: this.config.inboxJobPerSec ?? 32,
- duration: 1000,
- },
- settings: {
- backoffStrategy: httpRelatedBackoff,
- },
- });
+ {
+ 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),
+ autorun: false,
+ concurrency: this.config.inboxJobConcurrency ?? 16,
+ limiter: {
+ max: this.config.inboxJobPerSec ?? 32,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- const inboxLogger = this.logger.createSubLogger('inbox');
+ const inboxLogger = this.logger.createSubLogger('inbox');
- this.inboxQueueWorker
- .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
- .on('completed', (job, result) => inboxLogger.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('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
+ this.inboxQueueWorker
+ .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
+ .on('completed', (job, result) => inboxLogger.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('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region webhook deliver
- this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
- autorun: false,
- concurrency: 64,
- limiter: {
- max: 64,
- duration: 1000,
- },
- settings: {
- backoffStrategy: httpRelatedBackoff,
- },
- });
+ {
+ this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: WebhookDeliver' }, () => this.webhookDeliverProcessorService.process(job));
+ } else {
+ return this.webhookDeliverProcessorService.process(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
+ autorun: false,
+ concurrency: 64,
+ limiter: {
+ max: 64,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- const webhookLogger = this.logger.createSubLogger('webhook');
+ const webhookLogger = this.logger.createSubLogger('webhook');
- this.webhookDeliverQueueWorker
- .on('active', (job) => webhookLogger.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('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
- .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
+ this.webhookDeliverQueueWorker
+ .on('active', (job) => webhookLogger.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('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
+ .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region relationship
- this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
- switch (job.name) {
- case 'follow': return this.relationshipProcessorService.processFollow(job);
- case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
- case 'block': return this.relationshipProcessorService.processBlock(job);
- case 'unblock': return this.relationshipProcessorService.processUnblock(job);
- default: throw new Error(`unrecognized job type ${job.name} for relationship`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
- autorun: false,
- concurrency: this.config.relationshipJobConcurrency ?? 16,
- limiter: {
- max: this.config.relationshipJobPerSec ?? 64,
- duration: 1000,
- },
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'follow': return this.relationshipProcessorService.processFollow(job);
+ case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
+ case 'block': return this.relationshipProcessorService.processBlock(job);
+ case 'unblock': return this.relationshipProcessorService.processUnblock(job);
+ default: throw new Error(`unrecognized job type ${job.name} for relationship`);
+ }
+ };
- const relationshipLogger = this.logger.createSubLogger('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),
+ autorun: false,
+ concurrency: this.config.relationshipJobConcurrency ?? 16,
+ limiter: {
+ max: this.config.relationshipJobPerSec ?? 64,
+ duration: 1000,
+ },
+ });
- this.relationshipQueueWorker
- .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
+ const relationshipLogger = this.logger.createSubLogger('relationship');
+
+ this.relationshipQueueWorker
+ .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region object storage
- this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
- switch (job.name) {
- case 'deleteFile': return this.deleteFileProcessorService.process(job);
- case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
- default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
- autorun: false,
- concurrency: 16,
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'deleteFile': return this.deleteFileProcessorService.process(job);
+ case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
+ default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
+ }
+ };
- const objectStorageLogger = this.logger.createSubLogger('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),
+ autorun: false,
+ concurrency: 16,
+ });
- this.objectStorageQueueWorker
- .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
+ const objectStorageLogger = this.logger.createSubLogger('objectStorage');
+
+ this.objectStorageQueueWorker
+ .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region ended poll notification
- this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
- autorun: false,
- });
+ {
+ 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),
+ autorun: false,
+ });
+ }
//#endregion
}
From dbf9e1194bf5b84ec711c14f27c11d2bfeb37f20 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 10:01:50 +0900
Subject: [PATCH 04/49] refactor(backend): remove unused logger option
---
packages/backend/src/core/LoggerService.ts | 4 ++--
.../backend/src/core/chart/ChartLoggerService.ts | 2 +-
packages/backend/src/logger.ts | 14 +++++---------
packages/backend/src/server/FileServerService.ts | 2 +-
packages/backend/src/server/ServerService.ts | 2 +-
5 files changed, 10 insertions(+), 14 deletions(-)
diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts
index 96d9b09992..f102461a50 100644
--- a/packages/backend/src/core/LoggerService.ts
+++ b/packages/backend/src/core/LoggerService.ts
@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
- public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
- return new Logger(domain, color, store);
+ public getLogger(domain: string, color?: KEYWORD | undefined) {
+ return new Logger(domain, color);
}
}
diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts
index afc728d564..20815ea968 100644
--- a/packages/backend/src/core/chart/ChartLoggerService.ts
+++ b/packages/backend/src/core/chart/ChartLoggerService.ts
@@ -14,6 +14,6 @@ export class ChartLoggerService {
constructor(
private loggerService: LoggerService,
) {
- this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
+ this.logger = this.loggerService.getLogger('chart', 'white');
}
}
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index d4705af601..ff5363a425 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
- private store: boolean;
- constructor(context: string, color?: KEYWORD, store = true) {
+ constructor(context: string, color?: KEYWORD) {
this.context = {
name: context,
color: color,
};
- this.store = store;
}
@bindThis
- public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
- const logger = new Logger(context, color, store);
+ public createSubLogger(context: string, color?: KEYWORD): Logger {
+ const logger = new Logger(context, color);
logger.parentLogger = this;
return logger;
}
@bindThis
- private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = [], store = true): void {
+ private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = []): void {
if (envOption.quiet) return;
- if (!this.store) store = false;
- if (level === 'debug') store = false;
if (this.parentLogger) {
- this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store);
+ this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts));
return;
}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 9db3aa1bfb..77a637d895 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -53,7 +53,7 @@ export class FileServerService {
private internalStorageService: InternalStorageService,
private loggerService: LoggerService,
) {
- this.logger = this.loggerService.getLogger('server', 'gray', false);
+ this.logger = this.loggerService.getLogger('server', 'gray');
//this.createServer = this.createServer.bind(this);
}
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 3572f16627..9c849480f2 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -68,7 +68,7 @@ export class ServerService implements OnApplicationShutdown {
private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService,
) {
- this.logger = this.loggerService.getLogger('server', 'gray', false);
+ this.logger = this.loggerService.getLogger('server', 'gray');
}
@bindThis
From 65d19279a2c19c3e6be1c4c50cc9b01c94420d6c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 10:11:43 +0900
Subject: [PATCH 05/49] fix
---
packages/backend/src/boot/master.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 75e1a80cd1..4bc5c799cf 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');
From ab69e113f4921462b72f1f352dfefe52b37862f5 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 11:20:54 +0900
Subject: [PATCH 06/49] enhance(backend): improve sentry integration
---
packages/backend/src/boot/worker.ts | 23 +++++++++++++++++++
.../src/queue/QueueProcessorService.ts | 4 ++--
.../backend/src/server/api/ApiCallService.ts | 14 +++++++----
3 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
index d4a7cd56e5..5d4a15b29f 100644
--- a/packages/backend/src/boot/worker.ts
+++ b/packages/backend/src/boot/worker.ts
@@ -4,13 +4,36 @@
*/
import cluster from 'node:cluster';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { envOption } from '@/env.js';
+import { loadConfig } from '@/config.js';
import { jobQueue, server } from './common.js';
/**
* Init worker process
*/
export async function workerMain() {
+ const config = loadConfig();
+
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index eb1901d069..4f333df791 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -165,7 +165,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.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) => systemLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
}
@@ -214,7 +214,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.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) => dbLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 271ef80554..e21a5e90ab 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -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) {
throw err;
} else {
@@ -108,10 +108,12 @@ export class ApiCallService implements OnApplicationShutdown {
id: errId,
},
});
- console.error(err, errId);
if (this.config.sentryForBackend) {
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ user: {
+ id: userId,
+ },
extra: {
ep: ep.name,
ps: data,
@@ -410,9 +412,13 @@ export class ApiCallService implements OnApplicationShutdown {
// API invoking
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 {
- 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));
}
}
From a697a7f97b401d1545a7f61694b2704b4d3ac9fc Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 11:38:34 +0900
Subject: [PATCH 07/49] enhance(backend): improve sentry integration
---
.../src/queue/QueueProcessorService.ts | 63 ++++++++++++++++---
1 file changed, 56 insertions(+), 7 deletions(-)
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 4f333df791..fdeb6a9518 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -165,7 +165,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => systemLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('failed', (job, err) => {
+ systemLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}`, {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
}
@@ -214,7 +221,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => dbLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
+ .on('failed', (job, err) => {
+ dbLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}`, {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
}
@@ -246,7 +260,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.deliverQueueWorker
.on('active', (job) => deliverLogger.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('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
+ .on('failed', (job, err) => {
+ deliverLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ if (config.sentryForBackend) {
+ Sentry.captureMessage('Queue: Deliver', {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
}
@@ -278,7 +299,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.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) => {
+ inboxLogger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage('Queue: Inbox', {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
}
@@ -310,7 +338,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.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('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
+ .on('failed', (job, err) => {
+ webhookLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ if (config.sentryForBackend) {
+ Sentry.captureMessage('Queue: WebhookDeliver', {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
}
@@ -349,7 +384,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.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) => {
+ relationshipLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}`, {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
}
@@ -382,7 +424,14 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.objectStorageQueueWorker
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.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) => {
+ objectStorageLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}`, {
+ extra: { job, err },
+ });
+ }
+ })
.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
}
From d55e638a231b380e733dc0ada3c3de410f918a93 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 11:40:11 +0900
Subject: [PATCH 08/49] lint fixes
---
packages/backend/src/NestLogger.ts | 2 +-
packages/backend/src/boot/entry.ts | 2 +-
packages/backend/src/postgres.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts
index 80f1f7a024..d0be19664f 100644
--- a/packages/backend/src/NestLogger.ts
+++ b/packages/backend/src/NestLogger.ts
@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
const logger = new Logger('core', 'cyan');
-const nestLogger = logger.createSubLogger('nest', 'green', false);
+const nestLogger = logger.createSubLogger('nest', 'green');
export class NestLogger implements LoggerService {
/**
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 04c6ca9723..25375c3015 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
-const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const clusterLogger = logger.createSubLogger('cluster', 'orange');
const ev = new Xev();
//#region Events
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 2d14537bbb..aa2aa5e373 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -85,7 +85,7 @@ import { bindThis } from '@/decorators.js';
export const dbLogger = new MisskeyLogger('db');
-const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
+const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
class MyCustomLogger implements Logger {
@bindThis
From 8f833d742fdb10f088479c78ad489461773a8b81 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 6 Jun 2024 11:51:31 +0900
Subject: [PATCH 09/49] enhance(backend): improve sentry integration
---
.../backend/src/queue/QueueProcessorService.ts | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index fdeb6a9518..6a87be021e 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -165,10 +165,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => {
+ .on('failed', (job, err: Error) => {
systemLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}`, {
+ Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
extra: { job, err },
});
}
@@ -224,7 +224,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('failed', (job, err) => {
dbLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}`, {
+ Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
extra: { job, err },
});
}
@@ -263,7 +263,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('failed', (job, err) => {
deliverLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
- Sentry.captureMessage('Queue: Deliver', {
+ Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
extra: { job, err },
});
}
@@ -302,7 +302,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('failed', (job, err) => {
inboxLogger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage('Queue: Inbox', {
+ Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
extra: { job, err },
});
}
@@ -341,7 +341,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('failed', (job, err) => {
webhookLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
- Sentry.captureMessage('Queue: WebhookDeliver', {
+ Sentry.captureMessage(`Queue: WebhookDeliver: ${err.message}`, {
extra: { job, err },
});
}
@@ -387,7 +387,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('failed', (job, err) => {
relationshipLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}`, {
+ Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
extra: { job, err },
});
}
@@ -427,7 +427,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('failed', (job, err) => {
objectStorageLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}`, {
+ Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
extra: { job, err },
});
}
From 00157864e95f4fae0c8aacff16ddac7f322ad68c Mon Sep 17 00:00:00 2001
From: taichan <40626578+tai-cha@users.noreply.github.com>
Date: Fri, 7 Jun 2024 09:00:01 +0900
Subject: [PATCH 10/49] =?UTF-8?q?fix(backend):=20=E3=83=81=E3=83=A3?=
=?UTF-8?q?=E3=83=BC=E3=83=88=E7=94=9F=E6=88=90=E6=99=82=E3=81=ABinstance.?=
=?UTF-8?q?isSuspended=E3=81=8C=E8=AA=AD=E3=81=BE=E3=82=8C=E3=81=A6?=
=?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F=E9=A1=8C=E3=81=AE=E4=BF=AE?=
=?UTF-8?q?=E6=AD=A3=20(#13951)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(backend): use sustensionState instead of isSuspended
* Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
packages/backend/src/core/chart/charts/federation.ts | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d4ef23d27..3cccb451d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@
-
### Server
--
+- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
## 2024.5.0
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index 5e4555ee96..c2329a2f73 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -47,7 +47,7 @@ export default class FederationChart extends Chart { // eslint-di
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host')
- .where('instance.isSuspended = true');
+ .where('instance.suspensionState != \'none\'');
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
.select('f.followerHost')
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
From 859271613958cc4a9bc8fcd9616c3146c07a50a4 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 7 Jun 2024 13:15:37 +0900
Subject: [PATCH 11/49] enhance(backend): improve sentry integration
---
packages/backend/src/queue/QueueProcessorService.ts | 7 +++++++
packages/backend/src/server/api/ApiCallService.ts | 1 +
2 files changed, 8 insertions(+)
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 6a87be021e..7bfe1f4caa 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -169,6 +169,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
systemLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
@@ -225,6 +226,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
dbLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
@@ -264,6 +266,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
deliverLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
@@ -303,6 +306,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
inboxLogger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
@@ -342,6 +346,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
webhookLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: WebhookDeliver: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
@@ -388,6 +393,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
relationshipLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
@@ -428,6 +434,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
objectStorageLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
extra: { job, err },
});
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index e21a5e90ab..166f9c8675 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -111,6 +111,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (this.config.sentryForBackend) {
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ level: 'error',
user: {
id: userId,
},
From e0cf5b24022e22179355cf9439d3cfea6036daba Mon Sep 17 00:00:00 2001
From: Porlam Nicla <84321396+GrapeApple0@users.noreply.github.com>
Date: Fri, 7 Jun 2024 14:46:46 +0900
Subject: [PATCH 12/49] =?UTF-8?q?=E9=85=8D=E4=BF=A1=E5=81=9C=E6=AD=A2?=
=?UTF-8?q?=E3=81=97=E3=81=9F=E3=82=A4=E3=83=B3=E3=82=B9=E3=82=BF=E3=83=B3?=
=?UTF-8?q?=E3=82=B9=E4=B8=80=E8=A6=A7=E3=81=8C=E8=A6=8B=E3=82=8C=E3=81=AA?=
=?UTF-8?q?=E3=81=8F=E3=81=AA=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?=
=?UTF-8?q?=E6=AD=A3=20(#13945)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 配信停止したインスタンス一覧が見れなくなる問題を修正
* Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
.../backend/src/server/api/endpoints/federation/instances.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3cccb451d5..f93811c606 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
## Unreleased
### General
--
+- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
### Client
-
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 4ef4315fb3..36f4bf5aa6 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -117,9 +117,9 @@ export default class extends Endpoint { // eslint-
if (typeof ps.suspended === 'boolean') {
if (ps.suspended) {
- query.andWhere('instance.isSuspended = TRUE');
+ query.andWhere('instance.suspensionState != \'none\'');
} else {
- query.andWhere('instance.isSuspended = FALSE');
+ query.andWhere('instance.suspensionState = \'none\'');
}
}
From 61fae45390283aee7ac582aa5303aae863de0f7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
<46447427+samunohito@users.noreply.github.com>
Date: Sat, 8 Jun 2024 15:34:19 +0900
Subject: [PATCH 13/49] =?UTF-8?q?feat:=20=E9=80=9A=E5=A0=B1=E3=82=92?=
=?UTF-8?q?=E5=8F=97=E3=81=91=E3=81=9F=E9=9A=9B=E3=81=AB=E3=83=A1=E3=83=BC?=
=?UTF-8?q?=E3=83=AB=E3=81=BE=E3=81=9F=E3=81=AFWebhook=E3=81=A7=E9=80=9A?=
=?UTF-8?q?=E7=9F=A5=E3=82=92=E9=80=81=E5=87=BA=E5=87=BA=E6=9D=A5=E3=82=8B?=
=?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20(#13758)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする
* モデログに対応&エンドポイントを単一オブジェクトでのサポートに変更(API経由で大量に作るシチュエーションもないと思うので)
* fix spdx
* fix migration
* fix migration
* fix models
* add e2e webhook
* tweak
* fix modlog
* fix bugs
* add tests and fix bugs
* add tests and fix bugs
* add tests
* fix path
* regenerate locale
* 混入除去
* 混入除去
* add abuseReportResolved
* fix pnpm-lock.yaml
* add abuseReportResolved test
* fix bugs
* fix ui
* add tests
* fix CHANGELOG.md
* add tests
* add RoleService.getModeratorIds tests
* WebhookServiceをUserとSystemに分割
* fix CHANGELOG.md
* fix test
* insertOneを使う用に
* fix
* regenerate locales
* revert version
* separate webhook job queue
* fix
* :art:
* Update QueueProcessorService.ts
---------
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 1 +
locales/index.d.ts | 94 +++
locales/ja-JP.yml | 27 +
...1713656541000-abuse-report-notification.js | 62 ++
.../core/AbuseReportNotificationService.ts | 406 ++++++++++
.../backend/src/core/AbuseReportService.ts | 128 ++++
packages/backend/src/core/CoreModule.ts | 44 +-
packages/backend/src/core/EmailService.ts | 2 +
.../backend/src/core/GlobalEventService.ts | 4 +
.../backend/src/core/NoteCreateService.ts | 12 +-
packages/backend/src/core/QueueModule.ts | 38 +-
packages/backend/src/core/QueueService.ts | 63 +-
packages/backend/src/core/RoleService.ts | 30 +-
.../backend/src/core/SystemWebhookService.ts | 233 ++++++
.../backend/src/core/UserBlockingService.ts | 6 +-
.../backend/src/core/UserFollowingService.ts | 12 +-
.../backend/src/core/UserWebhookService.ts | 99 +++
packages/backend/src/core/WebhookService.ts | 97 ---
.../src/core/activitypub/ApInboxService.ts | 10 +-
...eportNotificationRecipientEntityService.ts | 88 +++
.../entities/SystemWebhookEntityService.ts | 74 ++
packages/backend/src/di-symbols.ts | 2 +
packages/backend/src/misc/json-schema.ts | 28 +-
.../AbuseReportNotificationRecipient.ts | 100 +++
.../backend/src/models/RepositoryModule.ts | 98 ++-
packages/backend/src/models/SystemWebhook.ts | 98 +++
packages/backend/src/models/_.ts | 6 +
.../abuse-report-notification-recipient.ts | 50 ++
.../src/models/json-schema/system-webhook.ts | 54 ++
packages/backend/src/postgres.ts | 8 +-
.../backend/src/queue/QueueProcessorModule.ts | 6 +-
.../src/queue/QueueProcessorService.ts | 153 ++--
packages/backend/src/queue/const.ts | 3 +-
.../SystemWebhookDeliverProcessorService.ts | 87 +++
... => UserWebhookDeliverProcessorService.ts} | 6 +-
packages/backend/src/queue/types.ts | 12 +-
.../backend/src/server/api/EndpointsModule.ts | 42 +-
packages/backend/src/server/api/endpoints.ts | 38 +-
.../notification-recipient/create.ts | 122 +++
.../notification-recipient/delete.ts | 44 ++
.../notification-recipient/list.ts | 55 ++
.../notification-recipient/show.ts | 64 ++
.../notification-recipient/update.ts | 128 ++++
.../server/api/endpoints/admin/queue/stats.ts | 5 +-
.../admin/resolve-abuse-user-report.ts | 53 +-
.../endpoints/admin/system-webhook/create.ts | 85 +++
.../endpoints/admin/system-webhook/delete.ts | 44 ++
.../endpoints/admin/system-webhook/list.ts | 60 ++
.../endpoints/admin/system-webhook/show.ts | 62 ++
.../endpoints/admin/system-webhook/update.ts | 91 +++
.../api/endpoints/users/report-abuse.ts | 54 +-
.../src/server/web/ClientServerService.ts | 17 +-
packages/backend/src/types.ts | 32 +
.../backend/test/e2e/synalio/abuse-report.ts | 401 ++++++++++
.../unit/AbuseReportNotificationService.ts | 343 +++++++++
packages/backend/test/unit/RoleService.ts | 132 +++-
.../backend/test/unit/SystemWebhookService.ts | 515 +++++++++++++
packages/frontend/src/components/MkButton.vue | 2 +
.../frontend/src/components/MkDivider.vue | 32 +
packages/frontend/src/components/MkSwitch.vue | 5 +-
.../components/MkSystemWebhookEditor.impl.ts | 45 ++
.../src/components/MkSystemWebhookEditor.vue | 217 ++++++
.../notification-recipient.editor.vue | 307 ++++++++
.../notification-recipient.item.vue | 114 +++
.../abuse-report/notification-recipient.vue | 176 +++++
packages/frontend/src/pages/admin/abuses.vue | 85 ++-
packages/frontend/src/pages/admin/index.vue | 5 +
.../src/pages/admin/modlog.ModLog.vue | 48 +-
.../src/pages/admin/system-webhook.item.vue | 117 +++
.../src/pages/admin/system-webhook.vue | 96 +++
packages/frontend/src/router/definition.ts | 8 +
packages/misskey-js/etc/misskey-js.api.md | 105 ++-
.../misskey-js/src/autogen/apiClientJSDoc.ts | 120 +++
packages/misskey-js/src/autogen/endpoint.ts | 28 +
packages/misskey-js/src/autogen/entities.ts | 18 +
packages/misskey-js/src/autogen/models.ts | 2 +
packages/misskey-js/src/autogen/types.ts | 693 ++++++++++++++++++
packages/misskey-js/src/consts.ts | 26 +
packages/misskey-js/src/entities.ts | 19 +-
79 files changed, 6527 insertions(+), 369 deletions(-)
create mode 100644 packages/backend/migration/1713656541000-abuse-report-notification.js
create mode 100644 packages/backend/src/core/AbuseReportNotificationService.ts
create mode 100644 packages/backend/src/core/AbuseReportService.ts
create mode 100644 packages/backend/src/core/SystemWebhookService.ts
create mode 100644 packages/backend/src/core/UserWebhookService.ts
delete mode 100644 packages/backend/src/core/WebhookService.ts
create mode 100644 packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
create mode 100644 packages/backend/src/core/entities/SystemWebhookEntityService.ts
create mode 100644 packages/backend/src/models/AbuseReportNotificationRecipient.ts
create mode 100644 packages/backend/src/models/SystemWebhook.ts
create mode 100644 packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
create mode 100644 packages/backend/src/models/json-schema/system-webhook.ts
create mode 100644 packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
rename packages/backend/src/queue/processors/{WebhookDeliverProcessorService.ts => UserWebhookDeliverProcessorService.ts} (92%)
create mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
create mode 100644 packages/backend/test/e2e/synalio/abuse-report.ts
create mode 100644 packages/backend/test/unit/AbuseReportNotificationService.ts
create mode 100644 packages/backend/test/unit/SystemWebhookService.ts
create mode 100644 packages/frontend/src/components/MkDivider.vue
create mode 100644 packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
create mode 100644 packages/frontend/src/components/MkSystemWebhookEditor.vue
create mode 100644 packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
create mode 100644 packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
create mode 100644 packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
create mode 100644 packages/frontend/src/pages/admin/system-webhook.item.vue
create mode 100644 packages/frontend/src/pages/admin/system-webhook.vue
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f93811c606..8b70636d82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## Unreleased
### General
+- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
### Client
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 0b1b86d373..acdc1fc421 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9305,6 +9305,10 @@ export interface Locale extends ILocale {
* Webhookを作成
*/
"createWebhook": string;
+ /**
+ * Webhookを編集
+ */
+ "modifyWebhook": string;
/**
* 名前
*/
@@ -9351,6 +9355,72 @@ export interface Locale extends ILocale {
*/
"mention": string;
};
+ "_systemEvents": {
+ /**
+ * ユーザーから通報があったとき
+ */
+ "abuseReport": string;
+ /**
+ * ユーザーからの通報を処理したとき
+ */
+ "abuseReportResolved": string;
+ };
+ /**
+ * Webhookを削除しますか?
+ */
+ "deleteConfirm": string;
+ };
+ "_abuseReport": {
+ "_notificationRecipient": {
+ /**
+ * 通報の通知先を追加
+ */
+ "createRecipient": string;
+ /**
+ * 通報の通知先を編集
+ */
+ "modifyRecipient": string;
+ /**
+ * 通知先の種類
+ */
+ "recipientType": string;
+ "_recipientType": {
+ /**
+ * メール
+ */
+ "mail": string;
+ /**
+ * Webhook
+ */
+ "webhook": string;
+ "_captions": {
+ /**
+ * モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)
+ */
+ "mail": string;
+ /**
+ * 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)
+ */
+ "webhook": string;
+ };
+ };
+ /**
+ * キーワード
+ */
+ "keywords": string;
+ /**
+ * 通知先ユーザー
+ */
+ "notifiedUser": string;
+ /**
+ * 使用するWebhook
+ */
+ "notifiedWebhook": string;
+ /**
+ * 通知先を削除しますか?
+ */
+ "deleteConfirm": string;
+ };
};
"_moderationLogTypes": {
/**
@@ -9497,6 +9567,30 @@ export interface Locale extends ILocale {
* ユーザーのバナーを解除
*/
"unsetUserBanner": string;
+ /**
+ * SystemWebhookを作成
+ */
+ "createSystemWebhook": string;
+ /**
+ * SystemWebhookを更新
+ */
+ "updateSystemWebhook": string;
+ /**
+ * SystemWebhookを削除
+ */
+ "deleteSystemWebhook": string;
+ /**
+ * 通報の通知先を作成
+ */
+ "createAbuseReportNotificationRecipient": string;
+ /**
+ * 通報の通知先を更新
+ */
+ "updateAbuseReportNotificationRecipient": string;
+ /**
+ * 通報の通知先を削除
+ */
+ "deleteAbuseReportNotificationRecipient": string;
};
"_fileViewer": {
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a89cfbd843..3ac1ce82a3 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2468,6 +2468,7 @@ _drivecleaner:
_webhookSettings:
createWebhook: "Webhookを作成"
+ modifyWebhook: "Webhookを編集"
name: "名前"
secret: "シークレット"
events: "Webhookを実行するタイミング"
@@ -2480,6 +2481,26 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
+ _systemEvents:
+ abuseReport: "ユーザーから通報があったとき"
+ abuseReportResolved: "ユーザーからの通報を処理したとき"
+ deleteConfirm: "Webhookを削除しますか?"
+
+_abuseReport:
+ _notificationRecipient:
+ createRecipient: "通報の通知先を追加"
+ modifyRecipient: "通報の通知先を編集"
+ recipientType: "通知先の種類"
+ _recipientType:
+ mail: "メール"
+ webhook: "Webhook"
+ _captions:
+ mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)"
+ webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)"
+ keywords: "キーワード"
+ notifiedUser: "通知先ユーザー"
+ notifiedWebhook: "使用するWebhook"
+ deleteConfirm: "通知先を削除しますか?"
_moderationLogTypes:
createRole: "ロールを作成"
@@ -2518,6 +2539,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
+ createSystemWebhook: "SystemWebhookを作成"
+ updateSystemWebhook: "SystemWebhookを更新"
+ deleteSystemWebhook: "SystemWebhookを削除"
+ createAbuseReportNotificationRecipient: "通報の通知先を作成"
+ updateAbuseReportNotificationRecipient: "通報の通知先を更新"
+ deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
_fileViewer:
title: "ファイルの詳細"
diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js
new file mode 100644
index 0000000000..4a754f81e2
--- /dev/null
+++ b/packages/backend/migration/1713656541000-abuse-report-notification.js
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AbuseReportNotification1713656541000 {
+ name = 'AbuseReportNotification1713656541000'
+
+ async up(queryRunner) {
+ await queryRunner.query(`
+ CREATE TABLE "system_webhook" (
+ "id" varchar(32) NOT NULL,
+ "isActive" boolean NOT NULL DEFAULT true,
+ "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "latestSentAt" timestamp with time zone NULL DEFAULT NULL,
+ "latestStatus" integer NULL DEFAULT NULL,
+ "name" varchar(255) NOT NULL,
+ "on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
+ "url" varchar(1024) NOT NULL,
+ "secret" varchar(1024) NOT NULL,
+ CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
+ );
+ CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
+ CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
+
+ CREATE TABLE "abuse_report_notification_recipient" (
+ "id" varchar(32) NOT NULL,
+ "isActive" boolean NOT NULL DEFAULT true,
+ "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" varchar(255) NOT NULL,
+ "method" varchar(64) NOT NULL,
+ "userId" varchar(32) NULL DEFAULT NULL,
+ "systemWebhookId" varchar(32) NULL DEFAULT NULL,
+ CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
+ CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
+ CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
+ CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
+ );
+ CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
+ CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
+ `);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
+ ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
+ DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
+ DROP INDEX "IDX_abuse_report_notification_recipient_method";
+ DROP INDEX "IDX_abuse_report_notification_recipient_userId";
+ DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
+ DROP TABLE "abuse_report_notification_recipient";
+
+ DROP INDEX "IDX_system_webhook_isActive";
+ DROP INDEX "IDX_system_webhook_on";
+ DROP TABLE "system_webhook";
+ `);
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..df752afcd8
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -0,0 +1,406 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
+import { Brackets, In, IsNull, Not } from 'typeorm';
+import * as Redis from 'ioredis';
+import sanitizeHtml from 'sanitize-html';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+import type {
+ AbuseReportNotificationRecipientRepository,
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiUser,
+} from '@/models/_.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportNotificationService implements OnApplicationShutdown {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ private idService: IdService,
+ private roleService: RoleService,
+ private systemWebhookService: SystemWebhookService,
+ private emailService: EmailService,
+ private metaService: MetaService,
+ private moderationLogService: ModerationLogService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ /**
+ * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
+ *
+ * @see RoleService.getModeratorIds
+ * @see GlobalEventService.publishAdminStream
+ */
+ @bindThis
+ public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const moderatorIds = await this.roleService.getModeratorIds(true, true);
+
+ for (const moderatorId of moderatorIds) {
+ for (const abuseReport of abuseReports) {
+ this.globalEventService.publishAdminStream(
+ moderatorId,
+ 'newAbuseUserReport',
+ {
+ id: abuseReport.id,
+ targetUserId: abuseReport.targetUserId,
+ reporterId: abuseReport.reporterId,
+ comment: abuseReport.comment,
+ },
+ );
+ }
+ }
+ }
+
+ /**
+ * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * メールアドレスの送信先は以下の通り.
+ * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
+ * - metaテーブルに設定されているメールアドレス
+ *
+ * @see EmailService.sendEmail
+ */
+ @bindThis
+ public async notifyMail(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
+ .filter(it => it.isActive && it.userProfile?.emailVerified)
+ .map(it => it.userProfile?.email)
+ .filter(isNotNull),
+ );
+
+ // 送信先の鮮度を保つため、毎回取得する
+ const meta = await this.metaService.fetch(true);
+ recipientEMailAddresses.push(
+ ...(meta.email ? [meta.email] : []),
+ );
+
+ if (recipientEMailAddresses.length <= 0) {
+ return;
+ }
+
+ for (const mailAddress of recipientEMailAddresses) {
+ await Promise.all(
+ abuseReports.map(it => {
+ // TODO: 送信処理はJobQueue化したい
+ return this.emailService.sendEmail(
+ mailAddress,
+ 'New Abuse Report',
+ sanitizeHtml(it.comment),
+ sanitizeHtml(it.comment),
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+ *
+ * @see SystemWebhookService.enqueueSystemWebhook
+ */
+ @bindThis
+ public async notifySystemWebhook(
+ abuseReports: MiAbuseUserReport[],
+ type: 'abuseReport' | 'abuseReportResolved',
+ ) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientWebhookIds = await this.fetchWebhookRecipients()
+ .then(it => it
+ .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
+ .map(it => it.systemWebhookId)
+ .filter(isNotNull));
+ for (const webhookId of recipientWebhookIds) {
+ await Promise.all(
+ abuseReports.map(it => {
+ return this.systemWebhookService.enqueueSystemWebhook(
+ webhookId,
+ type,
+ it,
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * 通報の通知先一覧を取得する.
+ *
+ * @param {Object} [params] クエリの取得条件
+ * @param {Object} [params.method] 取得する通知先の通知方法
+ * @param {Object} [opts] 動作時の詳細なオプション
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
+ * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchRecipients(
+ params?: {
+ ids?: MiAbuseReportNotificationRecipient['id'][],
+ method?: RecipientMethod[],
+ },
+ opts?: {
+ removeUnauthorized?: boolean,
+ joinUser?: boolean,
+ joinSystemWebhook?: boolean,
+ },
+ ): Promise {
+ const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
+
+ if (opts?.joinUser) {
+ query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
+ query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
+ }
+
+ if (opts?.joinSystemWebhook) {
+ query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
+ }
+
+ if (params?.ids) {
+ query.andWhere({ id: In(params.ids) });
+ }
+
+ if (params?.method) {
+ query.andWhere(new Brackets(qb => {
+ if (params.method?.includes('email')) {
+ qb.orWhere({ method: 'email', userId: Not(IsNull()) });
+ }
+ if (params.method?.includes('webhook')) {
+ qb.orWhere({ method: 'webhook', userId: IsNull() });
+ }
+ }));
+ }
+
+ const recipients = await query.getMany();
+ if (recipients.length <= 0) {
+ return [];
+ }
+
+ // アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
+ return (opts?.removeUnauthorized ?? true)
+ ? await this.removeUnauthorizedRecipientUsers(recipients)
+ : recipients;
+ }
+
+ /**
+ * EMailの通知先一覧を取得する.
+ * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
+ *
+ * @param {Object} [opts]
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchEMailRecipients(opts?: {
+ removeUnauthorized?: boolean
+ }): Promise {
+ return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
+ }
+
+ /**
+ * Webhookの通知先一覧を取得する.
+ * リレーション先の{@link MiSystemWebhook}も同時に取得する.
+ */
+ @bindThis
+ public fetchWebhookRecipients(): Promise {
+ return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
+ }
+
+ /**
+ * 通知先を作成する.
+ */
+ @bindThis
+ public async createRecipient(
+ params: {
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const id = this.idService.gen();
+ await this.abuseReportNotificationRecipientRepository.insert({
+ ...params,
+ id,
+ });
+
+ const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
+
+ this.moderationLogService
+ .log(updater, 'createAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: created,
+ })
+ .then();
+
+ return created;
+ }
+
+ /**
+ * 通知先を更新する.
+ */
+ @bindThis
+ public async updateRecipient(
+ params: {
+ id: MiAbuseReportNotificationRecipient['id'];
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ await this.abuseReportNotificationRecipientRepository.update(params.id, {
+ isActive: params.isActive,
+ updatedAt: new Date(),
+ name: params.name,
+ method: params.method,
+ userId: params.userId,
+ systemWebhookId: params.systemWebhookId,
+ });
+
+ const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ this.moderationLogService
+ .log(updater, 'updateAbuseReportNotificationRecipient', {
+ recipientId: params.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * 通知先を削除する.
+ */
+ @bindThis
+ public async deleteRecipient(
+ id: MiAbuseReportNotificationRecipient['id'],
+ updater: MiUser,
+ ) {
+ const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
+
+ await this.abuseReportNotificationRecipientRepository.delete(id);
+
+ this.moderationLogService
+ .log(updater, 'deleteAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: entity,
+ })
+ .then();
+ }
+
+ /**
+ * モデレータ権限を持たない(*1)通知先ユーザを削除する.
+ *
+ * *1: 以下の両方を満たすものの事を言う
+ * - 通知先にユーザIDが設定されている
+ * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
+ *
+ * @param recipients 通知先一覧の配列
+ * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
+ */
+ @bindThis
+ private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise {
+ const userRecipients = recipients.filter(it => it.userId !== null);
+ const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(isNotNull));
+ if (recipientUserIds.size <= 0) {
+ // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
+ return recipients;
+ }
+
+ // モデレータ権限の有無で通知先設定を振り分ける
+ const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
+ const authorizedUserRecipients = Array.of();
+ const unauthorizedUserRecipients = Array.of();
+ for (const recipient of userRecipients) {
+ // eslint-disable-next-line
+ if (authorizedUserIds.includes(recipient.userId!)) {
+ authorizedUserRecipients.push(recipient);
+ } else {
+ unauthorizedUserRecipients.push(recipient);
+ }
+ }
+
+ // モデレータ権限を持たない通知先をDBから削除する
+ if (unauthorizedUserRecipients.length > 0) {
+ await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
+ }
+ const nonUserRecipients = recipients.filter(it => it.userId === null);
+ return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'roleUpdated':
+ case 'roleDeleted':
+ case 'userRoleUnassigned': {
+ // 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
+ process.nextTick(async () => {
+ const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
+ userId: Not(IsNull()),
+ });
+ await this.removeUnauthorizedRecipientUsers(recipients);
+ });
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
new file mode 100644
index 0000000000..69c51509ba
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
+import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportService {
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ private idService: IdService,
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private queueService: QueueService,
+ private instanceActorService: InstanceActorService,
+ private apRendererService: ApRendererService,
+ private moderationLogService: ModerationLogService,
+ ) {
+ }
+
+ /**
+ * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
+ * - 管理者用Redisイベント
+ * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async report(params: {
+ targetUserId: MiAbuseUserReport['targetUserId'],
+ targetUserHost: MiAbuseUserReport['targetUserHost'],
+ reporterId: MiAbuseUserReport['reporterId'],
+ reporterHost: MiAbuseUserReport['reporterHost'],
+ comment: string,
+ }[]) {
+ const entities = params.map(param => {
+ return {
+ id: this.idService.gen(),
+ targetUserId: param.targetUserId,
+ targetUserHost: param.targetUserHost,
+ reporterId: param.reporterId,
+ reporterHost: param.reporterHost,
+ comment: param.comment,
+ };
+ });
+
+ const reports = Array.of();
+ for (const entity of entities) {
+ const report = await this.abuseUserReportsRepository.insertOne(entity);
+ reports.push(report);
+ }
+
+ return Promise.all([
+ this.abuseReportNotificationService.notifyAdminStream(reports),
+ this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
+ this.abuseReportNotificationService.notifyMail(reports),
+ ]);
+ }
+
+ /**
+ * 通報を解決し、その内容を下記の手段で管理者各位に通知する.
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @param operator 通報を処理したユーザ
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async resolve(
+ params: {
+ reportId: string;
+ forward: boolean;
+ }[],
+ operator: MiUser,
+ ) {
+ const paramsMap = new Map(params.map(it => [it.reportId, it]));
+ const reports = await this.abuseUserReportsRepository.findBy({
+ id: In(params.map(it => it.reportId)),
+ });
+
+ for (const report of reports) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const ps = paramsMap.get(report.id)!;
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ resolved: true,
+ assigneeId: operator.id,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ });
+
+ if (ps.forward && report.targetUserHost != null) {
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ // eslint-disable-next-line
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+ }
+
+ this.moderationLogService
+ .log(operator, 'resolveAbuseReport', {
+ reportId: report.id,
+ report: report,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ })
+ .then();
+ }
+
+ return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
+ .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
+ }
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index be80df6f1c..b5b34487ec 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -5,6 +5,13 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -56,7 +63,7 @@ import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
-import { WebhookService } from './WebhookService.js';
+import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
@@ -144,6 +151,8 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
+const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
+const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -196,7 +205,8 @@ const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
-const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
+const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
+const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -225,6 +235,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
+const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -258,6 +269,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
+const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -285,6 +297,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -337,7 +351,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -366,6 +381,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -399,6 +415,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -422,6 +439,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -474,7 +493,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -503,6 +523,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -536,6 +557,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -560,6 +582,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -612,7 +636,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -640,6 +665,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -673,6 +699,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -696,6 +723,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -748,7 +777,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -776,6 +806,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -809,6 +840,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 08f8f80a6e..435dbbae28 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -32,6 +33,7 @@ export class EmailService {
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
+ private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 90efd63f3a..a70743bed2 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -227,6 +228,9 @@ export interface InternalEventTypes {
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
+ systemWebhookCreated: MiSystemWebhook;
+ systemWebhookDeleted: MiSystemWebhook;
+ systemWebhookUpdated: MiSystemWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index e5580f36d1..0c9de117d2 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js';
import { QueueService } from '@/core/QueueService.js';
@@ -205,7 +205,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -606,7 +606,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'note', {
+ this.queueService.userWebhookDeliver(webhook, 'note', {
note: noteObj,
});
}
@@ -633,7 +633,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'reply', {
+ this.queueService.userWebhookDeliver(webhook, 'reply', {
note: noteObj,
});
}
@@ -656,7 +656,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'renote', {
+ this.queueService.userWebhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
@@ -788,7 +788,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'mention', {
+ this.queueService.userWebhookDeliver(webhook, 'mention', {
note: detailPackedNote,
});
}
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index 216734e9e5..b10b8e5899 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { QUEUE, baseQueueOptions } from '@/queue/const.js';
+import { baseQueueOptions, QUEUE } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
+import {
+ DeliverJobData,
+ EndedPollNotificationJobData,
+ InboxJobData,
+ RelationshipJobData,
+ UserWebhookDeliverJobData,
+ SystemWebhookDeliverJobData,
+} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
-import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue>;
export type EndedPollNotificationQueue = Bull.Queue;
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue;
export type ObjectStorageQueue = Bull.Queue;
-export type WebhookDeliverQueue = Bull.Queue;
+export type UserWebhookDeliverQueue = Bull.Queue;
+export type SystemWebhookDeliverQueue = Bull.Queue;
const $system: Provider = {
provide: 'queue:system',
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
inject: [DI.config],
};
-const $webhookDeliver: Provider = {
- provide: 'queue:webhookDeliver',
- useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
+const $userWebhookDeliver: Provider = {
+ provide: 'queue:userWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
+ inject: [DI.config],
+};
+
+const $systemWebhookDeliver: Provider = {
+ provide: 'queue:systemWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
exports: [
$system,
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
})
export class QueueModule implements OnApplicationShutdown {
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {}
public async dispose(): Promise {
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
- this.webhookDeliverQueue.close(),
+ this.userWebhookDeliverQueue.close(),
+ this.systemWebhookDeliverQueue.close(),
]);
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index c258a22927..80827a500b 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
+import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
-import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import type {
+ DbJobData,
+ DeliverJobData,
+ RelationshipJobData,
+ SystemWebhookDeliverJobData,
+ ThinUser,
+ UserWebhookDeliverJobData,
+} from '../queue/types.js';
+import type {
+ DbQueue,
+ DeliverQueue,
+ EndedPollNotificationQueue,
+ InboxQueue,
+ ObjectStorageQueue,
+ RelationshipQueue,
+ SystemQueue,
+ UserWebhookDeliverQueue,
+ SystemWebhookDeliverQueue,
+} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
-import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@@ -31,7 +49,8 @@ export class QueueService {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
@@ -431,9 +450,13 @@ export class QueueService {
});
}
+ /**
+ * @see UserWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
@bindThis
- public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
- const data = {
+ public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+ const data: UserWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
@@ -444,7 +467,33 @@ export class QueueService {
eventId: randomUUID(),
};
- return this.webhookDeliverQueue.add(webhook.id, data, {
+ return this.userWebhookDeliverQueue.add(webhook.id, data, {
+ attempts: 4,
+ backoff: {
+ type: 'custom',
+ },
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
+ /**
+ * @see SystemWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
+ @bindThis
+ public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ const data: SystemWebhookDeliverJobData = {
+ type,
+ content,
+ webhookId: webhook.id,
+ to: webhook.url,
+ secret: webhook.secret,
+ createdAt: Date.now(),
+ eventId: randomUUID(),
+ };
+
+ return this.systemWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index d6eea70297..e2ebecb99f 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -410,14 +410,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
- public async getModeratorIds(includeAdmins = true): Promise {
+ public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
- const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
- const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
- roleId: In(moderatorRoles.map(r => r.id)),
- }) : [];
+ const moderatorRoles = includeAdmins
+ ? roles.filter(r => r.isModerator || r.isAdministrator)
+ : roles.filter(r => r.isModerator);
+
// TODO: isRootなアカウントも含める
- return assigns.map(a => a.userId);
+ const assigns = moderatorRoles.length > 0
+ ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
+ : [];
+
+ const now = Date.now();
+ const result = [
+ // Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+ ...new Set(
+ assigns
+ .filter(it =>
+ (excludeExpire)
+ ? (it.expiresAt == null || it.expiresAt.getTime() > now)
+ : true,
+ )
+ .map(a => a.userId),
+ ),
+ ];
+
+ return result.sort((x, y) => x.localeCompare(y));
}
@bindThis
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
new file mode 100644
index 0000000000..bc6851f788
--- /dev/null
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -0,0 +1,233 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { IdService } from '@/core/IdService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import Logger from '@/logger.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class SystemWebhookService implements OnApplicationShutdown {
+ private logger: Logger;
+ private activeSystemWebhooksFetched = false;
+ private activeSystemWebhooks: MiSystemWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ private idService: IdService,
+ private queueService: QueueService,
+ private moderationLogService: ModerationLogService,
+ private loggerService: LoggerService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ this.logger = this.loggerService.getLogger('webhook');
+ }
+
+ @bindThis
+ public async fetchActiveSystemWebhooks() {
+ if (!this.activeSystemWebhooksFetched) {
+ this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
+ isActive: true,
+ });
+ this.activeSystemWebhooksFetched = true;
+ }
+
+ return this.activeSystemWebhooks;
+ }
+
+ /**
+ * SystemWebhook の一覧を取得する.
+ */
+ @bindThis
+ public async fetchSystemWebhooks(params?: {
+ ids?: MiSystemWebhook['id'][];
+ isActive?: MiSystemWebhook['isActive'];
+ on?: MiSystemWebhook['on'];
+ }): Promise {
+ const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
+ if (params) {
+ if (params.ids && params.ids.length > 0) {
+ query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
+ }
+ if (params.isActive !== undefined) {
+ query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
+ }
+ if (params.on && params.on.length > 0) {
+ query.andWhere(':on <@ systemWebhook.on', { on: params.on });
+ }
+ }
+
+ return query.getMany();
+ }
+
+ /**
+ * SystemWebhook を作成する.
+ */
+ @bindThis
+ public async createSystemWebhook(
+ params: {
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const id = this.idService.gen();
+ await this.systemWebhooksRepository.insert({
+ ...params,
+ id,
+ });
+
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
+ this.moderationLogService
+ .log(updater, 'createSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook: webhook,
+ })
+ .then();
+
+ return webhook;
+ }
+
+ /**
+ * SystemWebhook を更新する.
+ */
+ @bindThis
+ public async updateSystemWebhook(
+ params: {
+ id: MiSystemWebhook['id'];
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise {
+ const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
+ await this.systemWebhooksRepository.update(beforeEntity.id, {
+ updatedAt: new Date(),
+ isActive: params.isActive,
+ name: params.name,
+ on: params.on,
+ url: params.url,
+ secret: params.secret,
+ });
+
+ const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
+ this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
+ this.moderationLogService
+ .log(updater, 'updateSystemWebhook', {
+ systemWebhookId: beforeEntity.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * SystemWebhook を削除する.
+ */
+ @bindThis
+ public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ await this.systemWebhooksRepository.delete(id);
+
+ this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
+ this.moderationLogService
+ .log(updater, 'deleteSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook,
+ })
+ .then();
+ }
+
+ /**
+ * SystemWebhook をWebhook配送キューに追加する
+ * @see QueueService.systemWebhookDeliver
+ */
+ @bindThis
+ public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+ const webhookEntity = typeof webhook === 'string'
+ ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
+ : webhook;
+ if (!webhookEntity || !webhookEntity.isActive) {
+ this.logger.info(`Webhook is not active or not found : ${webhook}`);
+ return;
+ }
+
+ if (!webhookEntity.on.includes(type)) {
+ this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
+ return;
+ }
+
+ return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'systemWebhookCreated': {
+ if (body.isActive) {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ break;
+ }
+ case 'systemWebhookUpdated': {
+ if (body.isActive) {
+ const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
+ } else {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ } else {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'systemWebhookDeleted': {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 96f389b54c..2f1310b8ef 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 406ea04031..267a6a3f1b 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
@@ -331,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'follow', {
+ this.queueService.userWebhookDeliver(webhook, 'follow', {
user: packed,
});
}
@@ -345,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'followed', {
+ this.queueService.userWebhookDeliver(webhook, 'followed', {
user: packed,
});
}
@@ -398,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
@@ -740,7 +740,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packedFollowee,
});
}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
new file mode 100644
index 0000000000..e96bfeea95
--- /dev/null
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { WebhooksRepository } from '@/models/_.js';
+import type { MiWebhook } from '@/models/Webhook.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class UserWebhookService implements OnApplicationShutdown {
+ private activeWebhooksFetched = false;
+ private activeWebhooks: MiWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.webhooksRepository)
+ private webhooksRepository: WebhooksRepository,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ public async getActiveWebhooks() {
+ if (!this.activeWebhooksFetched) {
+ this.activeWebhooks = await this.webhooksRepository.findBy({
+ active: true,
+ });
+ this.activeWebhooksFetched = true;
+ }
+
+ return this.activeWebhooks;
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'webhookCreated': {
+ if (body.active) {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ break;
+ }
+ case 'webhookUpdated': {
+ if (body.active) {
+ const i = this.activeWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ };
+ } else {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ } else {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'webhookDeleted': {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
deleted file mode 100644
index 6be34977b0..0000000000
--- a/packages/backend/src/core/WebhookService.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import type { GlobalEvents } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
-
-@Injectable()
-export class WebhookService implements OnApplicationShutdown {
- private webhooksFetched = false;
- private webhooks: MiWebhook[] = [];
-
- constructor(
- @Inject(DI.redisForSub)
- private redisForSub: Redis.Redis,
-
- @Inject(DI.webhooksRepository)
- private webhooksRepository: WebhooksRepository,
- ) {
- //this.onMessage = this.onMessage.bind(this);
- this.redisForSub.on('message', this.onMessage);
- }
-
- @bindThis
- public async getActiveWebhooks() {
- if (!this.webhooksFetched) {
- this.webhooks = await this.webhooksRepository.findBy({
- active: true,
- });
- this.webhooksFetched = true;
- }
-
- return this.webhooks;
- }
-
- @bindThis
- private async onMessage(_: string, data: string): Promise {
- const obj = JSON.parse(data);
-
- if (obj.channel === 'internal') {
- const { type, body } = obj.message as GlobalEvents['internal']['payload'];
- switch (type) {
- case 'webhookCreated':
- if (body.active) {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- break;
- case 'webhookUpdated':
- if (body.active) {
- const i = this.webhooks.findIndex(a => a.id === body.id);
- if (i > -1) {
- this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- };
- } else {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- } else {
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- }
- break;
- case 'webhookDeleted':
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- break;
- default:
- break;
- }
- }
- }
-
- @bindThis
- public dispose(): void {
- this.redisForSub.off('message', this.onMessage);
- }
-
- @bindThis
- public onApplicationShutdown(signal?: string | undefined): void {
- this.dispose();
- }
-}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index d0d206760c..de3178b482 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -29,6 +29,7 @@ import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -57,9 +58,6 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- @Inject(DI.abuseUserReportsRepository)
- private abuseUserReportsRepository: AbuseUserReportsRepository,
-
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -68,6 +66,7 @@ export class ApInboxService {
private utilityService: UtilityService,
private idService: IdService,
private metaService: MetaService,
+ private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
private reactionService: ReactionService,
@@ -545,14 +544,13 @@ export class ApInboxService {
});
if (users.length < 1) return 'skip';
- await this.abuseUserReportsRepository.insert({
- id: this.idService.gen(),
+ await this.abuseReportService.report([{
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
- });
+ }]);
return 'ok';
}
diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
new file mode 100644
index 0000000000..6819afafd9
--- /dev/null
+++ b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+
+@Injectable()
+export class AbuseReportNotificationRecipientEntityService {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ private userEntityService: UserEntityService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
+ opts?: {
+ users: Map>,
+ webhooks: Map>,
+ },
+ ): Promise> {
+ const recipient = typeof src === 'object'
+ ? src
+ : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src });
+ const user = recipient.userId
+ ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId))
+ : undefined;
+ const webhook = recipient.systemWebhookId
+ ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId))
+ : undefined;
+
+ return {
+ id: recipient.id,
+ isActive: recipient.isActive,
+ updatedAt: recipient.updatedAt.toISOString(),
+ name: recipient.name,
+ method: recipient.method,
+ userId: recipient.userId ?? undefined,
+ user: user,
+ systemWebhookId: recipient.systemWebhookId ?? undefined,
+ systemWebhook: webhook,
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[],
+ ): Promise[]> {
+ const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object');
+ const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ objs.push(
+ ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ const userIds = objs.map(it => it.userId).filter(isNotNull);
+ const users: Map> = (userIds.length > 0)
+ ? await this.userEntityService.packMany(userIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(isNotNull);
+ const systemWebhooks: Map> = (systemWebhookIds.length > 0)
+ ? await this.systemWebhookEntityService.packMany(systemWebhookIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ return Promise
+ .all(
+ objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
new file mode 100644
index 0000000000..e18734091c
--- /dev/null
+++ b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+
+@Injectable()
+export class SystemWebhookEntityService {
+ constructor(
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiSystemWebhook['id'] | MiSystemWebhook,
+ opts?: {
+ webhooks: Map
+ },
+ ): Promise> {
+ const webhook = typeof src === 'object'
+ ? src
+ : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src });
+
+ return {
+ id: webhook.id,
+ isActive: webhook.isActive,
+ updatedAt: webhook.updatedAt.toISOString(),
+ latestSentAt: webhook.latestSentAt?.toISOString() ?? null,
+ latestStatus: webhook.latestStatus,
+ name: webhook.name,
+ on: webhook.on,
+ url: webhook.url,
+ secret: webhook.secret,
+ };
+ }
+
+ @bindThis
+ public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise[]> {
+ if (src.length === 0) {
+ return [];
+ }
+
+ const webhooks = Array.of();
+ webhooks.push(
+ ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'),
+ );
+
+ const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ webhooks.push(
+ ...await this.systemWebhooksRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ return Promise
+ .all(
+ webhooks.map(x =>
+ this.pack(x, {
+ webhooks: new Map(webhooks.map(x => [x.id, x])),
+ }),
+ ),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 919f4794a3..271082b4ff 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -49,6 +49,7 @@ export const DI = {
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'),
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
+ abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
authSessionsRepository: Symbol('authSessionsRepository'),
accessTokensRepository: Symbol('accessTokensRepository'),
@@ -70,6 +71,7 @@ export const DI = {
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
+ systemWebhooksRepository: Symbol('systemWebhooksRepository'),
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 41e5bfe9e4..a721b8663c 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -4,12 +4,12 @@
*/
import {
- packedUserLiteSchema,
- packedUserDetailedNotMeOnlySchema,
packedMeDetailedOnlySchema,
- packedUserDetailedNotMeSchema,
packedMeDetailedSchema,
+ packedUserDetailedNotMeOnlySchema,
+ packedUserDetailedNotMeSchema,
packedUserDetailedSchema,
+ packedUserLiteSchema,
packedUserSchema,
} from '@/models/json-schema/user.js';
import { packedNoteSchema } from '@/models/json-schema/note.js';
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
-import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
+import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@@ -38,25 +38,27 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
import {
- packedRoleLiteSchema,
- packedRoleSchema,
- packedRolePoliciesSchema,
+ packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaLogicsSchema,
- packedRoleCondFormulaValueNot,
- packedRoleCondFormulaValueIsLocalOrRemoteSchema,
packedRoleCondFormulaValueAssignedRoleSchema,
packedRoleCondFormulaValueCreatedSchema,
- packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
+ packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+ packedRoleCondFormulaValueNot,
packedRoleCondFormulaValueSchema,
packedRoleCondFormulaValueUserSettingBooleanSchema,
+ packedRoleLiteSchema,
+ packedRolePoliciesSchema,
+ packedRoleSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
-import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
+import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js';
import {
- packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
+ packedMetaLiteSchema,
} from '@/models/json-schema/meta.js';
+import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
+import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -111,6 +113,8 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
+ SystemWebhook: packedSystemWebhookSchema,
+ AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
};
export type Packed = SchemaType;
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
new file mode 100644
index 0000000000..fbff880afc
--- /dev/null
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
+import { MiUserProfile } from '@/models/UserProfile.js';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+/**
+ * 通報受信時に通知を送信する方法.
+ */
+export type RecipientMethod = 'email' | 'webhook';
+
+@Entity('abuse_report_notification_recipient')
+export class MiAbuseReportNotificationRecipient {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index()
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * 通知方法.
+ */
+ @Index()
+ @Column('varchar', {
+ length: 64,
+ })
+ public method: RecipientMethod;
+
+ /**
+ * 通知先のユーザID.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public userId: MiUser['id'] | null;
+
+ /**
+ * 通知先のユーザ.
+ */
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
+ public user: MiUser | null;
+
+ /**
+ * 通知先のユーザプロフィール.
+ */
+ @ManyToOne(type => MiUserProfile, {})
+ @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
+ public userProfile: MiUserProfile | null;
+
+ /**
+ * 通知先のシステムWebhookId.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public systemWebhookId: string | null;
+
+ /**
+ * 通知先のシステムWebhook.
+ */
+ @ManyToOne(type => MiSystemWebhook, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public systemWebhook: MiSystemWebhook | null;
+}
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index d3062d6b36..ea0f88baba 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -3,11 +3,83 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { Provider } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { 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 { Provider } from '@nestjs/common';
const $usersRepository: Provider = {
provide: DI.usersRepository,
@@ -225,6 +297,12 @@ const $abuseUserReportsRepository: Provider = {
inject: [DI.db],
};
+const $abuseReportNotificationRecipientRepository: Provider = {
+ provide: DI.abuseReportNotificationRecipientRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
+ inject: [DI.db],
+};
+
const $registrationTicketsRepository: Provider = {
provide: DI.registrationTicketsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository),
@@ -351,6 +429,12 @@ const $webhooksRepository: Provider = {
inject: [DI.db],
};
+const $systemWebhooksRepository: Provider = {
+ provide: DI.systemWebhooksRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
+ inject: [DI.db],
+};
+
const $adsRepository: Provider = {
provide: DI.adsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository),
@@ -412,8 +496,7 @@ const $reversiGamesRepository: Provider = {
};
@Module({
- imports: [
- ],
+ imports: [],
providers: [
$usersRepository,
$notesRepository,
@@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
$reversiGamesRepository,
],
})
-export class RepositoryModule {}
+export class RepositoryModule {
+}
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
new file mode 100644
index 0000000000..86fb323d1d
--- /dev/null
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -0,0 +1,98 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
+import { Serialized } from '@/types.js';
+import { id } from './util/id.js';
+
+export const systemWebhookEventTypes = [
+ // ユーザからの通報を受けたとき
+ 'abuseReport',
+ // 通報を処理したとき
+ 'abuseReportResolved',
+] as const;
+export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
+
+@Entity('system_webhook')
+export class MiSystemWebhook {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index('IDX_system_webhook_isActive', { synchronize: false })
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 最後に送信された日時.
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestSentAt: Date | null;
+
+ /**
+ * 最後に送信されたステータスコード
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public latestStatus: number | null;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * イベント種別.
+ */
+ @Index('IDX_system_webhook_on', { synchronize: false })
+ @Column('varchar', {
+ length: 128,
+ array: true,
+ default: '{}',
+ })
+ public on: SystemWebhookEventType[];
+
+ /**
+ * Webhook送信先のURL.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public url: string;
+
+ /**
+ * Webhook検証用の値.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public secret: string;
+
+ static deserialize(obj: Serialized): MiSystemWebhook {
+ return {
+ ...obj,
+ updatedAt: new Date(obj.updatedAt),
+ latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
+ };
+ }
+}
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 2e6a41586e..d366ce48d0 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -11,6 +11,7 @@ import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transfor
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
@@ -68,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiUserMemo } from '@/models/UserMemo.js';
import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiChannel } from '@/models/Channel.js';
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
import { MiRole } from '@/models/Role.js';
@@ -144,6 +146,7 @@ export const miRepository = {
export {
MiAbuseUserReport,
+ MiAbuseReportNotificationRecipient,
MiAccessToken,
MiAd,
MiAnnouncement,
@@ -201,6 +204,7 @@ export {
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
+ MiSystemWebhook,
MiChannel,
MiRetentionAggregation,
MiRole,
@@ -213,6 +217,7 @@ export {
};
export type AbuseUserReportsRepository = Repository & MiRepository;
+export type AbuseReportNotificationRecipientRepository = Repository & MiRepository;
export type AccessTokensRepository = Repository & MiRepository;
export type AdsRepository = Repository & MiRepository;
export type AnnouncementsRepository = Repository & MiRepository;
@@ -270,6 +275,7 @@ export type UserProfilesRepository = Repository & MiRepository & MiRepository;
export type UserSecurityKeysRepository = Repository & MiRepository;
export type WebhooksRepository = Repository & MiRepository;
+export type SystemWebhooksRepository = Repository & MiRepository;
export type ChannelsRepository = Repository & MiRepository;
export type RetentionAggregationsRepository = Repository & MiRepository;
export type RolesRepository = Repository & MiRepository;
diff --git a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
new file mode 100644
index 0000000000..6215f0f5a2
--- /dev/null
+++ b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
@@ -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;
diff --git a/packages/backend/src/models/json-schema/system-webhook.ts b/packages/backend/src/models/json-schema/system-webhook.ts
new file mode 100644
index 0000000000..d83065a743
--- /dev/null
+++ b/packages/backend/src/models/json-schema/system-webhook.ts
@@ -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;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index aa2aa5e373..251a03c303 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -5,13 +5,12 @@
// https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg';
-pg.types.setTypeParser(20, Number);
-
import { DataSource, Logger } from 'typeorm';
import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
@@ -69,6 +68,7 @@ import { MiUserProfile } from '@/models/UserProfile.js';
import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiChannel } from '@/models/Channel.js';
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
import { MiRole } from '@/models/Role.js';
@@ -83,6 +83,8 @@ import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+pg.types.setTypeParser(20, Number);
+
export const dbLogger = new MisskeyLogger('db');
const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
@@ -167,6 +169,7 @@ export const entities = [
MiHashtag,
MiSwSubscription,
MiAbuseUserReport,
+ MiAbuseReportNotificationRecipient,
MiRegistrationTicket,
MiSignin,
MiModerationLog,
@@ -185,6 +188,7 @@ export const entities = [
MiPasswordResetRequest,
MiUserPending,
MiWebhook,
+ MiSystemWebhook,
MiUserIp,
MiRetentionAggregation,
MiRole,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 8086158997..a1fd38fcc5 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -11,7 +11,8 @@ import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.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 { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
@@ -71,7 +72,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeleteFileProcessorService,
CleanRemoteFilesProcessorService,
RelationshipProcessorService,
- WebhookDeliverProcessorService,
+ UserWebhookDeliverProcessorService,
+ SystemWebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
DeliverProcessorService,
InboxProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 7bfe1f4caa..7bd74f3210 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -10,7 +10,8 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.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 { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
@@ -76,7 +77,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
private dbQueueWorker: Bull.Worker;
private deliverQueueWorker: Bull.Worker;
private inboxQueueWorker: Bull.Worker;
- private webhookDeliverQueueWorker: Bull.Worker;
+ private userWebhookDeliverQueueWorker: Bull.Worker;
+ private systemWebhookDeliverQueueWorker: Bull.Worker;
private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
@@ -86,7 +88,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
private config: Config,
private queueLoggerService: QueueLoggerService,
- private webhookDeliverProcessorService: WebhookDeliverProcessorService,
+ private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
+ private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
@@ -160,13 +163,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
autorun: false,
});
- const systemLogger = this.logger.createSubLogger('system');
+ const logger = this.logger.createSubLogger('system');
this.systemQueueWorker
- .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('active', (job) => logger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => {
- systemLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
level: 'error',
@@ -174,8 +177,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
})
- .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -217,13 +220,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
autorun: false,
});
- const dbLogger = this.logger.createSubLogger('db');
+ const logger = this.logger.createSubLogger('db');
this.dbQueueWorker
- .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('active', (job) => logger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
- dbLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
level: 'error',
@@ -231,8 +234,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
})
- .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -257,13 +260,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
},
});
- const deliverLogger = this.logger.createSubLogger('deliver');
+ const logger = this.logger.createSubLogger('deliver');
this.deliverQueueWorker
- .on('active', (job) => deliverLogger.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('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) => {
- deliverLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
level: 'error',
@@ -271,8 +274,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
})
- .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -297,13 +300,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
},
});
- const inboxLogger = this.logger.createSubLogger('inbox');
+ const logger = this.logger.createSubLogger('inbox');
this.inboxQueueWorker
- .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
- .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
+ .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
+ .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => {
- inboxLogger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
level: 'error',
@@ -311,21 +314,21 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
})
- .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
- //#region webhook deliver
+ //#region user-webhook deliver
{
- this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => {
+ this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
if (this.config.sentryForBackend) {
- return Sentry.startSpan({ name: 'Queue: WebhookDeliver' }, () => this.webhookDeliverProcessorService.process(job));
+ return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
} else {
- return this.webhookDeliverProcessorService.process(job);
+ return this.userWebhookDeliverProcessorService.process(job);
}
}, {
- ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
+ ...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
autorun: false,
concurrency: 64,
limiter: {
@@ -337,22 +340,62 @@ export class QueueProcessorService implements OnApplicationShutdown {
},
});
- const webhookLogger = this.logger.createSubLogger('webhook');
+ const logger = this.logger.createSubLogger('user-webhook');
- this.webhookDeliverQueueWorker
- .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
+ this.userWebhookDeliverQueueWorker
+ .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) => {
- webhookLogger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
+ logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
- Sentry.captureMessage(`Queue: WebhookDeliver: ${err.message}`, {
+ Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
- .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
+ .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
@@ -384,13 +427,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
},
});
- const relationshipLogger = this.logger.createSubLogger('relationship');
+ const logger = this.logger.createSubLogger('relationship');
this.relationshipQueueWorker
- .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('active', (job) => logger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
- relationshipLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
level: 'error',
@@ -398,8 +441,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
})
- .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -425,13 +468,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
concurrency: 16,
});
- const objectStorageLogger = this.logger.createSubLogger('objectStorage');
+ const logger = this.logger.createSubLogger('objectStorage');
this.objectStorageQueueWorker
- .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('active', (job) => logger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
- objectStorageLogger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
+ logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
level: 'error',
@@ -439,8 +482,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
})
- .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@@ -467,7 +510,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker.run(),
this.deliverQueueWorker.run(),
this.inboxQueueWorker.run(),
- this.webhookDeliverQueueWorker.run(),
+ this.userWebhookDeliverQueueWorker.run(),
+ this.systemWebhookDeliverQueueWorker.run(),
this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
@@ -481,7 +525,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker.close(),
this.deliverQueueWorker.close(),
this.inboxQueueWorker.close(),
- this.webhookDeliverQueueWorker.close(),
+ this.userWebhookDeliverQueueWorker.close(),
+ this.systemWebhookDeliverQueueWorker.close(),
this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts
index 132e916612..67f689b618 100644
--- a/packages/backend/src/queue/const.ts
+++ b/packages/backend/src/queue/const.ts
@@ -14,7 +14,8 @@ export const QUEUE = {
DB: 'db',
RELATIONSHIP: 'relationship',
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 {
diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
new file mode 100644
index 0000000000..f6bef52684
--- /dev/null
+++ b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
@@ -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): Promise {
+ 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;
+ }
+ }
+ }
+}
diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
similarity index 92%
rename from packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
rename to packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
index 8c260c0137..9ec630ef70 100644
--- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
@@ -13,10 +13,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type { WebhookDeliverJobData } from '../types.js';
+import { UserWebhookDeliverJobData } from '../types.js';
@Injectable()
-export class WebhookDeliverProcessorService {
+export class UserWebhookDeliverProcessorService {
private logger: Logger;
constructor(
@@ -33,7 +33,7 @@ export class WebhookDeliverProcessorService {
}
@bindThis
- public async process(job: Bull.Job): Promise {
+ public async process(job: Bull.Job): Promise {
try {
this.logger.debug(`delivering ${job.data.webhookId}`);
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index ce57ba745e..a4077a0547 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -106,7 +106,17 @@ export type EndedPollNotificationJobData = {
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;
content: unknown;
webhookId: MiWebhook['id'];
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index c645f4bcc6..41576bedaa 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -6,8 +6,13 @@
import { Module } from '@nestjs/common';
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_meta from './endpoints/admin/meta.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_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_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_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_show from './endpoints/announcements/show.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_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_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 };
@@ -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_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_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_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.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,
$admin_meta,
$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_delete,
$admin_accounts_findByEmail,
@@ -832,6 +857,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
+ $admin_systemWebhook_create,
+ $admin_systemWebhook_delete,
+ $admin_systemWebhook_list,
+ $admin_systemWebhook_show,
+ $admin_systemWebhook_update,
$announcements,
$announcements_show,
$antennas_create,
@@ -1129,6 +1159,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
exports: [
$admin_meta,
$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_delete,
$admin_accounts_findByEmail,
@@ -1203,6 +1238,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
+ $admin_systemWebhook_create,
+ $admin_systemWebhook_delete,
+ $admin_systemWebhook_list,
+ $admin_systemWebhook_show,
+ $admin_systemWebhook_update,
$announcements,
$announcements_show,
$antennas_create,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index a38c62f35a..3dfb7fdad4 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -6,8 +6,18 @@
import { permissions } from 'misskey-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_meta from './endpoints/admin/meta.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_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_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_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_updateInstance from './endpoints/admin/federation/update-instance.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_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_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_show from './endpoints/announcements/show.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 = [
['admin/meta', ep___admin_meta],
['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/delete', ep___admin_accounts_delete],
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
@@ -453,6 +474,11 @@ const eps = [
['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['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/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@@ -873,8 +899,12 @@ export interface IEndpoint {
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
return {
name: name,
- get meta() { return ep.meta ?? {}; },
- get params() { return ep.paramDef; },
+ get meta() {
+ return ep.meta ?? {};
+ },
+ get params() {
+ return ep.paramDef;
+ },
};
});
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
new file mode 100644
index 0000000000..bdfbcba518
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
@@ -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 { // 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);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
new file mode 100644
index 0000000000..b6dc44e09c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
@@ -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 { // eslint-disable-line import/no-default-export
+ constructor(
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.abuseReportNotificationService.deleteRecipient(
+ ps.id,
+ me,
+ );
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
new file mode 100644
index 0000000000..dad9161a8a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
@@ -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 { // 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);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
new file mode 100644
index 0000000000..557798f946
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
@@ -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 { // 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]);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
new file mode 100644
index 0000000000..bd4b485217
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { 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 { // 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);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index 9694b3fa40..d7f9e4eaa3 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
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 = {
tags: ['admin'],
@@ -53,7 +53,8 @@ export default class extends Endpoint { // eslint-
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@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) => {
const deliverJobCounts = await this.deliverQueue.getJobCounts();
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 8b0456068b..9b79100fcf 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -5,12 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js';
-import { InstanceActorService } from '@/core/InstanceActorService.js';
-import { QueueService } from '@/core/QueueService.js';
-import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import type { AbuseUserReportsRepository } from '@/models/_.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 = {
tags: ['admin'],
@@ -18,6 +16,16 @@ export const meta = {
requireCredential: true,
requireModerator: true,
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;
export const paramDef = {
@@ -29,47 +37,20 @@ export const paramDef = {
required: ['reportId'],
} as const;
-// TODO: ロジックをサービスに切り出す
-
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
-
- private queueService: QueueService,
- private instanceActorService: InstanceActorService,
- private apRendererService: ApRendererService,
- private moderationLogService: ModerationLogService,
+ private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
-
- if (report == null) {
- throw new Error('report not found');
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
}
- if (ps.forward && report.targetUserHost != null) {
- 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,
- });
+ await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
new file mode 100644
index 0000000000..28071e7a33
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
@@ -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 { // 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);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
new file mode 100644
index 0000000000..9cdfc7e70f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
@@ -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 { // eslint-disable-line import/no-default-export
+ constructor(
+ private systemWebhookService: SystemWebhookService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.systemWebhookService.deleteSystemWebhook(
+ ps.id,
+ me,
+ );
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
new file mode 100644
index 0000000000..7a440a774e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
@@ -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 { // 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);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
new file mode 100644
index 0000000000..75862c96a7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
@@ -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 { // 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]);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
new file mode 100644
index 0000000000..8d68bb8f87
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
@@ -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 { // 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);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 48e14b68cc..5ff6de37d2 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -3,17 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import sanitizeHtml from 'sanitize-html';
-import { Inject, Injectable } from '@nestjs/common';
-import type { AbuseUserReportsRepository } from '@/models/_.js';
-import { IdService } from '@/core/IdService.js';
+import { Injectable } from '@nestjs/common';
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 { RoleService } from '@/core/RoleService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -57,60 +51,32 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.abuseUserReportsRepository)
- private abuseUserReportsRepository: AbuseUserReportsRepository,
-
- private idService: IdService,
- private metaService: MetaService,
- private emailService: EmailService,
private getterService: GetterService,
private roleService: RoleService,
- private globalEventService: GlobalEventService,
+ private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
// 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);
throw err;
});
- if (user.id === me.id) {
+ if (targetUser.id === me.id) {
throw new ApiError(meta.errors.cannotReportYourself);
}
- if (await this.roleService.isAdministrator(user)) {
+ if (await this.roleService.isAdministrator(targetUser)) {
throw new ApiError(meta.errors.cannotReportAdmin);
}
- const report = await this.abuseUserReportsRepository.insertOne({
- id: this.idService.gen(),
- targetUserId: user.id,
- targetUserHost: user.host,
+ await this.abuseReportService.report([{
+ targetUserId: targetUser.id,
+ targetUserHost: targetUser.host,
reporterId: me.id,
reporterHost: null,
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));
- }
- });
+ }]);
});
}
}
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index ab03489c0d..f55790b636 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -25,7 +25,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.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 { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@@ -111,7 +120,8 @@ export class ClientServerService {
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@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);
}
@@ -239,7 +249,8 @@ export class ClientServerService {
this.inboxQueue,
this.dbQueue,
this.objectStorageQueue,
- this.webhookDeliverQueue,
+ this.userWebhookDeliverQueue,
+ this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
serverAdapter,
});
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 929070d0d2..ecbbee4eff 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -90,6 +90,12 @@ export const moderationLogTypes = [
'deleteAvatarDecoration',
'unsetUserAvatar',
'unsetUserBanner',
+ 'createSystemWebhook',
+ 'updateSystemWebhook',
+ 'deleteSystemWebhook',
+ 'createAbuseReportNotificationRecipient',
+ 'updateAbuseReportNotificationRecipient',
+ 'deleteAbuseReportNotificationRecipient',
] as const;
export type ModerationLogPayloads = {
@@ -282,6 +288,32 @@ export type ModerationLogPayloads = {
userHost: string | null;
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 = {
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
new file mode 100644
index 0000000000..b0cc3d13ec
--- /dev/null
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -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(postAction: () => Promise): Promise {
+ const fastify = Fastify();
+
+ let timeoutHandle: NodeJS.Timeout | null = null;
+ const result = await new Promise(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, credential?: UserToken): Promise {
+ 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, credential?: UserToken): Promise {
+ 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, credential?: UserToken): Promise {
+ const res = await api(
+ 'users/report-abuse',
+ {
+ userId: alice.id,
+ comment: randomString(),
+ ...args,
+ },
+ credential ?? admin,
+ );
+ return res.body;
+ }
+
+ async function resolveAbuseReport(args?: Partial, credential?: UserToken): Promise {
+ 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');
+ });
+ });
+});
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..e971659070
--- /dev/null
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -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;
+ let emailService: jest.Mocked;
+ let webhookService: jest.Mocked;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+ let alice: MiUser;
+ let bob: MiUser;
+ let systemWebhook1: MiSystemWebhook;
+ let systemWebhook2: MiSystemWebhook;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createUser(data: Partial = {}) {
+ 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 = {}) {
+ 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 = {}) {
+ 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;
+ emailService = app.get(EmailService) as jest.Mocked;
+ webhookService = app.get(SystemWebhookService) as jest.Mocked;
+
+ 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]);
+ });
+ });
+});
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index ec441735d7..69fa4162fb 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -3,8 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-
process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
@@ -13,7 +11,14 @@ import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import { GlobalModule } from '@/GlobalModule.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 { MetaService } from '@/core/MetaService.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 { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
@@ -39,27 +45,27 @@ describe('RoleService', () => {
let notificationService: jest.Mocked;
let clock: lolex.InstalledClock;
- function createUser(data: Partial = {}) {
+ async function createUser(data: Partial = {}) {
const un = secureRndstr(16);
- return usersRepository.insert({
+ const x = await usersRepository.insert({
id: genAidx(Date.now()),
username: un,
usernameLower: un,
...data,
- })
- .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+ });
+ return await usersRepository.findOneByOrFail(x.identifiers[0]);
}
- function createRole(data: Partial = {}) {
- return rolesRepository.insert({
+ async function createRole(data: Partial = {}) {
+ const x = await rolesRepository.insert({
id: genAidx(Date.now()),
updatedAt: new Date(),
lastUsedAt: new Date(),
name: '',
description: '',
...data,
- })
- .then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
+ });
+ return await rolesRepository.findOneByOrFail(x.identifiers[0]);
}
function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial = {}) {
@@ -71,6 +77,20 @@ describe('RoleService', () => {
});
}
+ async function assignRole(args: Partial) {
+ 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() {
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', () => {
test('~かつ~', async () => {
const [user1, user2, user3, user4] = await Promise.all([
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
new file mode 100644
index 0000000000..41b7f977ca
--- /dev/null
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -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;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createUser(data: Partial = {}) {
+ return await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createWebhook(data: Partial = {}) {
+ 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;
+
+ 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);
+ });
+ });
+ });
+ });
+});
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 3489255b91..25b003ba5a 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:type="type"
:name="name"
:value="value"
+ :disabled="disabled"
@click="emit('click', $event)"
@mousedown="onMousedown"
>
@@ -55,6 +56,7 @@ const props = defineProps<{
asLike?: boolean;
name?: string;
value?: string;
+ disabled?: boolean;
}>();
const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue
new file mode 100644
index 0000000000..e4e3af99e4
--- /dev/null
+++ b/packages/frontend/src/components/MkDivider.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index a19b45448b..721ac357f4 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@keydown.enter="toggle"
>
-
+
@@ -34,16 +34,19 @@ const props = defineProps<{
modelValue: boolean | Ref;
disabled?: boolean;
helpText?: string;
+ noBody?: boolean;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: boolean): void;
+ (ev: 'change', v: boolean): void;
}>();
const checked = toRefs(props).modelValue;
const toggle = () => {
if (props.disabled) return;
emit('update:modelValue', !checked.value);
+ emit('change', !checked.value);
};
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
new file mode 100644
index 0000000000..1222d3261d
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -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 {
+ 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;
+}
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
new file mode 100644
index 0000000000..007d841f00
--- /dev/null
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+ {{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
+
+
+
+
+
+ {{ i18n.ts._webhookSettings.name }}
+
+
+ URL
+
+
+ {{ i18n.ts._webhookSettings.secret }}
+
+
+ {{ i18n.ts._webhookSettings.events }}
+
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.abuseReport }}
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}
+
+
+
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts.ok }}
+
+ {{ i18n.ts.cancel }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
new file mode 100644
index 0000000000..ffe9c620d6
--- /dev/null
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -0,0 +1,307 @@
+
+
+
+
+
+ {{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }}
+
+
+
+
+
+ {{ i18n.ts.title }}
+
+
+ {{ i18n.ts._abuseReport._notificationRecipient.recipientType }}
+
+
+
+ {{ methodCaption }}
+
+
+
+
+ {{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}
+
+
+
+
+ {{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts.ok }}
+ {{ i18n.ts.cancel }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
new file mode 100644
index 0000000000..0b86808faf
--- /dev/null
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
{{ methodName }}
+
{{ entity.name }}
+
+
+ {{
+ `${i18n.ts._abuseReport._notificationRecipient.notifiedUser}: ` + ((user.name) ? `${user.name}(${user.username})` : user.username)
+ }}
+
+
+ {{ `${i18n.ts._abuseReport._notificationRecipient.notifiedWebhook}: ` + systemWebhook.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
new file mode 100644
index 0000000000..a52f8eb7af
--- /dev/null
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
+
+
+
+
+ {{ i18n.ts._abuseReport._notificationRecipient.recipientType }}
+
+
+
+
+
+ {{ i18n.ts._abuseReport._notificationRecipient.keywords }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index d2f4a4b531..9a9fa472a5 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -7,30 +7,33 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
-
-
- {{ i18n.ts.state }}
-
-
-
-
-
- {{ i18n.ts.reporteeOrigin }}
-
-
-
-
-
- {{ i18n.ts.reporterOrigin }}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -60,6 +61,7 @@ import MkPagination from '@/components/MkPagination.vue';
import XAbuseReport from '@/components/MkAbuseReport.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkButton from '@/components/MkButton.vue';
const reports = shallowRef>();
@@ -80,7 +82,7 @@ const pagination = {
};
function resolved(reportId) {
- reports.value.removeItem(reportId);
+ reports.value?.removeItem(reportId);
}
const headerActions = computed(() => []);
@@ -92,3 +94,26 @@ definePageMetadata(() => ({
icon: 'ti ti-exclamation-circle',
}));
+
+
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 794feae202..292f10da1a 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -214,6 +214,11 @@ const menuDef = computed(() => [{
text: i18n.ts.externalServices,
to: '/admin/external-services',
active: currentPage.value?.route.name === 'external-services',
+ }, {
+ icon: 'ti ti-webhook',
+ text: 'Webhook',
+ to: '/admin/system-webhook',
+ active: currentPage.value?.route.name === 'system-webhook',
}, {
icon: 'ti ti-adjustments',
text: i18n.ts.other,
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index e33c882721..91f1c7c5e6 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -8,9 +8,35 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._moderationLogTypes[log.type] }}
: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}
@@ -40,6 +66,12 @@ SPDX-License-Identifier: AGPL-3.0-only
: {{ log.info.avatarDecoration.name }}
: {{ log.info.before.name }}
: {{ log.info.avatarDecoration.name }}
+ : {{ log.info.webhook.name }}
+ : {{ log.info.before.name }}
+ : {{ log.info.webhook.name }}
+ : {{ log.info.recipient.name }}
+ : {{ log.info.before.name }}
+ : {{ log.info.recipient.name }}
@@ -116,6 +148,16 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+
+
+
+
+
+
+
raw
diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue
new file mode 100644
index 0000000000..0c07122af3
--- /dev/null
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+ {{ entity.name || entity.url }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue
new file mode 100644
index 0000000000..7a40eec944
--- /dev/null
+++ b/packages/frontend/src/pages/admin/system-webhook.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._webhookSettings.createWebhook }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index c12ae0fa57..8a443f627b 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -471,6 +471,14 @@ const routes: RouteDef[] = [{
path: '/invites',
name: 'invites',
component: page(() => import('@/pages/admin/invites.vue')),
+ }, {
+ path: '/abuse-report-notification-recipient',
+ name: 'abuse-report-notification-recipient',
+ component: page(() => import('@/pages/admin/abuse-report/notification-recipient.vue')),
+ }, {
+ path: '/system-webhook',
+ name: 'system-webhook',
+ component: page(() => import('@/pages/admin/system-webhook.vue')),
}, {
path: '/',
component: page(() => import('@/pages/_empty_.vue')),
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 6ff711cabb..bea89f2a7c 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -6,6 +6,11 @@
import { EventEmitter } from 'eventemitter3';
+// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
+
// @public (undocumented)
export type Acct = {
username: string;
@@ -21,13 +26,38 @@ declare namespace acct {
}
export { acct }
-// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
-//
// @public (undocumented)
type Ad = components['schemas']['Ad'];
// Warning: (ae-forgotten-export) The symbol "operations" needs to be exported by the entry point index.d.ts
//
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json'];
+
// @public (undocumented)
type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json'];
@@ -307,6 +337,33 @@ type AdminShowUsersResponse = operations['admin___show-users']['responses']['200
// @public (undocumented)
type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json'];
+// @public (undocumented)
+type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
+
// @public (undocumented)
type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
@@ -1133,6 +1190,15 @@ declare namespace entities {
AdminMetaResponse,
AdminAbuseUserReportsRequest,
AdminAbuseUserReportsResponse,
+ AdminAbuseReportNotificationRecipientListRequest,
+ AdminAbuseReportNotificationRecipientListResponse,
+ AdminAbuseReportNotificationRecipientShowRequest,
+ AdminAbuseReportNotificationRecipientShowResponse,
+ AdminAbuseReportNotificationRecipientCreateRequest,
+ AdminAbuseReportNotificationRecipientCreateResponse,
+ AdminAbuseReportNotificationRecipientUpdateRequest,
+ AdminAbuseReportNotificationRecipientUpdateResponse,
+ AdminAbuseReportNotificationRecipientDeleteRequest,
AdminAccountsCreateRequest,
AdminAccountsCreateResponse,
AdminAccountsDeleteRequest,
@@ -1228,6 +1294,15 @@ declare namespace entities {
AdminRolesUpdateDefaultPoliciesRequest,
AdminRolesUsersRequest,
AdminRolesUsersResponse,
+ AdminSystemWebhookCreateRequest,
+ AdminSystemWebhookCreateResponse,
+ AdminSystemWebhookDeleteRequest,
+ AdminSystemWebhookListRequest,
+ AdminSystemWebhookListResponse,
+ AdminSystemWebhookShowRequest,
+ AdminSystemWebhookShowResponse,
+ AdminSystemWebhookUpdateRequest,
+ AdminSystemWebhookUpdateResponse,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -1733,7 +1808,9 @@ declare namespace entities {
ReversiGameDetailed,
MetaLite,
MetaDetailedOnly,
- MetaDetailed
+ MetaDetailed,
+ SystemWebhook,
+ AbuseReportNotificationRecipient
}
}
export { entities }
@@ -2380,8 +2457,23 @@ type ModerationLog = {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
} | {
- type: 'unsetUserBanner';
- info: ModerationLogPayloads['unsetUserBanner'];
+ type: 'createSystemWebhook';
+ info: ModerationLogPayloads['createSystemWebhook'];
+} | {
+ type: 'updateSystemWebhook';
+ info: ModerationLogPayloads['updateSystemWebhook'];
+} | {
+ type: 'deleteSystemWebhook';
+ info: ModerationLogPayloads['deleteSystemWebhook'];
+} | {
+ type: 'createAbuseReportNotificationRecipient';
+ info: ModerationLogPayloads['createAbuseReportNotificationRecipient'];
+} | {
+ type: 'updateAbuseReportNotificationRecipient';
+ info: ModerationLogPayloads['updateAbuseReportNotificationRecipient'];
+} | {
+ type: 'deleteAbuseReportNotificationRecipient';
+ info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
});
// @public (undocumented)
@@ -2921,6 +3013,9 @@ type SwUpdateRegistrationRequest = operations['sw___update-registration']['reque
// @public (undocumented)
type SwUpdateRegistrationResponse = operations['sw___update-registration']['responses']['200']['content']['application/json'];
+// @public (undocumented)
+type SystemWebhook = components['schemas']['SystemWebhook'];
+
// @public (undocumented)
type TestRequest = operations['test']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 181f7274b7..e799d4a0c5 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -25,6 +25,66 @@ declare module '../api.js' {
credential?: string | null,
): Promise>;
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
/**
* No description provided.
*
@@ -840,6 +900,66 @@ declare module '../api.js' {
credential?: string | null,
): Promise>;
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
/**
* No description provided.
*
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index ab3baf1670..20c8509d4c 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -4,6 +4,15 @@ import type {
AdminMetaResponse,
AdminAbuseUserReportsRequest,
AdminAbuseUserReportsResponse,
+ AdminAbuseReportNotificationRecipientListRequest,
+ AdminAbuseReportNotificationRecipientListResponse,
+ AdminAbuseReportNotificationRecipientShowRequest,
+ AdminAbuseReportNotificationRecipientShowResponse,
+ AdminAbuseReportNotificationRecipientCreateRequest,
+ AdminAbuseReportNotificationRecipientCreateResponse,
+ AdminAbuseReportNotificationRecipientUpdateRequest,
+ AdminAbuseReportNotificationRecipientUpdateResponse,
+ AdminAbuseReportNotificationRecipientDeleteRequest,
AdminAccountsCreateRequest,
AdminAccountsCreateResponse,
AdminAccountsDeleteRequest,
@@ -99,6 +108,15 @@ import type {
AdminRolesUpdateDefaultPoliciesRequest,
AdminRolesUsersRequest,
AdminRolesUsersResponse,
+ AdminSystemWebhookCreateRequest,
+ AdminSystemWebhookCreateResponse,
+ AdminSystemWebhookDeleteRequest,
+ AdminSystemWebhookListRequest,
+ AdminSystemWebhookListResponse,
+ AdminSystemWebhookShowRequest,
+ AdminSystemWebhookShowResponse,
+ AdminSystemWebhookUpdateRequest,
+ AdminSystemWebhookUpdateResponse,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -558,6 +576,11 @@ import type {
export type Endpoints = {
'admin/meta': { req: EmptyRequest; res: AdminMetaResponse };
'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse };
+ 'admin/abuse-report/notification-recipient/list': { req: AdminAbuseReportNotificationRecipientListRequest; res: AdminAbuseReportNotificationRecipientListResponse };
+ 'admin/abuse-report/notification-recipient/show': { req: AdminAbuseReportNotificationRecipientShowRequest; res: AdminAbuseReportNotificationRecipientShowResponse };
+ 'admin/abuse-report/notification-recipient/create': { req: AdminAbuseReportNotificationRecipientCreateRequest; res: AdminAbuseReportNotificationRecipientCreateResponse };
+ 'admin/abuse-report/notification-recipient/update': { req: AdminAbuseReportNotificationRecipientUpdateRequest; res: AdminAbuseReportNotificationRecipientUpdateResponse };
+ 'admin/abuse-report/notification-recipient/delete': { req: AdminAbuseReportNotificationRecipientDeleteRequest; res: EmptyResponse };
'admin/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse };
'admin/accounts/delete': { req: AdminAccountsDeleteRequest; res: EmptyResponse };
'admin/accounts/find-by-email': { req: AdminAccountsFindByEmailRequest; res: AdminAccountsFindByEmailResponse };
@@ -632,6 +655,11 @@ export type Endpoints = {
'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse };
'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse };
'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse };
+ 'admin/system-webhook/create': { req: AdminSystemWebhookCreateRequest; res: AdminSystemWebhookCreateResponse };
+ 'admin/system-webhook/delete': { req: AdminSystemWebhookDeleteRequest; res: EmptyResponse };
+ 'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
+ 'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
+ 'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 02ca932d8a..357b5e9eaf 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -7,6 +7,15 @@ export type EmptyResponse = Record | undefined;
export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json'];
export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json'];
+export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json'];
export type AdminAccountsCreateRequest = operations['admin___accounts___create']['requestBody']['content']['application/json'];
export type AdminAccountsCreateResponse = operations['admin___accounts___create']['responses']['200']['content']['application/json'];
export type AdminAccountsDeleteRequest = operations['admin___accounts___delete']['requestBody']['content']['application/json'];
@@ -102,6 +111,15 @@ export type AdminRolesUnassignRequest = operations['admin___roles___unassign']['
export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin___roles___update-default-policies']['requestBody']['content']['application/json'];
export type AdminRolesUsersRequest = operations['admin___roles___users']['requestBody']['content']['application/json'];
export type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
+export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index a6e5fbe689..04574849d4 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -51,3 +51,5 @@ export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
export type MetaLite = components['schemas']['MetaLite'];
export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
export type MetaDetailed = components['schemas']['MetaDetailed'];
+export type SystemWebhook = components['schemas']['SystemWebhook'];
+export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 2c80676f3e..bdcc1dfd77 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -30,6 +30,56 @@ export type paths = {
*/
post: operations['admin___abuse-user-reports'];
};
+ '/admin/abuse-report/notification-recipient/list': {
+ /**
+ * admin/abuse-report/notification-recipient/list
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+ */
+ post: operations['admin___abuse-report___notification-recipient___list'];
+ };
+ '/admin/abuse-report/notification-recipient/show': {
+ /**
+ * admin/abuse-report/notification-recipient/show
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+ */
+ post: operations['admin___abuse-report___notification-recipient___show'];
+ };
+ '/admin/abuse-report/notification-recipient/create': {
+ /**
+ * admin/abuse-report/notification-recipient/create
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ post: operations['admin___abuse-report___notification-recipient___create'];
+ };
+ '/admin/abuse-report/notification-recipient/update': {
+ /**
+ * admin/abuse-report/notification-recipient/update
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ post: operations['admin___abuse-report___notification-recipient___update'];
+ };
+ '/admin/abuse-report/notification-recipient/delete': {
+ /**
+ * admin/abuse-report/notification-recipient/delete
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ post: operations['admin___abuse-report___notification-recipient___delete'];
+ };
'/admin/accounts/create': {
/**
* admin/accounts/create
@@ -697,6 +747,56 @@ export type paths = {
*/
post: operations['admin___roles___users'];
};
+ '/admin/system-webhook/create': {
+ /**
+ * admin/system-webhook/create
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ post: operations['admin___system-webhook___create'];
+ };
+ '/admin/system-webhook/delete': {
+ /**
+ * admin/system-webhook/delete
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ post: operations['admin___system-webhook___delete'];
+ };
+ '/admin/system-webhook/list': {
+ /**
+ * admin/system-webhook/list
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ post: operations['admin___system-webhook___list'];
+ };
+ '/admin/system-webhook/show': {
+ /**
+ * admin/system-webhook/show
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ post: operations['admin___system-webhook___show'];
+ };
+ '/admin/system-webhook/update': {
+ /**
+ * admin/system-webhook/update
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ post: operations['admin___system-webhook___update'];
+ };
'/announcements': {
/**
* announcements
@@ -4859,6 +4959,32 @@ export type components = {
cacheRemoteSensitiveFiles: boolean;
};
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
+ SystemWebhook: {
+ id: string;
+ isActive: boolean;
+ /** Format: date-time */
+ updatedAt: string;
+ /** Format: date-time */
+ latestSentAt: string | null;
+ latestStatus: number | null;
+ name: string;
+ on: ('abuseReport' | 'abuseReportResolved')[];
+ url: string;
+ secret: string;
+ };
+ AbuseReportNotificationRecipient: {
+ id: string;
+ isActive: boolean;
+ /** Format: date-time */
+ updatedAt: string;
+ name: string;
+ /** @enum {string} */
+ method: 'email' | 'webhook';
+ userId?: string;
+ user?: components['schemas']['UserLite'];
+ systemWebhookId?: string;
+ systemWebhook?: components['schemas']['SystemWebhook'];
+ };
};
responses: never;
parameters: never;
@@ -5125,6 +5251,292 @@ export type operations = {
};
};
};
+ /**
+ * admin/abuse-report/notification-recipient/list
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+ */
+ 'admin___abuse-report___notification-recipient___list': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ method?: ('email' | 'webhook')[];
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['AbuseReportNotificationRecipient'][];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/abuse-report/notification-recipient/show
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient*
+ */
+ 'admin___abuse-report___notification-recipient___show': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['AbuseReportNotificationRecipient'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/abuse-report/notification-recipient/create
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ 'admin___abuse-report___notification-recipient___create': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ isActive: boolean;
+ name: string;
+ /** @enum {string} */
+ method: 'email' | 'webhook';
+ /** Format: misskey:id */
+ userId?: string;
+ /** Format: misskey:id */
+ systemWebhookId?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['AbuseReportNotificationRecipient'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/abuse-report/notification-recipient/update
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ 'admin___abuse-report___notification-recipient___update': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ isActive: boolean;
+ name: string;
+ /** @enum {string} */
+ method: 'email' | 'webhook';
+ /** Format: misskey:id */
+ userId?: string;
+ /** Format: misskey:id */
+ systemWebhookId?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['AbuseReportNotificationRecipient'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/abuse-report/notification-recipient/delete
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient*
+ */
+ 'admin___abuse-report___notification-recipient___delete': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
/**
* admin/accounts/create
* @description No description provided.
@@ -9615,6 +10027,287 @@ export type operations = {
};
};
};
+ /**
+ * admin/system-webhook/create
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ 'admin___system-webhook___create': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ isActive: boolean;
+ name: string;
+ on: ('abuseReport' | 'abuseReportResolved')[];
+ url: string;
+ secret: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['SystemWebhook'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/system-webhook/delete
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ 'admin___system-webhook___delete': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/system-webhook/list
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ 'admin___system-webhook___list': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ isActive?: boolean;
+ on?: ('abuseReport' | 'abuseReportResolved')[];
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['SystemWebhook'][];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/system-webhook/show
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ 'admin___system-webhook___show': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['SystemWebhook'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/system-webhook/update
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook*
+ */
+ 'admin___system-webhook___update': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ id: string;
+ isActive: boolean;
+ name: string;
+ on: ('abuseReport' | 'abuseReportResolved')[];
+ url: string;
+ secret: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['SystemWebhook'];
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
/**
* announcements
* @description No description provided.
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index fd6ef4d68d..03b9069290 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -325,4 +325,30 @@ export type ModerationLogPayloads = {
userHost: string | null;
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;
+ };
};
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 35503d6d6f..7a84cb6a1a 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -132,8 +132,23 @@ export type ModerationLog = {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
} | {
- type: 'unsetUserBanner';
- info: ModerationLogPayloads['unsetUserBanner'];
+ type: 'createSystemWebhook';
+ info: ModerationLogPayloads['createSystemWebhook'];
+} | {
+ type: 'updateSystemWebhook';
+ info: ModerationLogPayloads['updateSystemWebhook'];
+} | {
+ type: 'deleteSystemWebhook';
+ info: ModerationLogPayloads['deleteSystemWebhook'];
+} | {
+ type: 'createAbuseReportNotificationRecipient';
+ info: ModerationLogPayloads['createAbuseReportNotificationRecipient'];
+} | {
+ type: 'updateAbuseReportNotificationRecipient';
+ info: ModerationLogPayloads['updateAbuseReportNotificationRecipient'];
+} | {
+ type: 'deleteAbuseReportNotificationRecipient';
+ info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
});
export type ServerStats = {
From 9849aab40283cbde2184e74d4795aec8ef8ccba3 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Sat, 8 Jun 2024 18:00:54 +0900
Subject: [PATCH 14/49] test(#10336): add `components/MkC.*` stories (#13830)
* test(storybook): add `components/MkC.*` stories
* test(storybook): add some tests
* test: add sleep
* test: comment-out flaky test
* test(storybook): add test for `MkChannelFollowButton`
* chore(storybook): tweak sleep duration in `MkChannelFollowButton` story test
* fix(chromatic): add delay to `MkChannelList`
* chore: replace `mswDecorator` with `mswLoader`
* fix(storybook): tweak some parameters
* chore: serve static files
* fix(chromatic): add delay to `MkCwButton`
* chore: delete logging for debug
* fix: add right click in `MkContextMenu` play
* refactor: remove unused imports
---
packages/frontend/.storybook/fakes.ts | 60 +++++++++
packages/frontend/.storybook/generate.tsx | 2 +-
packages/frontend/.storybook/main.ts | 1 +
packages/frontend/.storybook/preview.ts | 4 +-
packages/frontend/package.json | 2 +
.../MkChannelFollowButton.stories.impl.ts | 77 ++++++++++++
.../src/components/MkChannelFollowButton.vue | 5 +-
.../components/MkChannelList.stories.impl.ts | 65 ++++++++++
.../MkChannelPreview.stories.impl.ts | 43 +++++++
.../src/components/MkChart.stories.impl.ts | 117 ++++++++++++++++++
packages/frontend/src/components/MkChart.vue | 90 ++++++++------
.../components/MkChartLegend.stories.impl.ts | 7 ++
.../components/MkChartTooltip.stories.impl.ts | 7 ++
.../components/MkClickerGame.stories.impl.ts | 79 ++++++++++++
.../frontend/src/components/MkClickerGame.vue | 2 +-
.../components/MkClipPreview.stories.impl.ts | 43 +++++++
.../components/MkCode.core.stories.impl.ts | 7 ++
.../src/components/MkCode.stories.impl.ts | 44 +++++++
.../components/MkCodeEditor.stories.impl.ts | 62 ++++++++++
.../components/MkCodeInline.stories.impl.ts | 37 ++++++
.../components/MkColorInput.stories.impl.ts | 50 ++++++++
.../components/MkContainer.stories.impl.ts | 7 ++
.../components/MkContextMenu.stories.impl.ts | 58 +++++++++
.../MkCropperDialog.stories.impl.ts | 75 +++++++++++
...kCustomEmojiDetailedDialog.stories.impl.ts | 38 ++++++
.../src/components/MkCwButton.stories.impl.ts | 89 +++++++++++++
packages/frontend/src/scripts/test-utils.ts | 10 ++
pnpm-lock.yaml | 88 +++++++------
28 files changed, 1083 insertions(+), 86 deletions(-)
create mode 100644 packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkChannelList.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkChannelPreview.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkChart.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkChartLegend.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkChartTooltip.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkClickerGame.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkClipPreview.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCode.core.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCode.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCodeEditor.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCodeInline.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkColorInput.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkContainer.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkContextMenu.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCropperDialog.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts
create mode 100644 packages/frontend/src/components/MkCwButton.stories.impl.ts
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 3a24ccb248..fdb155261b 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -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) {
return {
id: 'somepostid',
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index d74c83a500..d21eea9d17 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -397,7 +397,7 @@ function toStories(component: string): Promise {
const globs = await Promise.all([
glob('src/components/global/Mk*.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/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'),
diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts
index d3822942cd..9f318cf449 100644
--- a/packages/frontend/.storybook/main.ts
+++ b/packages/frontend/.storybook/main.ts
@@ -15,6 +15,7 @@ const _dirname = fileURLToPath(new URL('.', import.meta.url));
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+ staticDirs: [{ from: '../assets', to: '/client-assets' }],
addons: [
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@storybook/addon-interactions'),
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 982a2979ac..73ee007fb8 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -7,7 +7,7 @@ import { FORCE_REMOUNT } from '@storybook/core-events';
import { addons } from '@storybook/preview-api';
import { type Preview, setup } from '@storybook/vue3';
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 locale from './locale.js';
import { commonHandlers, onUnhandledRequest } from './mocks.js';
@@ -122,7 +122,6 @@ const preview = {
}
return story;
},
- mswDecorator,
(Story, context) => {
return {
setup() {
@@ -137,6 +136,7 @@ const preview = {
};
},
],
+ loaders: [mswLoader],
parameters: {
controls: {
exclude: /^__/,
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 56b824c0c5..66940a1601 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -104,6 +104,7 @@
"@types/node": "20.12.7",
"@types/punycode": "2.1.4",
"@types/sanitize-html": "2.11.0",
+ "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "9.0.8",
@@ -128,6 +129,7 @@
"prettier": "3.2.5",
"react": "18.3.1",
"react-dom": "18.3.1",
+ "seedrandom": "3.0.5",
"start-server-and-test": "2.0.3",
"storybook": "8.0.9",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
new file mode 100644
index 0000000000..b99620da22
--- /dev/null
+++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
@@ -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: '',
+ };
+ },
+ args: {
+ channel: channel(),
+ full: true,
+ },
+ async play({ canvasElement }) {
+ await s.acquire();
+ await sleep(1000);
+ const canvas = within(canvasElement);
+ const buttonElement = canvas.getByRole('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;
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 6b1b380e41..841d37a568 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -26,17 +26,18 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index a5578d4de6..ae39098305 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -82,16 +82,19 @@ defineExpose({
.body {
text-overflow: ellipsis;
overflow: clip;
+ margin-left: -10px;
+ padding: 10px;
}
.name {
color: #fff;
- filter: drop-shadow(0 0 4px #000);
+ filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5));
font-weight: bold;
}
.username {
color: #fff;
- filter: drop-shadow(0 0 4px #000);
+ filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5));
+ font-weight: normal;
}
From ac12ab8629f0a0172250f949a98ee1efb1d0890d Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Sat, 22 Jun 2024 12:51:02 +0900
Subject: [PATCH 46/49] =?UTF-8?q?fix(backend):=20=E3=83=95=E3=82=A3?=
=?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=AE=E3=83=8E=E3=83=BC=E3=83=88=E3=81=AE?=
=?UTF-8?q?MFM=E3=81=AFHTML=E3=81=AB=E3=83=AC=E3=83=B3=E3=83=80=E3=83=BC?=
=?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=8B=E3=82=89=E8=BF=94=E3=81=99=20(#1400?=
=?UTF-8?q?6)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(backend): フィードのノートのMFMはHTMLにレンダーしてから返す (test wip)
* chore: beforeEachを使う?
* fix: プレーンテキストにフォールバックしてMFMが含まれていないか調べる方針を実装
* fix: application/jsonだとパースされるのでその作用をキャンセル
* build: fix lint error
* docs: update CHANGELOG.md
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 1 +
packages/backend/src/server/web/FeedService.ts | 6 +++++-
packages/backend/test/e2e/fetch-resource.ts | 17 +++++++++++++++++
packages/backend/test/utils.ts | 5 +++--
4 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5eb698385..ab9f5f8000 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
+- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts
index 10e3ed2682..9d810ddc84 100644
--- a/packages/backend/src/server/web/FeedService.ts
+++ b/packages/backend/src/server/web/FeedService.ts
@@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { MfmService } from "@/core/MfmService.js";
+import { parse as mfmParse } from 'mfm-js';
@Injectable()
export class FeedService {
@@ -33,6 +35,7 @@ export class FeedService {
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
+ private mfmService: MfmService,
) {
}
@@ -76,13 +79,14 @@ export class FeedService {
id: In(note.fileIds),
}) : [];
const file = files.find(file => file.type.startsWith('image/'));
+ const text = note.text;
feed.addItem({
title: `New note by ${author.name}`,
link: `${this.config.url}/notes/${note.id}`,
date: this.idService.parse(note.id).date,
description: note.cw ?? undefined,
- content: note.text ?? undefined,
+ content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
});
}
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index 4851ed14be..7efd688ec2 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -153,6 +153,23 @@ describe('Webリソース', () => {
path: path('nonexisting'),
status: 404,
}));
+
+ describe(' has entry such ', () => {
+ beforeEach(() => {
+ post(alice, { text: "**a**" })
+ });
+
+ test('MFMを含まない。', async () => {
+ const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text());
+ const _body: unknown = content.body;
+ // JSONフィードのときは改めて文字列化する
+ const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string;
+
+ if (body.includes("**a**")) {
+ throw new Error("MFM shouldn't be included");
+ }
+ });
+ })
});
describe.each([{ path: '/api/foo' }])('$path', ({ path }) => {
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 86814fffe0..aad4ab37c9 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -17,6 +17,7 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js';
+import { type Response } from 'node-fetch';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@@ -454,7 +455,7 @@ export type SimpleGetResponse = {
type: string | null,
location: string | null
};
-export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise => {
+export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise = _ => Promise.resolve(null)): Promise => {
const res = await relativeFetch(path, {
headers: {
Accept: accept,
@@ -482,7 +483,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
const body =
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
- null;
+ await bodyExtractor(res);
return {
status: res.status,
From b50eb511b0cf6fb05d37c3370726f940c1438a99 Mon Sep 17 00:00:00 2001
From: yupix
Date: Sat, 22 Jun 2024 14:52:27 +0900
Subject: [PATCH 47/49] =?UTF-8?q?refactor:=20api/*/update=E7=B3=BB?=
=?UTF-8?q?=E3=81=AE=E5=BF=85=E9=A0=88=E3=82=AD=E3=83=BC=E3=82=92=E6=9C=80?=
=?UTF-8?q?=E4=BD=8E=E9=99=90=E3=81=AB=20(#13824)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor: clips/updateの必須キーをclipIdのみに
* refactor: admin/roles/update の必須キーをroleIdのみに
* feat: pages/update の必須キーをpageIdのみに
* refactor: gallery/posts/update の必須キーをpostidのみに
* feat: misskey-jsの型を更新
* feat: i/webhooks/updateの必須キーをwebhookIdのみに
* feat: admin/ad/updateの必須キーをidのみに
* feat: misskey-jsの型を更新
* chore: update CHANGELOG.md
* docs: update CHANGELOG.md
* fix: secretが更新できなくなる場合がある
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
* Update packages/backend/src/server/api/endpoints/gallery/posts/update.ts
---------
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 6 ++
.../server/api/endpoints/admin/ad/update.ts | 6 +-
.../api/endpoints/admin/roles/update.ts | 14 ----
.../src/server/api/endpoints/clips/update.ts | 4 +-
.../api/endpoints/gallery/posts/update.ts | 24 ++++---
.../server/api/endpoints/i/webhooks/update.ts | 6 +-
.../src/server/api/endpoints/pages/update.ts | 23 ++----
packages/misskey-js/src/autogen/types.ts | 71 +++++++++----------
8 files changed, 70 insertions(+), 84 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab9f5f8000..c1af63ad23 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,12 @@
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
+- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
+- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
+- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
+- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
+- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
+- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
index 62358457ff..4e3d731aca 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -40,7 +40,7 @@ export const paramDef = {
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
},
- required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
+ required: ['id'],
} as const;
@Injectable()
@@ -63,8 +63,8 @@ export default class extends Endpoint { // eslint-
ratio: ps.ratio,
memo: ps.memo,
imageUrl: ps.imageUrl,
- expiresAt: new Date(ps.expiresAt),
- startsAt: new Date(ps.startsAt),
+ expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
+ startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
});
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 5242e0be2f..465ad7aaaf 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
@@ -50,19 +49,6 @@ export const paramDef = {
},
required: [
'roleId',
- 'name',
- 'description',
- 'color',
- 'iconUrl',
- 'target',
- 'condFormula',
- 'isPublic',
- 'isModerator',
- 'isAdministrator',
- 'asBadge',
- 'canEditMembersByModerator',
- 'displayOrder',
- 'policies',
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
index 3b44ba81b3..603a3ccf3d 100644
--- a/packages/backend/src/server/api/endpoints/clips/update.ts
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ClipService } from '@/core/ClipService.js';
@@ -41,7 +41,7 @@ export const paramDef = {
isPublic: { type: 'boolean' },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
},
- required: ['clipId', 'name'],
+ required: ['clipId'],
} as const;
@Injectable()
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
index 2f977784ec..5243ee9603 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
@@ -47,7 +47,7 @@ export const paramDef = {
} },
isSensitive: { type: 'boolean', default: false },
},
- required: ['postId', 'title', 'fileIds'],
+ required: ['postId'],
} as const;
@Injectable()
@@ -62,15 +62,19 @@ export default class extends Endpoint { // eslint-
private galleryPostEntityService: GalleryPostEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const files = (await Promise.all(ps.fileIds.map(fileId =>
- this.driveFilesRepository.findOneBy({
- id: fileId,
- userId: me.id,
- }),
- ))).filter(x => x != null);
+ let files: Array | undefined;
- if (files.length === 0) {
- throw new Error();
+ if (ps.fileIds) {
+ files = (await Promise.all(ps.fileIds.map(fileId =>
+ this.driveFilesRepository.findOneBy({
+ id: fileId,
+ userId: me.id,
+ }),
+ ))).filter(x => x != null);
+
+ if (files.length === 0) {
+ throw new Error();
+ }
}
await this.galleryPostsRepository.update({
@@ -81,7 +85,7 @@ export default class extends Endpoint { // eslint-
title: ps.title,
description: ps.description,
isSensitive: ps.isSensitive,
- fileIds: files.map(file => file.id),
+ fileIds: files ? files.map(file => file.id) : undefined,
});
const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
index 6e380d76f8..07a25bd82a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
@@ -34,13 +34,13 @@ export const paramDef = {
webhookId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 },
- secret: { type: 'string', maxLength: 1024, default: '' },
+ secret: { type: 'string', nullable: true, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
} },
active: { type: 'boolean' },
},
- required: ['webhookId', 'name', 'url', 'on', 'active'],
+ required: ['webhookId'],
} as const;
// TODO: ロジックをサービスに切り出す
@@ -66,7 +66,7 @@ export default class extends Endpoint { // eslint-
await this.webhooksRepository.update(webhook.id, {
name: ps.name,
url: ps.url,
- secret: ps.secret,
+ secret: ps.secret === null ? '' : ps.secret,
on: ps.on,
active: ps.active,
});
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index b8e5e70a25..f11bbbcb1a 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -70,7 +70,7 @@ export const paramDef = {
alignCenter: { type: 'boolean' },
hideTitleWhenPinned: { type: 'boolean' },
},
- required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
+ required: ['pageId'],
} as const;
@Injectable()
@@ -91,9 +91,8 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
- let eyeCatchingImage = null;
if (ps.eyeCatchingImageId != null) {
- eyeCatchingImage = await this.driveFilesRepository.findOneBy({
+ const eyeCatchingImage = await this.driveFilesRepository.findOneBy({
id: ps.eyeCatchingImageId,
userId: me.id,
});
@@ -116,23 +115,15 @@ export default class extends Endpoint { // eslint-
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
title: ps.title,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- name: ps.name === undefined ? page.name : ps.name,
+ name: ps.name,
summary: ps.summary === undefined ? page.summary : ps.summary,
content: ps.content,
variables: ps.variables,
script: ps.script,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- font: ps.font === undefined ? page.font : ps.font,
- eyeCatchingImageId: ps.eyeCatchingImageId === null
- ? null
- : ps.eyeCatchingImageId === undefined
- ? page.eyeCatchingImageId
- : eyeCatchingImage!.id,
+ alignCenter: ps.alignCenter,
+ hideTitleWhenPinned: ps.hideTitleWhenPinned,
+ font: ps.font,
+ eyeCatchingImageId: ps.eyeCatchingImageId,
});
});
}
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index bdcc1dfd77..72aca4dee2 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5881,15 +5881,15 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
id: string;
- memo: string;
- url: string;
- imageUrl: string;
- place: string;
- priority: string;
- ratio: number;
- expiresAt: number;
- startsAt: number;
- dayOfWeek: number;
+ memo?: string;
+ url?: string;
+ imageUrl?: string;
+ place?: string;
+ priority?: string;
+ ratio?: number;
+ expiresAt?: number;
+ startsAt?: number;
+ dayOfWeek?: number;
};
};
};
@@ -9744,21 +9744,21 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
roleId: string;
- name: string;
- description: string;
- color: string | null;
- iconUrl: string | null;
+ name?: string;
+ description?: string;
+ color?: string | null;
+ iconUrl?: string | null;
/** @enum {string} */
- target: 'manual' | 'conditional';
- condFormula: Record;
- isPublic: boolean;
- isModerator: boolean;
- isAdministrator: boolean;
+ target?: 'manual' | 'conditional';
+ condFormula?: Record;
+ isPublic?: boolean;
+ isModerator?: boolean;
+ isAdministrator?: boolean;
isExplorable?: boolean;
- asBadge: boolean;
- canEditMembersByModerator: boolean;
- displayOrder: number;
- policies: Record;
+ asBadge?: boolean;
+ canEditMembersByModerator?: boolean;
+ displayOrder?: number;
+ policies?: Record;
};
};
};
@@ -13400,7 +13400,7 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
clipId: string;
- name: string;
+ name?: string;
isPublic?: boolean;
description?: string | null;
};
@@ -16247,9 +16247,9 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
postId: string;
- title: string;
+ title?: string;
description?: string | null;
- fileIds: string[];
+ fileIds?: string[];
/** @default false */
isSensitive?: boolean;
};
@@ -20030,12 +20030,11 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
webhookId: string;
- name: string;
- url: string;
- /** @default */
- secret?: string;
- on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
- active: boolean;
+ name?: string;
+ url?: string;
+ secret?: string | null;
+ on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
+ active?: boolean;
};
};
};
@@ -23404,16 +23403,16 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
pageId: string;
- title: string;
- name: string;
+ title?: string;
+ name?: string;
summary?: string | null;
- content: {
+ content?: {
[key: string]: unknown;
}[];
- variables: {
+ variables?: {
[key: string]: unknown;
}[];
- script: string;
+ script?: string;
/** Format: misskey:id */
eyeCatchingImageId?: string | null;
/** @enum {string} */
From faeab96e01c7c7be5dfc85716b4a0b05b93f50ab Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Sat, 22 Jun 2024 14:55:24 +0900
Subject: [PATCH 48/49] ci: add quote (#13990)
---
.github/workflows/storybook.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml
index c52883ffdd..daa76509c8 100644
--- a/.github/workflows/storybook.yml
+++ b/.github/workflows/storybook.yml
@@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
BRANCH="$HEAD_REF"
fi
- pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
+ pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER")
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
From bf403aa656627fc4b29aed329aa044d42a791acf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 22 Jun 2024 15:35:54 +0900
Subject: [PATCH 49/49] =?UTF-8?q?fix(frontend):=20=E3=83=99=E3=83=BC?=
=?UTF-8?q?=E3=82=B9=E3=83=AD=E3=83=BC=E3=83=AB=E3=82=92=E7=B7=A8=E9=9B=86?=
=?UTF-8?q?=E3=81=97=E3=81=A6=E3=82=82UI=E4=B8=8A=E3=81=A7=E3=81=AF?=
=?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=81=8C=E5=8F=8D=E6=98=A0=E3=81=95=E3=82=8C?=
=?UTF-8?q?=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
=?UTF-8?q?=20(#13995)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): ベースロールを変更してもUI上では変更が反映されない問題を修正
* Update CHANGELOG.md
---
CHANGELOG.md | 1 +
packages/frontend/src/pages/admin/roles.vue | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1af63ad23..a913e42500 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
+- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 9753d9f6cb..50323e3de5 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -243,7 +243,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { instance } from '@/instance.js';
+import { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { ROLE_POLICIES } from '@/const.js';
import { useRouter } from '@/router/supplier.js';
@@ -267,6 +267,7 @@ async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-policies', {
policies,
});
+ fetchInstance(true);
}
function create() {