diff --git a/locales/index.d.ts b/locales/index.d.ts index 370049963..e5b197a92 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9484,6 +9484,18 @@ export interface Locale extends ILocale { * メンションされたとき */ "mention": string; + /** + * 通報が作成されたとき + */ + "reportCreated": string; + /** + * 通報が解決されたとき + */ + "reportResolved": string; + /** + * 通報が自動解決されたとき + */ + "reportAutoResolved": string; }; }; "_abuse": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4348df93e..f4899dafd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2513,6 +2513,9 @@ _webhookSettings: renote: "Renoteされたとき" reaction: "リアクションがあったとき" mention: "メンションされたとき" + reportCreated: "通報が登録されたとき" + reportResolved: "通報が解決されたとき" + reportAutoResolved: "通報が自動解決されたとき" _abuse: _resolver: diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts index 2c7c7ed9a..32efa9697 100644 --- a/packages/backend/src/models/Webhook.ts +++ b/packages/backend/src/models/Webhook.ts @@ -7,7 +7,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { id } from './util/id.js'; import { MiUser } from './User.js'; -export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; +export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'reportCreated', 'reportResolved', 'reportAutoResolved'] as const; @Entity('webhook') export class MiWebhook { diff --git a/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts b/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts index 444cc0413..d80e31542 100644 --- a/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts +++ b/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts @@ -15,6 +15,7 @@ import type { AbuseReportResolversRepository, AbuseUserReportsRepository, UsersR import { DI } from '@/di-symbols.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { QueueService } from '@/core/QueueService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { DbAbuseReportJobData } from '../types.js'; import type * as Bull from 'bullmq'; @@ -39,6 +40,7 @@ export class ReportAbuseProcessorService { private apRendererService: ApRendererService, private roleService: RoleService, private queueService: QueueService, + private webhookService: WebhookService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('report-abuse'); } @@ -86,6 +88,20 @@ export class ReportAbuseProcessorService { forwarded: resolver.forward && job.data.targetUserHost !== null && job.data.reporterHost === null, }); + const activeWebhooks = await this.webhookService.getActiveWebhooks(); + for (const webhook of activeWebhooks) { + const webhookUser = await this.usersRepository.findOneByOrFail({ + id: webhook.userId, + }); + const isAdmin = await this.roleService.isAdministrator(webhookUser); + if (webhook.on.includes('reportAutoResolved') && isAdmin) { + this.queueService.webhookDeliver(webhook, 'reportAutoResolved', { + resolver: resolver, + report: job.data, + }); + } + } + return; } } 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 8b0456068..0fa3fe833 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 @@ -11,6 +11,8 @@ import { QueueService } from '@/core/QueueService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { WebhookService } from '@/core/WebhookService.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin'], @@ -44,6 +46,8 @@ export default class extends Endpoint { // eslint- private instanceActorService: InstanceActorService, private apRendererService: ApRendererService, private moderationLogService: ModerationLogService, + private webhookService: WebhookService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); @@ -59,11 +63,24 @@ export default class extends Endpoint { // eslint- this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false); } - await this.abuseUserReportsRepository.update(report.id, { + const updatedReport = await this.abuseUserReportsRepository.update(report.id, { resolved: true, assigneeId: me.id, forwarded: ps.forward && report.targetUserHost != null, - }); + }).then(() => this.abuseUserReportsRepository.findOneBy({ id: ps.reportId })); + + const activeWebhooks = await this.webhookService.getActiveWebhooks(); + for (const webhook of activeWebhooks) { + const webhookUser = await this.usersRepository.findOneByOrFail({ + id: webhook.userId, + }); + const isAdmin = await this.roleService.isAdministrator(webhookUser); + if (webhook.on.includes('reportResolved') && isAdmin) { + this.queueService.webhookDeliver(webhook, 'reportResolved', { + updatedReport, + }); + } + } this.moderationLogService.log(me, 'resolveAbuseReport', { reportId: report.id, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 3afcb73a4..71f9644f6 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -27,6 +27,11 @@ export const meta = { code: 'TOO_MANY_WEBHOOKS', id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', }, + youAreNotAdmin: { + message: 'You are not an administrator.', + code: 'YOU_ARE_NOT_ADMIN', + id: '26601bea-079b-4782-8dac-071febe2acf9', + }, }, res: { @@ -90,6 +95,12 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.tooManyWebhooks); } + if (ps.on.includes('reportCreated') || ps.on.includes('reportResolved') || ps.on.includes('reportAutoResolved')) { + if (!await this.roleService.isAdministrator(me)) { + throw new ApiError(meta.errors.youAreNotAdmin); + } + } + const webhook = await this.webhooksRepository.insert({ id: this.idService.gen(), userId: me.id, 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 6eb73da3b..40a2efd4c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -9,6 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js'; import { webhookEventTypes } from '@/models/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -25,6 +26,11 @@ export const meta = { code: 'NO_SUCH_WEBHOOK', id: 'fb0fea69-da18-45b1-828d-bd4fd1612518', }, + youAreNotAdmin: { + message: 'You are not an administrator.', + code: 'YOU_ARE_NOT_ADMIN', + id: 'a70c7643-1db5-4ebf-becd-ff4b4223cf23', + }, }, } as const; @@ -53,6 +59,7 @@ export default class extends Endpoint { // eslint- private webhooksRepository: WebhooksRepository, private globalEventService: GlobalEventService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const webhook = await this.webhooksRepository.findOneBy({ @@ -64,6 +71,12 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchWebhook); } + if (ps.on.includes('reportCreated') || ps.on.includes('reportResolved') || ps.on.includes('reportAutoResolved')) { + if (!await this.roleService.isAdministrator(me)) { + throw new ApiError(meta.errors.youAreNotAdmin); + } + } + await this.webhooksRepository.update(webhook.id, { name: ps.name, url: ps.url, 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 a1130541d..4d5886aff 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -5,13 +5,14 @@ import sanitizeHtml from 'sanitize-html'; import { Inject, Injectable } from '@nestjs/common'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { AbuseUserReportsRepository, UsersRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; import { QueueService } from '@/core/QueueService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -69,10 +70,14 @@ export default class extends Endpoint { // eslint- @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private idService: IdService, private getterService: GetterService, private roleService: RoleService, private queueService: QueueService, + private webhookService: WebhookService, ) { super(meta, paramDef, async (ps, me) => { // Lookup user @@ -95,6 +100,19 @@ export default class extends Endpoint { // eslint- category: ps.category, }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); + const activeWebhooks = await this.webhookService.getActiveWebhooks(); + for (const webhook of activeWebhooks) { + const webhookUser = await this.usersRepository.findOneByOrFail({ + id: webhook.userId, + }); + const isAdmin = await this.roleService.isAdministrator(webhookUser); + if (webhook.on.includes('reportCreated') && isAdmin) { + this.queueService.webhookDeliver(webhook, 'reportCreated', { + report, + }); + } + } + this.queueService.createReportAbuseJob(report); }); } diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index e9fb1e471..7a441bb55 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._webhookSettings._events.renote }} {{ i18n.ts._webhookSettings._events.reaction }} {{ i18n.ts._webhookSettings._events.mention }} + {{ i18n.ts._webhookSettings._events.reportCreated }} + {{ i18n.ts._webhookSettings._events.reportResolved }} + {{ i18n.ts._webhookSettings._events.reportAutoResolved }} @@ -52,6 +55,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { useRouter } from '@/router/supplier.js'; +import { $i } from '@/account.js'; const router = useRouter(); @@ -75,6 +79,9 @@ const event_reply = ref(webhook.on.includes('reply')); const event_renote = ref(webhook.on.includes('renote')); const event_reaction = ref(webhook.on.includes('reaction')); const event_mention = ref(webhook.on.includes('mention')); +const event_reportCreated = ref(webhook.on.includes('reportCreated')); +const event_reportResolved = ref(webhook.on.includes('reportResolved')); +const event_reportAutoResolved = ref(webhook.on.includes('reportAutoResolved')); async function save(): Promise { const events = []; @@ -85,6 +92,9 @@ async function save(): Promise { if (event_renote.value) events.push('renote'); if (event_reaction.value) events.push('reaction'); if (event_mention.value) events.push('mention'); + if (event_reportCreated.value) events.push('reportCreated'); + if (event_reportResolved.value) events.push('reportResolved'); + if (event_reportAutoResolved.value) events.push('reportAutoResolved'); os.apiWithDialog('i/webhooks/update', { name: name.value, diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index 5bf85e48f..723884f0a 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._webhookSettings._events.renote }} {{ i18n.ts._webhookSettings._events.reaction }} {{ i18n.ts._webhookSettings._events.mention }} + {{ i18n.ts._webhookSettings._events.reportCreated }} + {{ i18n.ts._webhookSettings._events.reportResolved }} + {{ i18n.ts._webhookSettings._events.reportAutoResolved }} @@ -47,6 +50,7 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { $i } from '@/account.js'; const name = ref(''); const url = ref(''); @@ -59,6 +63,9 @@ const event_reply = ref(true); const event_renote = ref(true); const event_reaction = ref(true); const event_mention = ref(true); +const event_reportCreated = ref(false); +const event_reportResolved = ref(false); +const event_reportAutoResolved = ref(false); async function create(): Promise { const events = []; @@ -69,6 +76,9 @@ async function create(): Promise { if (event_renote.value) events.push('renote'); if (event_reaction.value) events.push('reaction'); if (event_mention.value) events.push('mention'); + if (event_reportCreated.value) events.push('reportCreated'); + if (event_reportResolved.value) events.push('reportResolved'); + if (event_reportAutoResolved.value) events.push('reportAutoResolved'); os.apiWithDialog('i/webhooks/create', { name: name.value,