feat: 通報の即時解決機能を追加 (#113)

* feat: 通報の即時解決機能を追加

* fix: 条件変更時に有効期限を変更していないのに勝手に更新される問題を修正

* fix: 条件のパターンの削除ができない問題を修正

* fix: リソルバーの通報を解決する判定基準が間違っていたのを修正

* fix: 変更する変数が間違っていたのを修正

* fix: getUTCMonthはゼロ始まりかも

* enhance: Storybookのストーリーを作成

* fix: 色々修正

* fix: 型エラーを修正

* [ci skip] Update CHANGELOG.md

* [ci skip] Update CHANGELOG.md

* Update CHANGELOG.md

* リファクタリング

* refactor: 型定義をよりよくした

* refactor: beforeExpiresAtの初期値はundefinedの方がいい

* refactor: 変数の名前を変更

* Fix: リモートサーバーから転送された通報も対象に追加

* Update CHANGELOG.md

* take review

---------

Co-authored-by: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com>
This commit is contained in:
まっちゃとーにゅ
2023-07-28 01:17:17 +09:00
committed by GitHub
parent 27f57b031b
commit 0bed053b7d
29 changed files with 1059 additions and 44 deletions

View File

@@ -13,6 +13,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js';
import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js';
import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@@ -358,6 +362,10 @@ const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements
const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default };
const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default };
const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default };
const $admin_abuseReportResolver_create: Provider = { provide: 'ep:admin/abuse-report-resolver/create', useClass: ep___admin_abuseReportResolver_create.default };
const $admin_abuseReportResolver_update: Provider = { provide: 'ep:admin/abuse-report-resolver/update', useClass: ep___admin_abuseReportResolver_update.default };
const $admin_abuseReportResolver_list: Provider = { provide: 'ep:admin/abuse-report-resolver/list', useClass: ep___admin_abuseReportResolver_list.default };
const $admin_abuseReportResolver_delete: Provider = { provide: 'ep:admin/abuse-report-resolver/delete', useClass: ep___admin_abuseReportResolver_delete.default };
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
@@ -707,6 +715,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_announcements_delete,
$admin_announcements_list,
$admin_announcements_update,
$admin_abuseReportResolver_create,
$admin_abuseReportResolver_delete,
$admin_abuseReportResolver_list,
$admin_abuseReportResolver_update,
$admin_deleteAllFilesOfAUser,
$admin_drive_cleanRemoteFiles,
$admin_drive_cleanup,
@@ -1050,6 +1062,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_announcements_delete,
$admin_announcements_list,
$admin_announcements_update,
$admin_abuseReportResolver_create,
$admin_abuseReportResolver_delete,
$admin_abuseReportResolver_list,
$admin_abuseReportResolver_update,
$admin_deleteAllFilesOfAUser,
$admin_drive_cleanRemoteFiles,
$admin_drive_cleanup,

View File

