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:
@@ -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));
|
||||
|
@@ -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';
|
||||
}
|
||||
|
@@ -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'),
|
||||
|
@@ -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 {}
|
||||
|
58
packages/backend/src/models/entities/AbuseReportResolver.ts
Normal file
58
packages/backend/src/models/entities/AbuseReportResolver.ts
Normal 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;
|
||||
}
|
@@ -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>;
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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`);
|
||||
}
|
||||
}, {
|
||||
|
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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 = {
|
||||
|
@@ -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,
|
||||
|
@@ -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],
|
||||
|
@@ -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]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user