mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-10-30 06:41:46 +09:00
feat: 通報の即時解決機能を追加 (misskey-dev/misskey#11032)
This commit is contained in:
parent
20d6591a60
commit
d576959e9b
@ -38,6 +38,7 @@
|
||||
- 유저 메뉴에서 '리모트 유저 정보 갱신'을 진행할 수 있도록
|
||||
- 설정에서 변경 가능한 옵션은 다이얼로그에서 안내하도록 주석 추가
|
||||
- 메모리 할당자를 jemalloc으로 설정 (MisskeyIO/misskey#152)
|
||||
- 신고 즉시 해결 기능 (misskey-dev/misskey#11032)
|
||||
|
||||
### Client
|
||||
- about-misskey 페이지에서 클라이언트 버전을 누르면 변경 사항을 볼 수 있음
|
||||
|
@ -1161,6 +1161,9 @@ expired: "Expired"
|
||||
doYouAgree: "Agree?"
|
||||
beSureToReadThisAsItIsImportant: "Please read this important information."
|
||||
iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree."
|
||||
doNotSendNotificationEmailsForAbuseReport: "Do not send out notification emails for reporting"
|
||||
emailToReceiveAbuseReport: "Email address to receive notification of the report"
|
||||
emailToReceiveAbuseReportCaption: "Specify the email address to receive notification of the report. If this field is left blank, the mail server's email address will be used."
|
||||
dialog: "Dialog"
|
||||
icon: "Icon"
|
||||
forYou: "For you"
|
||||
@ -2298,3 +2301,20 @@ _webhookSettings:
|
||||
renote: "When renoted"
|
||||
reaction: "When receiving a reaction"
|
||||
mention: "When being mentioned"
|
||||
_abuse:
|
||||
_resolver:
|
||||
1hour: "one hour"
|
||||
12hours: "half day"
|
||||
1day: "one day"
|
||||
1week: "one week"
|
||||
1month: "one month"
|
||||
3months: "three months"
|
||||
6months: "six months"
|
||||
1year: "one year"
|
||||
indefinitely: "indefinitely"
|
||||
expiresAt: "Expiration date of this condition"
|
||||
targetUserPattern: "Patterns to report to"
|
||||
reporterPattern: "Pattern of reporting source"
|
||||
reportContentPattern: "Patterns of Reported Contents"
|
||||
list: "List"
|
||||
resolver: "Resolver"
|
||||
|
22
locales/index.d.ts
vendored
22
locales/index.d.ts
vendored
@ -1165,6 +1165,9 @@ export interface Locale {
|
||||
"doYouAgree": string;
|
||||
"beSureToReadThisAsItIsImportant": string;
|
||||
"iHaveReadXCarefullyAndAgree": string;
|
||||
"doNotSendNotificationEmailsForAbuseReport": string;
|
||||
"emailToReceiveAbuseReport": string;
|
||||
"emailToReceiveAbuseReportCaption": string;
|
||||
"dialog": string;
|
||||
"icon": string;
|
||||
"forYou": string;
|
||||
@ -2464,6 +2467,25 @@ export interface Locale {
|
||||
"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: {
|
||||
[lang: string]: Locale;
|
||||
|
@ -1162,6 +1162,9 @@ expired: "期限切れ"
|
||||
doYouAgree: "同意しますか?"
|
||||
beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。"
|
||||
iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。"
|
||||
doNotSendNotificationEmailsForAbuseReport: "通報の通知メールを発送しないようにする"
|
||||
emailToReceiveAbuseReport: "通報通知を受け取るためのメールアドレス"
|
||||
emailToReceiveAbuseReportCaption: "通報通知を受け取るためのメールアドレスを指定します。ここの入力欄を空にするとメールサーバーのメールアドレスが使用されます。"
|
||||
dialog: "ダイアログ"
|
||||
icon: "アイコン"
|
||||
forYou: "あなたへ"
|
||||
@ -2375,3 +2378,21 @@ _webhookSettings:
|
||||
renote: "Renoteされたとき"
|
||||
reaction: "リアクションがあったとき"
|
||||
mention: "メンションされたとき"
|
||||
|
||||
_abuse:
|
||||
_resolver:
|
||||
1hour: "一時間"
|
||||
12hours: "半日"
|
||||
1day: "一日"
|
||||
1week: "一週間"
|
||||
1month: "一ヶ月"
|
||||
3months: "三ヶ月"
|
||||
6months: "六ヶ月"
|
||||
1year: "一年"
|
||||
indefinitely: "無期限"
|
||||
expiresAt: "この条件の有効期限"
|
||||
targetUserPattern: "通報先のパターン"
|
||||
reporterPattern: "通報元のパターン"
|
||||
reportContentPattern: "通報内容のパターン"
|
||||
list: "一覧"
|
||||
resolver: "リソルバー"
|
||||
|
@ -1162,6 +1162,9 @@ expired: "만료됨"
|
||||
doYouAgree: "동의하십니까?"
|
||||
beSureToReadThisAsItIsImportant: "중요하므로 반드시 읽어주세요."
|
||||
iHaveReadXCarefullyAndAgree: "\"{x}\"의 내용을 읽고 동의할게요."
|
||||
doNotSendNotificationEmailsForAbuseReport: "신고 알림 메일을 발송하지 않기"
|
||||
emailToReceiveAbuseReport: "신고 알림을 받을 수 있는 이메일 주소"
|
||||
emailToReceiveAbuseReportCaption: "신고 알림을 받을 이메일 주소를 지정해 주세요. 이곳의 입력란을 비워두면 메일 서버의 이메일 주소가 사용돼요."
|
||||
dialog: "다이얼로그"
|
||||
icon: "아이콘"
|
||||
forYou: "당신에게"
|
||||
@ -2295,3 +2298,20 @@ _webhookSettings:
|
||||
renote: "누군가 내 글을 리노트 했을 때"
|
||||
reaction: "누군가 내 노트에 리액션 했을 때"
|
||||
mention: "누군가 나를 멘션 했을 때"
|
||||
_abuse:
|
||||
_resolver:
|
||||
1hour: "1시간"
|
||||
12hours: "반나절"
|
||||
1day: "하루"
|
||||
1week: "일주일"
|
||||
1month: "1개월"
|
||||
3months: "3개월"
|
||||
6months: "6개월"
|
||||
1year: "1년"
|
||||
indefinitely: "무기한"
|
||||
expiresAt: "이 조건의 유효기간"
|
||||
targetUserPattern: "신고 대상 패턴"
|
||||
reporterPattern: "신고자 패턴"
|
||||
reportContentPattern: "신고 내용 패턴"
|
||||
list: "목록"
|
||||
resolver: "리졸버"
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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"`);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class NotificationEmailsForAbuseReport1691120548582 {
|
||||
name = 'NotificationEmailsForAbuseReport1691120548582'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "emailToReceiveAbuseReport" character varying(1024)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "doNotSendNotificationEmailsForAbuseReport" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "doNotSendNotificationEmailsForAbuseReport"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailToReceiveAbuseReport"`);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { MiAbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
||||
import type { MiWebhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@ -341,6 +342,11 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createReportAbuseJob(report: MiAbuseUserReport) {
|
||||
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));
|
||||
|
@ -552,7 +552,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,
|
||||
@ -560,7 +560,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';
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export const DI = {
|
||||
//#region Repositories
|
||||
usersRepository: Symbol('usersRepository'),
|
||||
notesRepository: Symbol('notesRepository'),
|
||||
abuseReportResolversRepository: Symbol('abuseReportResolversRepository'),
|
||||
announcementsRepository: Symbol('announcementsRepository'),
|
||||
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
||||
appsRepository: Symbol('appsRepository'),
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAttestationChallenge, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEvent, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMessagingMessage, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserGroup, MiUserGroupJoining, MiUserGroupInvitation, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './index.js';
|
||||
import { MiAbuseReportResolver, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAttestationChallenge, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEvent, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMessagingMessage, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserGroup, MiUserGroupJoining, MiUserGroupInvitation, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './index.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
@ -435,6 +435,12 @@ const $userMemosRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $abuseReportResolversRepository: Provider = {
|
||||
provide: DI.abuseReportResolversRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportResolver),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
@ -510,6 +516,7 @@ const $userMemosRepository: Provider = {
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$abuseReportResolversRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@ -583,6 +590,7 @@ const $userMemosRepository: Provider = {
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$abuseReportResolversRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
63
packages/backend/src/models/entities/AbuseReportResolver.ts
Normal file
63
packages/backend/src/models/entities/AbuseReportResolver.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Column, Entity, PrimaryColumn, Index } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
|
||||
@Entity('abuse_report_resolver')
|
||||
export class MiAbuseReportResolver {
|
||||
@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;
|
||||
}
|
@ -41,6 +41,11 @@ export class MiMeta {
|
||||
})
|
||||
public maintainerEmail: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: true,
|
||||
})
|
||||
public emailToReceiveAbuseReport: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
@ -540,6 +545,11 @@ export class MiMeta {
|
||||
})
|
||||
public enableIdenticonGeneration: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public doNotSendNotificationEmailsForAbuseReport: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: { },
|
||||
})
|
||||
|
@ -3,6 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { MiAbuseReportResolver } from '@/models/entities/AbuseReportResolver.js';
|
||||
import { MiAbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
||||
import { MiAccessToken } from '@/models/entities/AccessToken.js';
|
||||
import { MiAd } from '@/models/entities/Ad.js';
|
||||
@ -77,6 +78,7 @@ import { MiUserListFavorite } from './entities/UserListFavorite.js';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
export {
|
||||
MiAbuseReportResolver,
|
||||
MiAbuseUserReport,
|
||||
MiAccessToken,
|
||||
MiAd,
|
||||
@ -150,6 +152,7 @@ export {
|
||||
MiUserMemo,
|
||||
};
|
||||
|
||||
export type AbuseReportResolversRepository = Repository<MiAbuseReportResolver>;
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
|
||||
export type AccessTokensRepository = Repository<MiAccessToken>;
|
||||
export type AdsRepository = Repository<MiAd>;
|
||||
|
@ -11,6 +11,7 @@ import { DataSource, Logger } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import { entities as charts } from '@/core/chart/entities.js';
|
||||
|
||||
import { MiAbuseReportResolver } from '@/models/entities/AbuseReportResolver.js';
|
||||
import { MiAbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
||||
import { MiAccessToken } from '@/models/entities/AccessToken.js';
|
||||
import { MiAd } from '@/models/entities/Ad.js';
|
||||
@ -131,6 +132,7 @@ class MyCustomLogger implements Logger {
|
||||
}
|
||||
|
||||
export const entities = [
|
||||
MiAbuseReportResolver,
|
||||
MiAnnouncement,
|
||||
MiAnnouncementRead,
|
||||
MiMeta,
|
||||
|
@ -33,6 +33,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';
|
||||
@ -69,6 +70,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||
DeleteFileProcessorService,
|
||||
CleanRemoteFilesProcessorService,
|
||||
RelationshipProcessorService,
|
||||
ReportAbuseProcessorService,
|
||||
WebhookDeliverProcessorService,
|
||||
EndedPollNotificationProcessorService,
|
||||
DeliverProcessorService,
|
||||
|
@ -32,6 +32,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';
|
||||
@ -107,6 +108,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,
|
||||
@ -179,6 +181,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,119 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 && job.data.reporterHost === 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 && job.data.reporterHost === 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.emailToReceiveAbuseReport || meta.email) && !meta.doNotSendNotificationEmailsForAbuseReport) {
|
||||
this.emailService.sendEmail(meta.emailToReceiveAbuseReport ?? meta.email!, 'New abuse report',
|
||||
sanitizeHtml(job.data.comment),
|
||||
sanitizeHtml(job.data.comment));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import type { MiDriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { MiNote } from '@/models/entities/Note.js';
|
||||
import type { MiUser } from '@/models/entities/User.js';
|
||||
import type { MiAbuseUserReport } from '@/models/entities/AbuseUserReport.js';
|
||||
import type { MiWebhook } from '@/models/entities/Webhook.js';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
@ -91,6 +92,8 @@ export type DbUserImportToDbJobData = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type DbAbuseReportJobData = MiAbuseUserReport;
|
||||
|
||||
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;
|
||||
|
||||
export type ObjectStorageFileJobData = {
|
||||
|
@ -18,6 +18,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';
|
||||
@ -391,6 +395,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 };
|
||||
@ -768,6 +776,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,
|
||||
@ -1138,6 +1150,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,
|
||||
|
@ -18,6 +18,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';
|
||||
@ -388,6 +392,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,141 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
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 RegExp(ps.targetUserPattern);
|
||||
} catch (e) {
|
||||
throw new ApiError(meta.errors.invalidRegularExpressionForTargetUser);
|
||||
}
|
||||
}
|
||||
if (ps.reporterPattern) {
|
||||
try {
|
||||
new RegExp(ps.reporterPattern);
|
||||
} catch (e) {
|
||||
throw new ApiError(meta.errors.invalidRegularExpressionForReporter);
|
||||
}
|
||||
}
|
||||
if (ps.reportContentPattern) {
|
||||
try {
|
||||
new RegExp(ps.reportContentPattern);
|
||||
} catch (e) {
|
||||
throw new ApiError(meta.errors.invalidRegularExpressionForReportContent);
|
||||
}
|
||||
}
|
||||
const now = new Date();
|
||||
let expirationDate: Date | null = new Date();
|
||||
|
||||
const previousMonth = expirationDate.getUTCMonth();
|
||||
(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((previousMonth + 1 + 1) / 12))); } :
|
||||
ps.expiresAt === '3months' ? function () {expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 3 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((previousMonth + 3 + 1) / 12))); } :
|
||||
ps.expiresAt === '6months' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 6 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((previousMonth + 6 + 1) / 12))); } :
|
||||
ps.expiresAt === '1year' ? function () { expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + 1); } : 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,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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,82 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AbuseReportResolversRepository, MiAbuseReportResolver } 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<MiAbuseReportResolver, '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 RegExp(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 RegExp(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 RegExp(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();
|
||||
const previousMonth = expirationDate.getUTCMonth();
|
||||
(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((previousMonth + 1 + 1) / 12))); } :
|
||||
ps.expiresAt === '3months' ? function () {expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 3 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((previousMonth + 3 + 1) / 12))); } :
|
||||
ps.expiresAt === '6months' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 6 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((previousMonth + 6 + 1) / 12))); } :
|
||||
ps.expiresAt === '1year' ? function () { expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + 1); } : function () { expirationDate = null; })();
|
||||
|
||||
properties.expiresAt = ps.expiresAt;
|
||||
properties.expirationDate = expirationDate;
|
||||
}
|
||||
|
||||
await this.abuseReportResolversRepository.update(ps.resolverId, {
|
||||
...properties,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -330,6 +330,14 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
doNotSendNotificationEmailsForAbuseReport: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
emailToReceiveAbuseReport: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
policies: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
@ -456,6 +464,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
||||
enableServerMachineStats: instance.enableServerMachineStats,
|
||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||
doNotSendNotificationEmailsForAbuseReport: instance.doNotSendNotificationEmailsForAbuseReport,
|
||||
emailToReceiveAbuseReport: instance.emailToReceiveAbuseReport,
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
};
|
||||
});
|
||||
|
@ -123,6 +123,8 @@ export const paramDef = {
|
||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||
enableServerMachineStats: { type: 'boolean' },
|
||||
enableIdenticonGeneration: { type: 'boolean' },
|
||||
doNotSendNotificationEmailsForAbuseReport: { type: 'boolean' },
|
||||
emailToReceiveAbuseReport: { type: 'string', nullable: true },
|
||||
serverRules: { type: 'array', items: { type: 'string' } },
|
||||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
@ -515,6 +517,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
|
||||
}
|
||||
|
||||
if (ps.doNotSendNotificationEmailsForAbuseReport !== undefined) {
|
||||
set.doNotSendNotificationEmailsForAbuseReport = ps.doNotSendNotificationEmailsForAbuseReport;
|
||||
}
|
||||
|
||||
if (ps.emailToReceiveAbuseReport !== undefined) {
|
||||
set.emailToReceiveAbuseReport = ps.emailToReceiveAbuseReport;
|
||||
}
|
||||
|
||||
if (ps.serverRules !== undefined) {
|
||||
set.serverRules = ps.serverRules;
|
||||
}
|
||||
|
@ -3,17 +3,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { 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 = {
|
||||
@ -60,11 +57,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
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
|
||||
@ -91,26 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -451,6 +451,22 @@ export type Endpoints = {
|
||||
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;
|
||||
@ -2889,7 +2905,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:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:654:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:658: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
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
@ -44,6 +44,10 @@ export type Endpoints = {
|
||||
'admin/announcements/delete': { req: { id: Announcement['id'] }; res: null; };
|
||||
'admin/announcements/list': { 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/cleanup': { req: TODO; res: TODO; };
|
||||
'admin/drive/files': { req: TODO; res: TODO; };
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* 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>;
|
160
packages/frontend/src/components/MkAbuseReportResolver.vue
Normal file
160
packages/frontend/src/components/MkAbuseReportResolver.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<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" placeholder="^(LocalUser|RemoteUser@RemoteHost)$" class="_code code" :class="$style.highlight" :highlight="highlighter" :lineNumbers="false" :ignoreTabKey="true" :readonly="!props.editable"/>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.label">{{ i18n.ts._abuse._resolver.reporterPattern }}</div>
|
||||
<PrismEditor v-model="value.reporterPattern" placeholder="^(LocalUser|.*@RemoteHost)$" class="_code code" :class="$style.highlight" :highlight="highlighter" :lineNumbers="false" :ignoreTabKey="true" :readonly="!props.editable"/>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.label">{{ i18n.ts._abuse._resolver.reportContentPattern }}</div>
|
||||
<PrismEditor v-model="value.reportContentPattern" placeholder=".*" class="_code code" :class="$style.highlight" :highlight="highlighter" :lineNumbers="false" :ignoreTabKey="true" :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;"><MkDate :time="expirationDate" mode="absolute">{{ expirationDate }}</MkDate></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: Date | 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);
|
||||
} 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>
|
@ -116,6 +116,10 @@ onMounted(() => {
|
||||
const myBg = computedStyle.getPropertyValue('--panel');
|
||||
bgSame = parentBg === myBg;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
toggle,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
:list="id"
|
||||
@focus="focused = true"
|
||||
@focus="onFocus"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
@ -103,6 +103,12 @@ const onKeydown = (ev: KeyboardEvent) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
if (!(props.readonly || props.disabled)) {
|
||||
focused.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
if (type.value === 'number') {
|
||||
|
@ -65,7 +65,7 @@ const opening = ref(false);
|
||||
const changed = ref(false);
|
||||
const invalid = ref(false);
|
||||
const filled = computed(() => v.value !== '' && v.value != null);
|
||||
const inputEl = ref(null);
|
||||
const inputEl = ref<HTMLSelectElement | null>(null);
|
||||
const prefixEl = ref(null);
|
||||
const suffixEl = ref(null);
|
||||
const container = ref(null);
|
||||
@ -124,6 +124,9 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
function show(ev: MouseEvent) {
|
||||
if (inputEl.value && inputEl.value.hasAttribute('disabled')) {
|
||||
return;
|
||||
}
|
||||
focused.value = true;
|
||||
opening.value = true;
|
||||
|
||||
|
@ -5,25 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div>
|
||||
<div v-if="tab === 'list'">
|
||||
<div class="reports">
|
||||
<div class="">
|
||||
<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>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
||||
<option value="resolved">{{ i18n.ts.resolved }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
||||
<MkSelect v-model="targetUserOrigin" :class="$style.targetUserOrigin">
|
||||
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
||||
<MkSelect v-model="reporterOrigin" :class="$style.reporterOrigin">
|
||||
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
||||
<option value="combined">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
@ -47,27 +47,89 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</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.buttonMargin" @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>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import XHeader from './_header_.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.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 { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let reports = $shallowRef<InstanceType<typeof MkPagination>>();
|
||||
let resolverPagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||
let folderComponent = $ref<InstanceType<typeof MkFolder>>();
|
||||
|
||||
let state = $ref('unresolved');
|
||||
let reporterOrigin = $ref('combined');
|
||||
let targetUserOrigin = $ref('combined');
|
||||
let searchUsername = $ref('');
|
||||
let searchHost = $ref('');
|
||||
let tab = $ref('list');
|
||||
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 = {
|
||||
endpoint: 'admin/abuse-user-reports' as const,
|
||||
@ -79,16 +141,108 @@ const pagination = {
|
||||
})),
|
||||
};
|
||||
|
||||
const resolverPagination = {
|
||||
endpoint: 'admin/abuse-report-resolver/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
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 headerTabs = $computed(() => []);
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'list',
|
||||
title: i18n.ts._abuse.list,
|
||||
}, {
|
||||
key: 'resolver',
|
||||
title: i18n.ts._abuse.resolver,
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.input-base {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.buttonMargin {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.state {
|
||||
@extend .input-base;
|
||||
@extend .buttonMargin;
|
||||
}
|
||||
.reporterOrigin {
|
||||
@extend .input-base;
|
||||
}
|
||||
|
||||
.targetUserOrigin {
|
||||
@extend .input-base;
|
||||
@extend .buttonMargin;
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin: 0 auto var(--margin) auto;
|
||||
}
|
||||
|
||||
.resolverList {
|
||||
background: var(--panel);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
</style>
|
||||
|
@ -36,6 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
|
||||
<div class="_panel" style="padding: 16px;">
|
||||
<MkSwitch v-model="doNotSendNotificationEmailsForAbuseReport">
|
||||
<template #label>{{ i18n.ts.doNotSendNotificationEmailsForAbuseReport }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@ -56,6 +62,7 @@ let enableServerMachineStats: boolean = $ref(false);
|
||||
let enableIdenticonGeneration: boolean = $ref(false);
|
||||
let enableChartsForRemoteUser: boolean = $ref(false);
|
||||
let enableChartsForFederatedInstances: boolean = $ref(false);
|
||||
let doNotSendNotificationEmailsForAbuseReport: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
@ -63,6 +70,7 @@ async function init() {
|
||||
enableIdenticonGeneration = meta.enableIdenticonGeneration;
|
||||
enableChartsForRemoteUser = meta.enableChartsForRemoteUser;
|
||||
enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances;
|
||||
doNotSendNotificationEmailsForAbuseReport = meta.doNotSendNotificationEmailsForAbuseReport;
|
||||
}
|
||||
|
||||
function save() {
|
||||
@ -71,6 +79,7 @@ function save() {
|
||||
enableIdenticonGeneration,
|
||||
enableChartsForRemoteUser,
|
||||
enableChartsForFederatedInstances,
|
||||
doNotSendNotificationEmailsForAbuseReport,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
@ -129,6 +129,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</template>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.abuseReports }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="emailToReceiveAbuseReport" type="email">
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #label>{{ i18n.ts.emailToReceiveAbuseReport }}</template>
|
||||
<template #caption>{{ i18n.ts.emailToReceiveAbuseReportCaption }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@ -163,6 +175,7 @@ let name: string | null = $ref(null);
|
||||
let description: string | null = $ref(null);
|
||||
let maintainerName: string | null = $ref(null);
|
||||
let maintainerEmail: string | null = $ref(null);
|
||||
let emailToReceiveAbuseReport: string | null = $ref(null);
|
||||
let pinnedUsers: string = $ref('');
|
||||
let cacheRemoteFiles: boolean = $ref(false);
|
||||
let cacheRemoteSensitiveFiles: boolean = $ref(false);
|
||||
@ -184,6 +197,7 @@ async function init(): Promise<void> {
|
||||
description = meta.description;
|
||||
maintainerName = meta.maintainerName;
|
||||
maintainerEmail = meta.maintainerEmail;
|
||||
emailToReceiveAbuseReport = meta.emailToReceiveAbuseReport;
|
||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
|
||||
@ -206,6 +220,8 @@ function save(): void {
|
||||
description,
|
||||
maintainerName,
|
||||
maintainerEmail,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
emailToReceiveAbuseReport: emailToReceiveAbuseReport || null,
|
||||
pinnedUsers: pinnedUsers.split('\n'),
|
||||
cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles,
|
||||
|
Loading…
Reference in New Issue
Block a user