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

@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
@@ -336,6 +337,11 @@ export class QueueService {
});
}
@bindThis
public createReportAbuseJob(report: AbuseUserReport) {
return this.dbQueue.add('reportAbuse', report);
}
@bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));

View File

@@ -512,7 +512,7 @@ export class ApInboxService {
});
if (users.length < 1) return 'skip';
await this.abuseUserReportsRepository.insert({
const report = await this.abuseUserReportsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
targetUserId: users[0].id,
@@ -520,7 +520,9 @@ export class ApInboxService {
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
});
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
this.queueService.createReportAbuseJob(report);
return 'ok';
}

View File

@@ -9,6 +9,7 @@ export const DI = {
//#region Repositories
usersRepository: Symbol('usersRepository'),
notesRepository: Symbol('notesRepository'),
abuseReportResolversRepository: Symbol('abuseReportResolversRepository'),
announcementsRepository: Symbol('announcementsRepository'),
announcementReadsRepository: Symbol('announcementReadsRepository'),
appsRepository: Symbol('appsRepository'),

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js';
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite, AbuseReportResolver } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -400,6 +400,12 @@ const $userMemosRepository: Provider = {
inject: [DI.db],
};
const $abuseReportResolversRepository: Provider = {
provide: DI.abuseReportResolversRepository,
useFactory: (db: DataSource) => db.getRepository(AbuseReportResolver),
inject: [DI.db],
};
@Module({
imports: [
],
@@ -470,6 +476,7 @@ const $userMemosRepository: Provider = {
$flashsRepository,
$flashLikesRepository,
$userMemosRepository,
$abuseReportResolversRepository,
],
exports: [
$usersRepository,
@@ -538,6 +545,7 @@ const $userMemosRepository: Provider = {
$flashsRepository,
$flashLikesRepository,
$userMemosRepository,
$abuseReportResolversRepository,
],
})
export class RepositoryModule {}

View File

@@ -0,0 +1,58 @@
import { Column, Entity, PrimaryColumn, Index } from 'typeorm';
import { id } from '../id.js';
@Entity()
export class AbuseReportResolver {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of AbuseReportResolver',
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The updated date of AbuseReportResolver',
})
public updatedAt: Date;
@Column('varchar', {
length: 256,
})
public name: string;
@Column('varchar', {
length: 1024,
nullable: true,
})
public targetUserPattern: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public reporterPattern: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public reportContentPattern: string | null;
@Index()
@Column('timestamp with time zone', {
comment: 'The expiration date of AbuseReportResolver',
nullable: true,
})
public expirationDate: Date | null;
@Column('enum', {
enum: ['1hour', '12hours', '1day', '1week', '1month', '3months', '6months', '1year', 'indefinitely']
})
public expiresAt: string;
@Column('boolean')
public forward: boolean;
}

View File

