feat: 通報の強化 (#14704)
* wip
* Update CHANGELOG.md
* lint
* Update types.ts
* wip
* ✌️
* Update MkAbuseReport.vue
* tweak
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RefineAbuseUserReport1728085812127 {
|
||||
name = 'RefineAbuseUserReport1728085812127'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
|
||||
}
|
||||
}
|
@@ -20,8 +20,10 @@ 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,
|
||||
@@ -77,16 +79,16 @@ export class AbuseReportService {
|
||||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @param operator 通報を処理したユーザ
|
||||
* @param moderator 通報を処理したユーザ
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async resolve(
|
||||
params: {
|
||||
reportId: string;
|
||||
forward: boolean;
|
||||
resolvedAs: MiAbuseUserReport['resolvedAs'];
|
||||
}[],
|
||||
operator: MiUser,
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||
const reports = await this.abuseUserReportsRepository.findBy({
|
||||
@@ -99,25 +101,15 @@ export class AbuseReportService {
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
resolved: true,
|
||||
assigneeId: operator.id,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
assigneeId: moderator.id,
|
||||
resolvedAs: ps.resolvedAs,
|
||||
});
|
||||
|
||||
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', {
|
||||
.log(moderator, 'resolveAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
resolvedAs: ps.resolvedAs,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
@@ -125,4 +117,58 @@ export class AbuseReportService {
|
||||
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async forward(
|
||||
reportId: MiAbuseUserReport['id'],
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
if (report.targetUserHost == null) {
|
||||
throw new Error('The target user host is null.');
|
||||
}
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
forwarded: true,
|
||||
});
|
||||
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
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(moderator, 'forwardAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
reportId: MiAbuseUserReport['id'],
|
||||
params: {
|
||||
moderationNote?: MiAbuseUserReport['moderationNote'];
|
||||
},
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
moderationNote: params.moderationNote,
|
||||
});
|
||||
|
||||
if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
|
||||
this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
before: report.moderationNote,
|
||||
after: params.moderationNote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -35,6 +35,8 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserRe
|
||||
comment: 'This is a dummy report for testing purposes.',
|
||||
targetUserHost: null,
|
||||
reporterHost: null,
|
||||
resolvedAs: null,
|
||||
moderationNote: 'foo',
|
||||
...override,
|
||||
};
|
||||
|
||||
|
@@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
|
||||
schema: 'UserDetailedNotMe',
|
||||
}) : null,
|
||||
forwarded: report.forwarded,
|
||||
resolvedAs: report.resolvedAs,
|
||||
moderationNote: report.moderationNote,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
|
||||
})
|
||||
public resolved: boolean;
|
||||
|
||||
/**
|
||||
* リモートサーバーに転送したかどうか
|
||||
*/
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
|
||||
})
|
||||
public comment: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 8192, default: '',
|
||||
})
|
||||
public moderationNote: string;
|
||||
|
||||
/**
|
||||
* accept 是認 ... 通報内容が正当であり、肯定的に対応された
|
||||
* reject 否認 ... 通報内容が正当でなく、否定的に対応された
|
||||
* null ... その他
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public resolvedAs: 'accept' | 'reject' | null;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
|
||||
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
|
||||
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
|
||||
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
|
||||
import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
|
||||
import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
|
||||
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
|
||||
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
|
||||
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
|
||||
@@ -453,6 +455,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
|
||||
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
|
||||
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
|
||||
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
|
||||
const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
|
||||
const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
|
||||
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
|
||||
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
|
||||
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
|
||||
@@ -842,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_relays_remove,
|
||||
$admin_resetPassword,
|
||||
$admin_resolveAbuseUserReport,
|
||||
$admin_forwardAbuseUserReport,
|
||||
$admin_updateAbuseUserReport,
|
||||
$admin_sendEmail,
|
||||
$admin_serverInfo,
|
||||
$admin_showModerationLogs,
|
||||
@@ -1225,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_relays_remove,
|
||||
$admin_resetPassword,
|
||||
$admin_resolveAbuseUserReport,
|
||||
$admin_forwardAbuseUserReport,
|
||||
$admin_updateAbuseUserReport,
|
||||
$admin_sendEmail,
|
||||
$admin_serverInfo,
|
||||
$admin_showModerationLogs,
|
||||
|
@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
|
||||
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
|
||||
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
|
||||
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
|
||||
import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
|
||||
import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
|
||||
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
|
||||
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
|
||||
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
|
||||
@@ -457,6 +459,8 @@ const eps = [
|
||||
['admin/relays/remove', ep___admin_relays_remove],
|
||||
['admin/reset-password', ep___admin_resetPassword],
|
||||
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
|
||||
['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
|
||||
['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
|
||||
['admin/send-email', ep___admin_sendEmail],
|
||||
['admin/server-info', ep___admin_serverInfo],
|
||||
['admin/show-moderation-logs', ep___admin_showModerationLogs],
|
||||
|
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 type { AbuseUserReportsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:resolve-abuse-user-report',
|
||||
|
||||
errors: {
|
||||
noSuchAbuseReport: {
|
||||
message: 'No such abuse report.',
|
||||
code: 'NO_SUCH_ABUSE_REPORT',
|
||||
id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
|
||||
kind: 'server',
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reportId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['reportId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
private abuseReportService: AbuseReportService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
|
||||
if (!report) {
|
||||
throw new ApiError(meta.errors.noSuchAbuseReport);
|
||||
}
|
||||
|
||||
await this.abuseReportService.forward(report.id, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reportId: { type: 'string', format: 'misskey:id' },
|
||||
forward: { type: 'boolean', default: false },
|
||||
resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
|
||||
},
|
||||
required: ['reportId'],
|
||||
} as const;
|
||||
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.noSuchAbuseReport);
|
||||
}
|
||||
|
||||
await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
|
||||
await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 type { AbuseUserReportsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:resolve-abuse-user-report',
|
||||
|
||||
errors: {
|
||||
noSuchAbuseReport: {
|
||||
message: 'No such abuse report.',
|
||||
code: 'NO_SUCH_ABUSE_REPORT',
|
||||
id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
|
||||
kind: 'server',
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reportId: { type: 'string', format: 'misskey:id' },
|
||||
moderationNote: { type: 'string' },
|
||||
},
|
||||
required: ['reportId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
private abuseReportService: AbuseReportService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
|
||||
if (!report) {
|
||||
throw new ApiError(meta.errors.noSuchAbuseReport);
|
||||
}
|
||||
|
||||
await this.abuseReportService.update(report.id, {
|
||||
moderationNote: ps.moderationNote,
|
||||
}, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -99,6 +99,8 @@ export const moderationLogTypes = [
|
||||
'markSensitiveDriveFile',
|
||||
'unmarkSensitiveDriveFile',
|
||||
'resolveAbuseReport',
|
||||
'forwardAbuseReport',
|
||||
'updateAbuseReportNote',
|
||||
'createInvitation',
|
||||
'createAd',
|
||||
'updateAd',
|
||||
@@ -267,7 +269,18 @@ export type ModerationLogPayloads = {
|
||||
resolveAbuseReport: {
|
||||
reportId: string;
|
||||
report: any;
|
||||
forwarded: boolean;
|
||||
forwarded?: boolean;
|
||||
resolvedAs?: string | null;
|
||||
};
|
||||
forwardAbuseReport: {
|
||||
reportId: string;
|
||||
report: any;
|
||||
};
|
||||
updateAbuseReportNote: {
|
||||
reportId: string;
|
||||
report: any;
|
||||
before: string;
|
||||
after: string;
|
||||
};
|
||||
createInvitation: {
|
||||
invitations: any[];
|
||||
|
@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
|
||||
const webhookBody2 = await captureWebhook(async () => {
|
||||
await resolveAbuseReport({
|
||||
reportId: webhookBody1.body.id,
|
||||
forward: false,
|
||||
}, admin);
|
||||
});
|
||||
|
||||
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
|
||||
const webhookBody2 = await captureWebhook(async () => {
|
||||
await resolveAbuseReport({
|
||||
reportId: abuseReportId,
|
||||
forward: false,
|
||||
}, admin);
|
||||
});
|
||||
|
||||
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
|
||||
const webhookBody2 = await captureWebhook(async () => {
|
||||
await resolveAbuseReport({
|
||||
reportId: webhookBody1.body.id,
|
||||
forward: false,
|
||||
}, admin);
|
||||
}).catch(e => e.message);
|
||||
|
||||
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
|
||||
const webhookBody2 = await captureWebhook(async () => {
|
||||
await resolveAbuseReport({
|
||||
reportId: abuseReportId,
|
||||
forward: false,
|
||||
}, admin);
|
||||
}).catch(e => e.message);
|
||||
|
||||
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
|
||||
const webhookBody2 = await captureWebhook(async () => {
|
||||
await resolveAbuseReport({
|
||||
reportId: abuseReportId,
|
||||
forward: false,
|
||||
}, admin);
|
||||
}).catch(e => e.message);
|
||||
|
||||
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
|
||||
const webhookBody2 = await captureWebhook(async () => {
|
||||
await resolveAbuseReport({
|
||||
reportId: abuseReportId,
|
||||
forward: false,
|
||||
}, admin);
|
||||
}).catch(e => e.message);
|
||||
|
||||
|
Reference in New Issue
Block a user