@@ -13,6 +13,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js';
import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js';
import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@@ -356,6 +360,10 @@ const eps = [
['admin/announcements/delete', ep___admin_announcements_delete],
['admin/announcements/list', ep___admin_announcements_list],
['admin/announcements/update', ep___admin_announcements_update],
['admin/abuse-report-resolver/create', ep___admin_abuseReportResolver_create],
['admin/abuse-report-resolver/list', ep___admin_abuseReportResolver_list],
['admin/abuse-report-resolver/delete', ep___admin_abuseReportResolver_delete],
['admin/abuse-report-resolver/update', ep___admin_abuseReportResolver_update],
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
['admin/drive/cleanup', ep___admin_drive_cleanup],

View File

@@ -0,0 +1,136 @@
import { Injectable, Inject } from '@nestjs/common';
import ms from 'ms';
import RE2 from 're2';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import type { AbuseReportResolversRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireAdmin: true,
res: {
type: 'object',
properties: {
name: {
type: 'string',
nullable: false, optional: false,
},
targetUserPattern: {
type: 'string',
nullable: true, optional: false,
},
reporterPattern: {
type: 'string',
nullable: true, optional: false,
},
reportContentPattern: {
type: 'string',
nullable: true, optional: false,
},
expiresAt: {
type: 'string',
nullable: false, optional: false,
},
forward: {
type: 'boolean',
nullable: false, optional: false,
},
},
},
errors: {
invalidRegularExpressionForTargetUser: {
message: 'Invalid regular expression for target user.',
code: 'INVALID_REGULAR_EXPRESSION_FOR_TARGET_USER',
id: 'c008484a-0a14-4e74-86f4-b176dc49fcaa',
},
invalidRegularExpressionForReporter: {
message: 'Invalid regular expression for reporter.',
code: 'INVALID_REGULAR_EXPRESSION_FOR_REPORTER',
id: '399b4062-257f-44c8-87cc-4ffae2527fbc',
},
invalidRegularExpressionForReportContent: {
message: 'Invalid regular expression for report content.',
code: 'INVALID_REGULAR_EXPRESSION_FOR_REPORT_CONTENT',
id: '88c124d8-f517-4c63-a464-0abc274168b',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
targetUserPattern: { type: 'string', nullable: true },
reporterPattern: { type: 'string', nullable: true },
reportContentPattern: { type: 'string', nullable: true },
expiresAt: { type: 'string', enum: ['1hour', '12hours', '1day', '1week', '1month', '3months', '6months', '1year', 'indefinitely'] },
forward: { type: 'boolean' },
},
required: ['name', 'targetUserPattern', 'reporterPattern', 'reportContentPattern', 'expiresAt', 'forward'],
} as const;
@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.abuseReportResolversRepository)
private abuseReportResolverRepository: AbuseReportResolversRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.targetUserPattern) {
try {
new RE2(ps.targetUserPattern);
} catch (e) {
throw new ApiError(meta.errors.invalidRegularExpressionForTargetUser);
}
}
if (ps.reporterPattern) {
try {
new RE2(ps.reporterPattern);
} catch (e) {
throw new ApiError(meta.errors.invalidRegularExpressionForReporter);
}
}
if (ps.reportContentPattern) {
try {
new RE2(ps.reportContentPattern);
} catch (e) {
throw new ApiError(meta.errors.invalidRegularExpressionForReportContent);
}
}
const now = new Date();
let expirationDate: Date | null = new Date();
(ps.expiresAt === '1hour' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 hour')); } :
ps.expiresAt === '12hours' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('12 hours')); } :
ps.expiresAt === '1day' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 day')); } :
ps.expiresAt === '1week' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 week')); } :
ps.expiresAt === '1month' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 1 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((expirationDate!.getUTCMonth() + 1 + 1) / 12))); } :
ps.expiresAt === '3months' ? function () {expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 3 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((expirationDate!.getUTCMonth() + 3 + 1) / 12 ))); } :
ps.expiresAt === '6months' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 6 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((expirationDate!.getUTCMonth() + 6 + 1) / 12))); } :
ps.expiresAt === '1year' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 year')); } : function () { expirationDate = null; })();
return await this.abuseReportResolverRepository.insert({
id: this.idService.genId(),
createdAt: now,
updatedAt: now,
name: ps.name,
targetUserPattern: ps.targetUserPattern,
reporterPattern: ps.reporterPattern,
reportContentPattern: ps.reportContentPattern,
expirationDate,
expiresAt: ps.expiresAt,
forward: ps.forward,
}).then(x => this.abuseReportResolverRepository.findOneByOrFail(x.identifiers[0]));
});
}
}

View File

@@ -0,0 +1,47 @@
import { Injectable, Inject } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { AbuseReportResolversRepository } from '@/models/index.js';
import { ApiError } from '../../../error.js';
export const meta = {
requireCrendential: true,
requireAdmin: true,
errors: {
resolverNotFound: {
message: 'Resolver not found.',
code: 'RESOLVER_NOT_FOUND',
id: '121fbea9-3e49-4456-998a-d4095c7ac5c5',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
resolverId: { type: 'string', format: 'misskey:id' },
},
required: ['resolverId'],
} as const;
@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.abuseReportResolversRepository)
private abuseReportResolversRepository: AbuseReportResolversRepository,
) {
super(meta, paramDef, async (ps, me) => {
const resolver = await this.abuseReportResolversRepository.findOneBy({
id: ps.resolverId,
});
if (resolver === null) {
throw new ApiError(meta.errors.resolverNotFound);
}
await this.abuseReportResolversRepository.delete(ps.resolverId);
});
}
}

View File

@@ -0,0 +1,77 @@
import { Injectable, Inject } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { QueryService } from '@/core/QueryService.js';
import type { AbuseReportResolversRepository } from '@/models/index.js';
export const meta = {
requireCredential: true,
requireAdmin: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
nullable: false, optional: false,
},
targetUserPattern: {
type: 'string',
nullable: true, optional: false,
},
reporterPattern: {
type: 'string',
nullable: true, optional: false,
},
reportContentPattern: {
type: 'string',
nullable: true, optional: false,
},
expiresAt: {
type: 'string',
nullable: false, optional: false,
},
forward: {
type: 'boolean',
nullable: false, optional: false,
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'number', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.abuseReportResolversRepository)
private abuseReportResolversRepository: AbuseReportResolversRepository,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.abuseReportResolversRepository.createQueryBuilder('abuseReportResolvers'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => {
qb.where('abuseReportResolvers.expirationDate > :date', { date: new Date() });
qb.orWhere('abuseReportResolvers.expirationDate IS NULL');
}))
.take(ps.limit);
return await query.getMany();
});
}
}

View File

