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:
parent
27f57b031b
commit
0bed053b7d
|
@ -2185,6 +2185,25 @@ export interface Locale {
|
||||||
"mention": string;
|
"mention": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"_abuse": {
|
||||||
|
"_resolver": {
|
||||||
|
"1hour": string;
|
||||||
|
"12hours": string;
|
||||||
|
"1day": string;
|
||||||
|
"1week": string;
|
||||||
|
"1month": string;
|
||||||
|
"3months": string;
|
||||||
|
"6months": string;
|
||||||
|
"1year": string;
|
||||||
|
"indefinitely": string;
|
||||||
|
"expiresAt": string;
|
||||||
|
"targetUserPattern": string;
|
||||||
|
"reporterPattern": string;
|
||||||
|
"reportContentPattern": string;
|
||||||
|
};
|
||||||
|
"list": string;
|
||||||
|
"resolver": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -2098,3 +2098,21 @@ _webhookSettings:
|
||||||
renote: "Renoteされたとき"
|
renote: "Renoteされたとき"
|
||||||
reaction: "リアクションがあったとき"
|
reaction: "リアクションがあったとき"
|
||||||
mention: "メンションされたとき"
|
mention: "メンションされたとき"
|
||||||
|
|
||||||
|
_abuse:
|
||||||
|
_resolver:
|
||||||
|
1hour: "一時間"
|
||||||
|
12hours: "半日"
|
||||||
|
1day: "一日"
|
||||||
|
1week: "一週間"
|
||||||
|
1month: "一ヶ月"
|
||||||
|
3months: "三ヶ月"
|
||||||
|
6months: "六ヶ月"
|
||||||
|
1year: "一年"
|
||||||
|
indefinitely: "無期限"
|
||||||
|
expiresAt: "この条件の有効期限"
|
||||||
|
targetUserPattern: "通報先のパターン"
|
||||||
|
reporterPattern: "通報元のパターン"
|
||||||
|
reportContentPattern: "通報内容のパターン"
|
||||||
|
list: "一覧"
|
||||||
|
resolver: "リソルバー"
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
export class AbuseReportResolver1686908762393 {
|
||||||
|
name = 'AbuseReportResolver1686908762393'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."abuse_report_resolver_expiresat_enum" AS ENUM('1hour', '12hours', '1day', '1week', '1month', '3months', '6months', '1year', 'indefinitely')`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "abuse_report_resolver" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "targetUserPattern" character varying(1024), "reporterPattern" character varying(1024), "reportContentPattern" character varying(1024), "expirationDate" TIMESTAMP WITH TIME ZONE, "expiresAt" "public"."abuse_report_resolver_expiresat_enum" NOT NULL, "forward" boolean NOT NULL, CONSTRAINT "PK_093500bf1bb38880d38b1bb41dc" PRIMARY KEY ("id")); COMMENT ON COLUMN "abuse_report_resolver"."createdAt" IS 'The created date of AbuseReportResolver'; COMMENT ON COLUMN "abuse_report_resolver"."updatedAt" IS 'The updated date of AbuseReportResolver'; COMMENT ON COLUMN "abuse_report_resolver"."expirationDate" IS 'The expiration date of AbuseReportResolver'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_fdd74ab625ed0f6a30c47b00e0" ON "abuse_report_resolver" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d90c2c0e555b1eb2e4f19c9ad4" ON "abuse_report_resolver" ("updatedAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e83a32a146021c72ba9bde6675" ON "abuse_report_resolver" ("expirationDate") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e83a32a146021c72ba9bde6675"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d90c2c0e555b1eb2e4f19c9ad4"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_fdd74ab625ed0f6a30c47b00e0"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "abuse_report_resolver"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."abuse_report_resolver_expiresat_enum"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.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 { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.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
|
@bindThis
|
||||||
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
|
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
|
||||||
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
||||||
|
|
|
@ -512,7 +512,7 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
if (users.length < 1) return 'skip';
|
if (users.length < 1) return 'skip';
|
||||||
|
|
||||||
await this.abuseUserReportsRepository.insert({
|
const report = await this.abuseUserReportsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
targetUserId: users[0].id,
|
targetUserId: users[0].id,
|
||||||
|
@ -520,7 +520,9 @@ export class ApInboxService {
|
||||||
reporterId: actor.id,
|
reporterId: actor.id,
|
||||||
reporterHost: actor.host,
|
reporterHost: actor.host,
|
||||||
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
|
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
|
||||||
});
|
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
this.queueService.createReportAbuseJob(report);
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const DI = {
|
||||||
//#region Repositories
|
//#region Repositories
|
||||||
usersRepository: Symbol('usersRepository'),
|
usersRepository: Symbol('usersRepository'),
|
||||||
notesRepository: Symbol('notesRepository'),
|
notesRepository: Symbol('notesRepository'),
|
||||||
|
abuseReportResolversRepository: Symbol('abuseReportResolversRepository'),
|
||||||
announcementsRepository: Symbol('announcementsRepository'),
|
announcementsRepository: Symbol('announcementsRepository'),
|
||||||
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
||||||
appsRepository: Symbol('appsRepository'),
|
appsRepository: Symbol('appsRepository'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -400,6 +400,12 @@ const $userMemosRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $abuseReportResolversRepository: Provider = {
|
||||||
|
provide: DI.abuseReportResolversRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(AbuseReportResolver),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
],
|
],
|
||||||
|
@ -470,6 +476,7 @@ const $userMemosRepository: Provider = {
|
||||||
$flashsRepository,
|
$flashsRepository,
|
||||||
$flashLikesRepository,
|
$flashLikesRepository,
|
||||||
$userMemosRepository,
|
$userMemosRepository,
|
||||||
|
$abuseReportResolversRepository,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
$usersRepository,
|
$usersRepository,
|
||||||
|
@ -538,6 +545,7 @@ const $userMemosRepository: Provider = {
|
||||||
$flashsRepository,
|
$flashsRepository,
|
||||||
$flashLikesRepository,
|
$flashLikesRepository,
|
||||||
$userMemosRepository,
|
$userMemosRepository,
|
||||||
|
$abuseReportResolversRepository,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class RepositoryModule {}
|
export class RepositoryModule {}
|
||||||
|
|
|
@ -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 { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
||||||
import { AccessToken } from '@/models/entities/AccessToken.js';
|
import { AccessToken } from '@/models/entities/AccessToken.js';
|
||||||
import { Ad } from '@/models/entities/Ad.js';
|
import { Ad } from '@/models/entities/Ad.js';
|
||||||
|
@ -67,6 +68,7 @@ import { UserListFavorite } from './entities/UserListFavorite.js';
|
||||||
import type { Repository } from 'typeorm';
|
import type { Repository } from 'typeorm';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AbuseReportResolver,
|
||||||
AbuseUserReport,
|
AbuseUserReport,
|
||||||
AccessToken,
|
AccessToken,
|
||||||
Ad,
|
Ad,
|
||||||
|
@ -135,6 +137,7 @@ export {
|
||||||
UserMemo,
|
UserMemo,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AbuseReportResolversRepository = Repository<AbuseReportResolver>;
|
||||||
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
|
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
|
||||||
export type AccessTokensRepository = Repository<AccessToken>;
|
export type AccessTokensRepository = Repository<AccessToken>;
|
||||||
export type AdsRepository = Repository<Ad>;
|
export type AdsRepository = Repository<Ad>;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { DataSource, Logger } from 'typeorm';
|
||||||
import * as highlight from 'cli-highlight';
|
import * as highlight from 'cli-highlight';
|
||||||
import { entities as charts } from '@/core/chart/entities.js';
|
import { entities as charts } from '@/core/chart/entities.js';
|
||||||
|
|
||||||
|
import { AbuseReportResolver } from '@/models/entities/AbuseReportResolver.js';
|
||||||
import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
||||||
import { AccessToken } from '@/models/entities/AccessToken.js';
|
import { AccessToken } from '@/models/entities/AccessToken.js';
|
||||||
import { Ad } from '@/models/entities/Ad.js';
|
import { Ad } from '@/models/entities/Ad.js';
|
||||||
|
@ -121,6 +122,7 @@ class MyCustomLogger implements Logger {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const entities = [
|
export const entities = [
|
||||||
|
AbuseReportResolver,
|
||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementRead,
|
AnnouncementRead,
|
||||||
Meta,
|
Meta,
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { ImportMutingProcessorService } from './processors/ImportMutingProcessor
|
||||||
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
|
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
|
||||||
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||||
|
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
||||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||||
|
@ -64,6 +65,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
DeleteFileProcessorService,
|
DeleteFileProcessorService,
|
||||||
CleanRemoteFilesProcessorService,
|
CleanRemoteFilesProcessorService,
|
||||||
RelationshipProcessorService,
|
RelationshipProcessorService,
|
||||||
|
ReportAbuseProcessorService,
|
||||||
WebhookDeliverProcessorService,
|
WebhookDeliverProcessorService,
|
||||||
EndedPollNotificationProcessorService,
|
EndedPollNotificationProcessorService,
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
|
||||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||||
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
||||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||||
|
import { ReportAbuseProcessorService } from './processors/ReportAbuseProcessorService.js';
|
||||||
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
|
||||||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
|
@ -102,6 +103,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private deleteFileProcessorService: DeleteFileProcessorService,
|
private deleteFileProcessorService: DeleteFileProcessorService,
|
||||||
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
||||||
private relationshipProcessorService: RelationshipProcessorService,
|
private relationshipProcessorService: RelationshipProcessorService,
|
||||||
|
private reportAbuseProcessorService: ReportAbuseProcessorService,
|
||||||
private tickChartsProcessorService: TickChartsProcessorService,
|
private tickChartsProcessorService: TickChartsProcessorService,
|
||||||
private resyncChartsProcessorService: ResyncChartsProcessorService,
|
private resyncChartsProcessorService: ResyncChartsProcessorService,
|
||||||
private cleanChartsProcessorService: CleanChartsProcessorService,
|
private cleanChartsProcessorService: CleanChartsProcessorService,
|
||||||
|
@ -174,6 +176,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
||||||
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
||||||
case 'deleteAccount': return this.deleteAccountProcessorService.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`);
|
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 { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import type { User } from '@/models/entities/User.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 { Webhook } from '@/models/entities/Webhook.js';
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
|
@ -86,6 +87,8 @@ export type DbUserImportToDbJobData = {
|
||||||
target: string;
|
target: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DbAbuseReportJobData = AbuseUserReport;
|
||||||
|
|
||||||
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
|
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
|
||||||
|
|
||||||
export type ObjectStorageFileJobData = {
|
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_delete from './endpoints/admin/announcements/delete.js';
|
||||||
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.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_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_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_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.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_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_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_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_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_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 };
|
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_delete,
|
||||||
$admin_announcements_list,
|
$admin_announcements_list,
|
||||||
$admin_announcements_update,
|
$admin_announcements_update,
|
||||||
|
$admin_abuseReportResolver_create,
|
||||||
|
$admin_abuseReportResolver_delete,
|
||||||
|
$admin_abuseReportResolver_list,
|
||||||
|
$admin_abuseReportResolver_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
|
@ -1050,6 +1062,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_announcements_delete,
|
$admin_announcements_delete,
|
||||||
$admin_announcements_list,
|
$admin_announcements_list,
|
||||||
$admin_announcements_update,
|
$admin_announcements_update,
|
||||||
|
$admin_abuseReportResolver_create,
|
||||||
|
$admin_abuseReportResolver_delete,
|
||||||
|
$admin_abuseReportResolver_list,
|
||||||
|
$admin_abuseReportResolver_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$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_delete from './endpoints/admin/announcements/delete.js';
|
||||||
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.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_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_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_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.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/delete', ep___admin_announcements_delete],
|
||||||
['admin/announcements/list', ep___admin_announcements_list],
|
['admin/announcements/list', ep___admin_announcements_list],
|
||||||
['admin/announcements/update', ep___admin_announcements_update],
|
['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/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||||
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
||||||
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
['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 { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js';
|
import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.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 { DI } from '@/di-symbols.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -59,11 +56,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
|
||||||
private emailService: EmailService,
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private globalEventService: GlobalEventService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Lookup user
|
// Lookup user
|
||||||
|
@ -90,26 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
comment: ps.comment,
|
comment: ps.comment,
|
||||||
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
// Publish event to moderators
|
this.queueService.createReportAbuseJob(report);
|
||||||
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
import MkAbuseReportResolver from './MkAbuseReportResolver.vue';
|
||||||
|
import type { StoryObj } from '@storybook/vue3';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkAbuseReportResolver,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkAbuseReportResolver v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
editable: true,
|
||||||
|
data: {
|
||||||
|
name: 'Sample',
|
||||||
|
targetUserPattern: '^.*@.+$',
|
||||||
|
reporterPattern: null,
|
||||||
|
reportContentPattern: null,
|
||||||
|
expiresAt: 'indefinitely',
|
||||||
|
forward: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkAbuseReportResolver>;
|
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<div class="_gaps dslkjkwejflew">
|
||||||
|
<MkInput v-model="value.name" :readonly="!props.editable">
|
||||||
|
<template #label>{{ i18n.ts.name }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<div>
|
||||||
|
<div :class="$style.label">{{ i18n.ts._abuse._resolver.targetUserPattern }}</div>
|
||||||
|
<PrismEditor v-model="value.targetUserPattern" class="_code code" :class="$style.highlight" :highlight="highlighter" :lineNumbers="false" :readonly="!props.editable"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div :class="$style.label">{{ i18n.ts._abuse._resolver.reporterPattern }}</div>
|
||||||
|
<PrismEditor v-model="value.reporterPattern" class="_code code" :class="$style.highlight" :highlight="highlighter" :lineNumbers="false" :readonly="!props.editable"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div :class="$style.label">{{ i18n.ts._abuse._resolver.reportContentPattern }}</div>
|
||||||
|
<PrismEditor v-model="value.reportContentPattern" class="_code code" :class="$style.highlight" :highlight="highlighter" :lineNumbers="false" :readonly="!props.editable"/>
|
||||||
|
</div>
|
||||||
|
<MkSelect v-model="value.expiresAt" :disabled="!props.editable">
|
||||||
|
<template #label>{{ i18n.ts._abuse._resolver.expiresAt }}<span v-if="expirationDate" style="float: right;">{{ expirationDate }}</span></template>
|
||||||
|
<option value="1hour">{{ i18n.ts._abuse._resolver['1hour'] }}</option>
|
||||||
|
<option value="12hours">{{ i18n.ts._abuse._resolver['12hours'] }}</option>
|
||||||
|
<option value="1day">{{ i18n.ts._abuse._resolver['1day'] }}</option>
|
||||||
|
<option value="1week">{{ i18n.ts._abuse._resolver['1week'] }}</option>
|
||||||
|
<option value="1month">{{ i18n.ts._abuse._resolver['1month'] }}</option>
|
||||||
|
<option value="3months">{{ i18n.ts._abuse._resolver['3months'] }}</option>
|
||||||
|
<option value="6months">{{ i18n.ts._abuse._resolver['6months'] }}</option>
|
||||||
|
<option value="1year">{{ i18n.ts._abuse._resolver['1year'] }}</option>
|
||||||
|
<option value="indefinitely">{{ i18n.ts._abuse._resolver.indefinitely }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSwitch v-model="value.forward" :disabled="!props.editable">
|
||||||
|
{{ i18n.ts.forwardReport }}
|
||||||
|
<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<slot name="button"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
|
import { PrismEditor } from 'vue-prism-editor';
|
||||||
|
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import 'vue-prism-editor/dist/prismeditor.min.css';
|
||||||
|
import 'prismjs/components/prism-clike';
|
||||||
|
import 'prismjs/components/prism-regex';
|
||||||
|
import 'prismjs/themes/prism-okaidia.css';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: {
|
||||||
|
name: string;
|
||||||
|
targetUserPattern: string | null;
|
||||||
|
reporterPattern: string | null;
|
||||||
|
reportContentPattern: string | null;
|
||||||
|
expiresAt: string;
|
||||||
|
forward: boolean;
|
||||||
|
expirationDate: string;
|
||||||
|
previousExpiresAt?: string;
|
||||||
|
}
|
||||||
|
editable: boolean;
|
||||||
|
data?: {
|
||||||
|
name: string;
|
||||||
|
targetUserPattern: string | null;
|
||||||
|
reporterPattern: string | null;
|
||||||
|
reportContentPattern: string | null;
|
||||||
|
expirationDate: string | null;
|
||||||
|
expiresAt: string;
|
||||||
|
forward: boolean;
|
||||||
|
previousExpiresAt?: string;
|
||||||
|
}
|
||||||
|
}>();
|
||||||
|
let expirationDate: string | null = $ref(null);
|
||||||
|
|
||||||
|
type NonNullType<T> = {
|
||||||
|
[P in keyof T]: NonNullable<T[P]>
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
const data = props.data ?? props.modelValue ?? {
|
||||||
|
name: '',
|
||||||
|
targetUserPattern: '',
|
||||||
|
reporterPattern: '',
|
||||||
|
reportContentPattern: '',
|
||||||
|
expirationDate: null,
|
||||||
|
expiresAt: 'indefinitely',
|
||||||
|
forward: false,
|
||||||
|
previousExpiresAt: undefined,
|
||||||
|
};
|
||||||
|
for (const [key, _value] of Object.entries(data)) {
|
||||||
|
if (_value === null) {
|
||||||
|
data[key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.modelValue && props.editable) {
|
||||||
|
emit('update:modelValue', data);
|
||||||
|
}
|
||||||
|
return data as NonNullType<typeof data>;
|
||||||
|
},
|
||||||
|
set(updateValue) {
|
||||||
|
if (props.modelValue && props.editable) {
|
||||||
|
emit('update:modelValue', updateValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function highlighter(code) {
|
||||||
|
return highlight(code, languages.regex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExpirationDate(empty = false) {
|
||||||
|
if (value.value.expirationDate && !empty) {
|
||||||
|
expirationDate = new Date(value.value.expirationDate).toLocaleString();
|
||||||
|
} else {
|
||||||
|
expirationDate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => value.value.expirationDate, () => renderExpirationDate(), { immediate: true });
|
||||||
|
watch(() => value.value.expiresAt, () => renderExpirationDate(true));
|
||||||
|
watch(() => props.editable, () => {
|
||||||
|
if (props.editable) {
|
||||||
|
value.value.previousExpiresAt = value.value.expiresAt;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.dslkjkwejflew .prism-editor__textarea {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dslkjkwejflew .prism-editor__editor {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style lang="scss" module>
|
||||||
|
.label {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 0 0 8px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -108,6 +108,10 @@ onMounted(() => {
|
||||||
const myBg = computedStyle.getPropertyValue('--panel');
|
const myBg = computedStyle.getPropertyValue('--panel');
|
||||||
bgSame = parentBg === myBg;
|
bgSame = parentBg === myBg;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggle,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
:step="step"
|
:step="step"
|
||||||
:list="id"
|
:list="id"
|
||||||
@focus="focused = true"
|
@focus="onFocus"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
@keydown="onKeydown($event)"
|
@keydown="onKeydown($event)"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
@ -98,6 +98,12 @@ const onKeydown = (ev: KeyboardEvent) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
if (!(props.readonly || props.disabled)) {
|
||||||
|
focused.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updated = () => {
|
const updated = () => {
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
if (type.value === 'number') {
|
if (type.value === 'number') {
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
:class="$style.inputCore"
|
:class="$style.inputCore"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="required"
|
:required="required"
|
||||||
:readonly="readonly"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
|
@ -60,7 +59,7 @@ const opening = ref(false);
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
const invalid = ref(false);
|
const invalid = ref(false);
|
||||||
const filled = computed(() => v.value !== '' && v.value != null);
|
const filled = computed(() => v.value !== '' && v.value != null);
|
||||||
const inputEl = ref(null);
|
const inputEl = ref<HTMLSelectElement | null>(null);
|
||||||
const prefixEl = ref(null);
|
const prefixEl = ref(null);
|
||||||
const suffixEl = ref(null);
|
const suffixEl = ref(null);
|
||||||
const container = ref(null);
|
const container = ref(null);
|
||||||
|
@ -119,6 +118,9 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function show(ev: MouseEvent) {
|
function show(ev: MouseEvent) {
|
||||||
|
if (inputEl.value && inputEl.value.hasAttribute('disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
focused.value = true;
|
focused.value = true;
|
||||||
opening.value = true;
|
opening.value = true;
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="900">
|
<MkSpacer :contentMax="900">
|
||||||
<div>
|
<div v-if="tab === 'list'">
|
||||||
<div class="reports">
|
<div class="reports">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="inputs" style="display: flex;">
|
<div class="inputs" style="display: flex;">
|
||||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
<MkSelect v-model="state" :class="$style.state">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
<option value="all">{{ i18n.ts.all }}</option>
|
||||||
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
||||||
<option value="resolved">{{ i18n.ts.resolved }}</option>
|
<option value="resolved">{{ i18n.ts.resolved }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
<MkSelect v-model="targetUserOrigin" :class="$style.targetUserOrigin">
|
||||||
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
<option value="combined">{{ i18n.ts.all }}</option>
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
<option value="local">{{ i18n.ts.local }}</option>
|
||||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
<MkSelect v-model="reporterOrigin" :class="$style.reporterOrigin">
|
||||||
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
<option value="combined">{{ i18n.ts.all }}</option>
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
<option value="local">{{ i18n.ts.local }}</option>
|
||||||
|
@ -42,27 +42,89 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkFolder ref="folderComponent">
|
||||||
|
<template #label><i class="ti ti-plus" style="margin-right: 5px;"></i>{{ i18n.ts.createNew }}</template>
|
||||||
|
<MkAbuseReportResolver v-model="newResolver" :editable="true">
|
||||||
|
<template #button>
|
||||||
|
<MkButton primary :class="$style.margin" @click="create">{{ i18n.ts.create }}</MkButton>
|
||||||
|
</template>
|
||||||
|
</MkAbuseReportResolver>
|
||||||
|
</MkFolder>
|
||||||
|
<MkPagination v-slot="{items}" ref="resolverPagingComponent" :pagination="resolverPagination">
|
||||||
|
<MkSpacer v-for="resolver in items" :key="resolver.id" :marginMin="14" :marginMax="22" :class="$style.resolverList">
|
||||||
|
<MkAbuseReportResolver v-model="editingResolver" :data="(resolver as any)" :editable="editableResolver === resolver.id">
|
||||||
|
<template #button>
|
||||||
|
<div v-if="editableResolver !== resolver.id">
|
||||||
|
<MkButton primary inline :class="$style['button-margin']" @click="edit(resolver.id)"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton>
|
||||||
|
<MkButton danger inline @click="deleteResolver(resolver.id)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<MkButton primary inline @click="save">{{ i18n.ts.save }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkAbuseReportResolver>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkAbuseReportResolver from '@/components/MkAbuseReportResolver.vue';
|
||||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
let reports = $shallowRef<InstanceType<typeof MkPagination>>();
|
let reports = $shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
let resolverPagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||||
|
let folderComponent = $ref<InstanceType<typeof MkFolder>>();
|
||||||
|
|
||||||
let state = $ref('unresolved');
|
let state = $ref('unresolved');
|
||||||
let reporterOrigin = $ref('combined');
|
let reporterOrigin = $ref('combined');
|
||||||
let targetUserOrigin = $ref('combined');
|
let targetUserOrigin = $ref('combined');
|
||||||
let searchUsername = $ref('');
|
let tab = $ref('list');
|
||||||
let searchHost = $ref('');
|
let editableResolver: null | string = $ref(null);
|
||||||
|
const defaultResolver = {
|
||||||
|
name: '',
|
||||||
|
targetUserPattern: '',
|
||||||
|
reporterPattern: '',
|
||||||
|
reportContentPattern: '',
|
||||||
|
expirationDate: '',
|
||||||
|
expiresAt: 'indefinitely',
|
||||||
|
forward: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let newResolver = $ref<{
|
||||||
|
name: string;
|
||||||
|
targetUserPattern: string;
|
||||||
|
reporterPattern: string;
|
||||||
|
reportContentPattern: string;
|
||||||
|
expirationDate: string;
|
||||||
|
expiresAt: string;
|
||||||
|
forward: boolean;
|
||||||
|
}>(defaultResolver);
|
||||||
|
|
||||||
|
let editingResolver = $ref<{
|
||||||
|
name: string;
|
||||||
|
targetUserPattern: string;
|
||||||
|
reporterPattern: string;
|
||||||
|
reportContentPattern: string;
|
||||||
|
expiresAt: string;
|
||||||
|
expirationDate: string;
|
||||||
|
forward: boolean;
|
||||||
|
previousExpiresAt?: string;
|
||||||
|
}>(defaultResolver);
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'admin/abuse-user-reports' as const,
|
endpoint: 'admin/abuse-user-reports' as const,
|
||||||
|
@ -74,16 +136,108 @@ const pagination = {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolverPagination = {
|
||||||
|
endpoint: 'admin/abuse-report-resolver/list' as const,
|
||||||
|
limit: 10,
|
||||||
|
};
|
||||||
|
|
||||||
function resolved(reportId) {
|
function resolved(reportId) {
|
||||||
reports.removeItem(reportId);
|
reports!.removeItem(reportId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(id: string) {
|
||||||
|
editableResolver = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(): void {
|
||||||
|
os.apiWithDialog('admin/abuse-report-resolver/update', {
|
||||||
|
resolverId: editableResolver,
|
||||||
|
name: editingResolver.name,
|
||||||
|
targetUserPattern: editingResolver.targetUserPattern || null,
|
||||||
|
reporterPattern: editingResolver.reporterPattern || null,
|
||||||
|
reportContentPattern: editingResolver.reportContentPattern || null,
|
||||||
|
...(editingResolver.previousExpiresAt && editingResolver.previousExpiresAt === editingResolver.expiresAt ? {} : {
|
||||||
|
expiresAt: editingResolver.expiresAt,
|
||||||
|
}),
|
||||||
|
forward: editingResolver.forward,
|
||||||
|
}).then(() => {
|
||||||
|
editableResolver = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteResolver(id: string): void {
|
||||||
|
os.apiWithDialog('admin/abuse-report-resolver/delete', {
|
||||||
|
resolverId: id,
|
||||||
|
}).then(() => {
|
||||||
|
resolverPagingComponent?.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(): void {
|
||||||
|
os.apiWithDialog('admin/abuse-report-resolver/create', {
|
||||||
|
name: newResolver.name,
|
||||||
|
targetUserPattern: newResolver.targetUserPattern || null,
|
||||||
|
reporterPattern: newResolver.reporterPattern || null,
|
||||||
|
reportContentPattern: newResolver.reportContentPattern || null,
|
||||||
|
expiresAt: newResolver.expiresAt,
|
||||||
|
forward: newResolver.forward,
|
||||||
|
}).then(() => {
|
||||||
|
folderComponent?.toggle();
|
||||||
|
resolverPagingComponent?.reload();
|
||||||
|
newResolver.name = '';
|
||||||
|
newResolver.targetUserPattern = '';
|
||||||
|
newResolver.reporterPattern = '';
|
||||||
|
newResolver.reportContentPattern = '';
|
||||||
|
newResolver.expiresAt = 'indefinitely';
|
||||||
|
newResolver.forward = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'list',
|
||||||
|
title: i18n.ts._abuse.list,
|
||||||
|
}, {
|
||||||
|
key: 'resolver',
|
||||||
|
title: i18n.ts._abuse.resolver,
|
||||||
|
}]);
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.abuseReports,
|
title: i18n.ts.abuseReports,
|
||||||
icon: 'ti ti-exclamation-circle',
|
icon: 'ti ti-exclamation-circle',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" module>
|
||||||
|
.input-base {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-margin {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
@extend .input-base;
|
||||||
|
@extend .button-margin;
|
||||||
|
}
|
||||||
|
.reporterOrigin {
|
||||||
|
@extend .input-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.targetUserOrigin {
|
||||||
|
@extend .input-base;
|
||||||
|
@extend .button-margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin {
|
||||||
|
margin: 0 auto var(--margin) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolverList {
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -430,6 +430,22 @@ export type Endpoints = {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
};
|
};
|
||||||
|
'admin/abuse-report-resolver/create': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
|
'admin/abuse-report-resolver/list': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
|
'admin/abuse-report-resolver/update': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
|
'admin/abuse-report-resolver/delete': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
'admin/drive/clean-remote-files': {
|
'admin/drive/clean-remote-files': {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
|
@ -2813,7 +2829,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
//
|
//
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:629:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -44,6 +44,10 @@ export type Endpoints = {
|
||||||
'admin/announcements/delete': { req: { id: Announcement['id'] }; res: null; };
|
'admin/announcements/delete': { req: { id: Announcement['id'] }; res: null; };
|
||||||
'admin/announcements/list': { req: TODO; res: TODO; };
|
'admin/announcements/list': { req: TODO; res: TODO; };
|
||||||
'admin/announcements/update': { req: TODO; res: TODO; };
|
'admin/announcements/update': { req: TODO; res: TODO; };
|
||||||
|
'admin/abuse-report-resolver/create': { req: TODO; res: TODO; };
|
||||||
|
'admin/abuse-report-resolver/list': { req: TODO; res: TODO; };
|
||||||
|
'admin/abuse-report-resolver/update': { req: TODO; res: TODO; };
|
||||||
|
'admin/abuse-report-resolver/delete': { req: TODO; res: TODO; };
|
||||||
'admin/drive/clean-remote-files': { req: TODO; res: TODO; };
|
'admin/drive/clean-remote-files': { req: TODO; res: TODO; };
|
||||||
'admin/drive/cleanup': { req: TODO; res: TODO; };
|
'admin/drive/cleanup': { req: TODO; res: TODO; };
|
||||||
'admin/drive/files': { req: TODO; res: TODO; };
|
'admin/drive/files': { req: TODO; res: TODO; };
|
||||||
|
|
Loading…
Reference in New Issue