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

@@ -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(),
});
});
}
}