feat(webhook): 通報の登録、解決、自動解決に対応 (#615)

This commit is contained in:
CyberRex 2024-04-22 09:22:11 +09:00 committed by GitHub
parent 432833747d
commit 470ce2e0f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 114 additions and 4 deletions

12
locales/index.d.ts vendored
View File

@ -9484,6 +9484,18 @@ export interface Locale extends ILocale {
* *
*/ */
"mention": string; "mention": string;
/**
*
*/
"reportCreated": string;
/**
*
*/
"reportResolved": string;
/**
*
*/
"reportAutoResolved": string;
}; };
}; };
"_abuse": { "_abuse": {

View File

@ -2513,6 +2513,9 @@ _webhookSettings:
renote: "Renoteされたとき" renote: "Renoteされたとき"
reaction: "リアクションがあったとき" reaction: "リアクションがあったとき"
mention: "メンションされたとき" mention: "メンションされたとき"
reportCreated: "通報が登録されたとき"
reportResolved: "通報が解決されたとき"
reportAutoResolved: "通報が自動解決されたとき"
_abuse: _abuse:
_resolver: _resolver:

View File

@ -7,7 +7,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiUser } from './User.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') @Entity('webhook')
export class MiWebhook { export class MiWebhook {

View File

@ -15,6 +15,7 @@ import type { AbuseReportResolversRepository, AbuseUserReportsRepository, UsersR
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DbAbuseReportJobData } from '../types.js'; import type { DbAbuseReportJobData } from '../types.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -39,6 +40,7 @@ export class ReportAbuseProcessorService {
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private queueService: QueueService, private queueService: QueueService,
private webhookService: WebhookService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('report-abuse'); 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, 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; return;
} }
} }

View File

@ -11,6 +11,8 @@ import { QueueService } from '@/core/QueueService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -44,6 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private webhookService: WebhookService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
@ -59,11 +63,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false); 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, resolved: true,
assigneeId: me.id, assigneeId: me.id,
forwarded: ps.forward && report.targetUserHost != null, 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', { this.moderationLogService.log(me, 'resolveAbuseReport', {
reportId: report.id, reportId: report.id,

View File

@ -27,6 +27,11 @@ export const meta = {
code: 'TOO_MANY_WEBHOOKS', code: 'TOO_MANY_WEBHOOKS',
id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', 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: { res: {
@ -90,6 +95,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyWebhooks); 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({ const webhook = await this.webhooksRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
userId: me.id, userId: me.id,

View File

@ -9,6 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import { webhookEventTypes } from '@/models/Webhook.js'; import { webhookEventTypes } from '@/models/Webhook.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -25,6 +26,11 @@ export const meta = {
code: 'NO_SUCH_WEBHOOK', code: 'NO_SUCH_WEBHOOK',
id: 'fb0fea69-da18-45b1-828d-bd4fd1612518', 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; } as const;
@ -53,6 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private webhooksRepository: WebhooksRepository, private webhooksRepository: WebhooksRepository,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const webhook = await this.webhooksRepository.findOneBy({ const webhook = await this.webhooksRepository.findOneBy({
@ -64,6 +71,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchWebhook); 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, { await this.webhooksRepository.update(webhook.id, {
name: ps.name, name: ps.name,
url: ps.url, url: ps.url,

View File

@ -5,13 +5,14 @@
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import { Inject, Injectable } from '@nestjs/common'; 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 { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -69,10 +70,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.abuseUserReportsRepository) @Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository, private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService, private idService: IdService,
private getterService: GetterService, private getterService: GetterService,
private roleService: RoleService, private roleService: RoleService,
private queueService: QueueService, private queueService: QueueService,
private webhookService: WebhookService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// Lookup user // Lookup user
@ -95,6 +100,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
category: ps.category, category: ps.category,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); }).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); this.queueService.createReportAbuseJob(report);
}); });
} }

View File

@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
<MkSwitch v-if="$i?.isAdmin" v-model="event_reportCreated">{{ i18n.ts._webhookSettings._events.reportCreated }}</MkSwitch>
<MkSwitch v-if="$i?.isAdmin" v-model="event_reportResolved">{{ i18n.ts._webhookSettings._events.reportResolved }}</MkSwitch>
<MkSwitch v-if="$i?.isAdmin" v-model="event_reportAutoResolved">{{ i18n.ts._webhookSettings._events.reportAutoResolved }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>
@ -52,6 +55,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import { $i } from '@/account.js';
const router = useRouter(); 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_renote = ref(webhook.on.includes('renote'));
const event_reaction = ref(webhook.on.includes('reaction')); const event_reaction = ref(webhook.on.includes('reaction'));
const event_mention = ref(webhook.on.includes('mention')); 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<void> { async function save(): Promise<void> {
const events = []; const events = [];
@ -85,6 +92,9 @@ async function save(): Promise<void> {
if (event_renote.value) events.push('renote'); if (event_renote.value) events.push('renote');
if (event_reaction.value) events.push('reaction'); if (event_reaction.value) events.push('reaction');
if (event_mention.value) events.push('mention'); 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', { os.apiWithDialog('i/webhooks/update', {
name: name.value, name: name.value,

View File

@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
<MkSwitch v-if="$i?.isAdmin" v-model="event_reportCreated">{{ i18n.ts._webhookSettings._events.reportCreated }}</MkSwitch>
<MkSwitch v-if="$i?.isAdmin" v-model="event_reportResolved">{{ i18n.ts._webhookSettings._events.reportResolved }}</MkSwitch>
<MkSwitch v-if="$i?.isAdmin" v-model="event_reportAutoResolved">{{ i18n.ts._webhookSettings._events.reportAutoResolved }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>
@ -47,6 +50,7 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js';
const name = ref(''); const name = ref('');
const url = ref(''); const url = ref('');
@ -59,6 +63,9 @@ const event_reply = ref(true);
const event_renote = ref(true); const event_renote = ref(true);
const event_reaction = ref(true); const event_reaction = ref(true);
const event_mention = 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<void> { async function create(): Promise<void> {
const events = []; const events = [];
@ -69,6 +76,9 @@ async function create(): Promise<void> {
if (event_renote.value) events.push('renote'); if (event_renote.value) events.push('renote');
if (event_reaction.value) events.push('reaction'); if (event_reaction.value) events.push('reaction');
if (event_mention.value) events.push('mention'); 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', { os.apiWithDialog('i/webhooks/create', {
name: name.value, name: name.value,