@@ -1,3 +1,4 @@
import { AbuseReportResolver } from '@/models/entities/AbuseReportResolver.js';
import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import { AccessToken } from '@/models/entities/AccessToken.js';
import { Ad } from '@/models/entities/Ad.js';
@@ -67,6 +68,7 @@ import { UserListFavorite } from './entities/UserListFavorite.js';
import type { Repository } from 'typeorm';
export {
AbuseReportResolver,
AbuseUserReport,
AccessToken,
Ad,
@@ -135,6 +137,7 @@ export {
UserMemo,
};
export type AbuseReportResolversRepository = Repository<AbuseReportResolver>;
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
export type AccessTokensRepository = Repository<AccessToken>;
export type AdsRepository = Repository<Ad>;

View File

@@ -6,6 +6,7 @@ import { DataSource, Logger } from 'typeorm';
import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
import { AbuseReportResolver } from '@/models/entities/AbuseReportResolver.js';
import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import { AccessToken } from '@/models/entities/AccessToken.js';
import { Ad } from '@/models/entities/Ad.js';
@@ -121,6 +122,7 @@ class MyCustomLogger implements Logger {
}
export const entities = [
AbuseReportResolver,
Announcement,
AnnouncementRead,
Meta,

View File

@@ -28,6 +28,7 @@ import { ImportMutingProcessorService } from './processors/ImportMutingProcessor
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
@@ -64,6 +65,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeleteFileProcessorService,
CleanRemoteFilesProcessorService,
RelationshipProcessorService,
ReportAbuseProcessorService,
WebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
DeliverProcessorService,

View File

@@ -27,6 +27,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
@@ -102,6 +103,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private deleteFileProcessorService: DeleteFileProcessorService,
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
private relationshipProcessorService: RelationshipProcessorService,
private reportAbuseProcessorService: ReportAbuseProcessorService,
private tickChartsProcessorService: TickChartsProcessorService,
private resyncChartsProcessorService: ResyncChartsProcessorService,
private cleanChartsProcessorService: CleanChartsProcessorService,
@@ -174,6 +176,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
case 'importAntennas': return this.importAntennasProcessorService.process(job);
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
case 'reportAbuse': return this.reportAbuseProcessorService.process(job);
default: throw new Error(`unrecognized job type ${job.name} for db`);
}
}, {

View File

@@ -0,0 +1,114 @@
import { Injectable, Inject } from '@nestjs/common';
import { MoreThan, IsNull } from 'typeorm';
import RE2 from 're2';
import sanitizeHtml from 'sanitize-html';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { EmailService } from '@/core/EmailService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { AbuseReportResolversRepository, AbuseUserReportsRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { QueueService } from '@/core/QueueService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DbAbuseReportJobData } from '../types.js';
import type * as Bull from 'bullmq';
@Injectable()
export class ReportAbuseProcessorService {
private logger: Logger;
constructor(
@Inject(DI.abuseReportResolversRepository)
private abuseReportResolversRepository: AbuseReportResolversRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private queueLoggerService: QueueLoggerService,
private globalEventService: GlobalEventService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private roleService: RoleService,
private metaService: MetaService,
private emailService: EmailService,
private queueService: QueueService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('report-abuse');
}
@bindThis
public async process(job: Bull.Job<DbAbuseReportJobData>): Promise<void> {
this.logger.info('Running...');
const resolvers = await this.abuseReportResolversRepository.find({
where: [
{ expirationDate: MoreThan(new Date()) },
{ expirationDate: IsNull() },
],
});
const targetUser = await this.usersRepository.findOneByOrFail({
id: job.data.targetUserId,
});
const reporter = await this.usersRepository.findOneByOrFail({
id: job.data.reporterId,
});
const actor = await this.instanceActorService.getInstanceActor();
const targetUserAcct = targetUser.host ? `${targetUser.username.toLowerCase()}@${targetUser.host}` : targetUser.username.toLowerCase();
const reporterAcct = reporter.host ? `${reporter.username.toLowerCase()}@${reporter.host}` : reporter.username.toLowerCase();
for (const resolver of resolvers) {
if (!(resolver.targetUserPattern || resolver.reporterPattern || resolver.reportContentPattern)) {
continue;
}
const isTargetUserPatternMatched = resolver.targetUserPattern ? new RE2(resolver.targetUserPattern).test(targetUserAcct) : true;
const isReporterPatternMatched = resolver.reporterPattern ? new RE2(resolver.reporterPattern).test(reporterAcct) : true;
const isReportContentPatternMatched = resolver.reportContentPattern ? new RE2(resolver.reportContentPattern).test(job.data.comment) : true;
if (isTargetUserPatternMatched && isReporterPatternMatched && isReportContentPatternMatched) {
if (resolver.forward && job.data.targetUserHost !== null) {
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, job.data.comment)), targetUser.inbox, false);
}
await this.abuseUserReportsRepository.update(job.data.id, {
resolved: true,
assigneeId: actor.id,
forwarded: resolver.forward && job.data.targetUserHost !== null,
});
return;
}
}
// Publish event to moderators
setImmediate(async () => {
const moderators = await this.roleService.getModerators();
for (const moderator of moderators) {
this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
id: job.data.id,
targetUserId: job.data.targetUserId,
reporterId: job.data.reporterId,
comment: job.data.comment,
});
}
const meta = await this.metaService.fetch();
if (meta.email) {
this.emailService.sendEmail(meta.email, 'New abuse report',
sanitizeHtml(job.data.comment),
sanitizeHtml(job.data.comment));
}
});
}
}

View File

@@ -2,6 +2,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Note } from '@/models/entities/Note.js';
import type { User } from '@/models/entities/User.js';
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import type { Webhook } from '@/models/entities/Webhook.js';
import type { IActivity } from '@/core/activitypub/type.js';
import type httpSignature from '@peertube/http-signature';
@@ -86,6 +87,8 @@ export type DbUserImportToDbJobData = {
target: string;
};
export type DbAbuseReportJobData = AbuseUserReport;
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
export type ObjectStorageFileJobData = {

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);
});
}
}