@@ -0,0 +1,117 @@
import { Injectable, Inject } from '@nestjs/common';
import ms from 'ms';
import RE2 from 're2';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { AbuseReportResolversRepository, AbuseReportResolver } from '@/models/index.js';
import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
requireAdmin: true,
errors: {
resolverNotFound: {
message: 'Resolver not found.',
id: 'fd32710e-75e1-4d20-bbd2-f89029acb16e',
code: 'RESOLVER_NOT_FOUND',
},
invalidRegularExpressionForTargetUser: {
message: 'Invalid regular expression for target user.',
code: 'INVALID_REGULAR_EXPRESSION_FOR_TARGET_USER',
id: 'c008484a-0a14-4e74-86f4-b176dc49fcaa',
},
invalidRegularExpressionForReporter: {
message: 'Invalid regular expression for reporter.',
code: 'INVALID_REGULAR_EXPRESSION_FOR_REPORTER',
id: '399b4062-257f-44c8-87cc-4ffae2527fbc',
},
invalidRegularExpressionForReportContent: {
message: 'Invalid regular expression for report content.',
code: 'INVALID_REGULAR_EXPRESSION_FOR_REPORT_CONTENT',
id: '88c124d8-f517-4c63-a464-0abc274168b',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
resolverId: { type: 'string', format: 'misskey:id' },
name: { type: 'string' },
targetUserPattern: { type: 'string', nullable: true },
reporterPattern: { type: 'string', nullable: true },
reportContentPattern: { type: 'string', nullable: true },
expiresAt: { type: 'string', enum: ['1hour', '12hours', '1day', '1week', '1month', '3months', '6months', '1year', 'indefinitely'] },
forward: { type: 'boolean' },
},
required: ['resolverId'],
} as const;
@Injectable() // eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.abuseReportResolversRepository)
private abuseReportResolversRepository: AbuseReportResolversRepository,
) {
super(meta, paramDef, async (ps, me) => {
const properties: Partial<Omit<AbuseReportResolver, 'id'>> = {};
const resolver = await this.abuseReportResolversRepository.findOneBy({
id: ps.resolverId,
});
if (resolver === null) throw new ApiError(meta.errors.resolverNotFound);
if (ps.name) properties.name = ps.name;
if (ps.targetUserPattern) {
try {
new RE2(ps.targetUserPattern);
} catch (e) {
throw new ApiError(meta.errors.invalidRegularExpressionForTargetUser);
}
properties.targetUserPattern = ps.targetUserPattern;
} else if (ps.targetUserPattern === null) {
properties.targetUserPattern = null;
}
if (ps.reporterPattern) {
try {
new RE2(ps.reporterPattern);
} catch (e) {
throw new ApiError(meta.errors.invalidRegularExpressionForReporter);
}
properties.reporterPattern = ps.reporterPattern;
} else if (ps.reporterPattern === null) {
properties.reporterPattern = null;
}
if (ps.reportContentPattern) {
try {
new RE2(ps.reportContentPattern);
} catch (e) {
throw new ApiError(meta.errors.invalidRegularExpressionForReportContent);
}
properties.reportContentPattern = ps.reportContentPattern;
} else if (ps.reportContentPattern === null) {
properties.reportContentPattern = null;
}
if (ps.forward) properties.forward = ps.forward;
if (ps.expiresAt) {
let expirationDate: Date | null = new Date();
(ps.expiresAt === '1hour' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 hour')); } :
ps.expiresAt === '12hours' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('12 hours')); } :
ps.expiresAt === '1day' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 day')); } :
ps.expiresAt === '1week' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 week')); } :
ps.expiresAt === '1month' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 1 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((expirationDate!.getUTCMonth() + 1 + 1) / 12))); } :
ps.expiresAt === '3months' ? function () {expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 3 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((expirationDate!.getUTCMonth() + 3 + 1) / 12))); } :
ps.expiresAt === '6months' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 6 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((expirationDate!.getUTCMonth() + 6 + 1) / 12))); } :
ps.expiresAt === '1year' ? function () { expirationDate!.setTime(expirationDate!.getTime() + ms('1 year')); } : function () { expirationDate = null; })();
properties.expiresAt = ps.expiresAt;
properties.expirationDate = expirationDate;
}
await this.abuseReportResolversRepository.update(ps.resolverId, {
...properties,
updatedAt: new Date(),
});
});
}
}

View File

@@ -1,14 +1,11 @@
import sanitizeHtml from 'sanitize-html';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { EmailService } from '@/core/EmailService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -59,11 +56,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private abuseUserReportsRepository: AbuseUserReportsRepository,
private idService: IdService,
private metaService: MetaService,
private emailService: EmailService,
private getterService: GetterService,
private roleService: RoleService,
private globalEventService: GlobalEventService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
// Lookup user
@@ -90,26 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
comment: ps.comment,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
// 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));
}
});
this.queueService.createReportAbuseJob(report);
});
}
}