From 87f61e714ad3b17856a6a5ac66051707badb3bd0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 19 Oct 2020 19:29:04 +0900 Subject: [PATCH] Resolve #6087 --- locales/ja-JP.yml | 7 + .../1603094348345-refine-abuse-user-report.ts | 38 ++++ ...1603095701770-refine-abuse-user-report2.ts | 20 +++ src/client/components/abuse-report-window.vue | 85 +++++++++ src/client/components/note.vue | 17 +- src/client/components/page-window.vue | 8 +- src/client/components/sidebar.vue | 7 +- src/client/components/ui/window.vue | 9 +- src/client/os.ts | 6 +- src/client/pages/instance/abuses.vue | 163 ++++++++++++++++++ src/client/router.ts | 1 + src/client/scripts/get-user-menu.ts | 14 +- src/models/entities/abuse-user-report.ts | 41 ++++- src/models/repositories/abuse-user-report.ts | 9 +- src/remote/activitypub/kernel/flag/index.ts | 4 +- .../api/endpoints/admin/abuse-user-reports.ts | 38 ++++ ...report.ts => resolve-abuse-user-report.ts} | 7 +- .../api/endpoints/users/report-abuse.ts | 10 +- 18 files changed, 461 insertions(+), 23 deletions(-) create mode 100644 migration/1603094348345-refine-abuse-user-report.ts create mode 100644 migration/1603095701770-refine-abuse-user-report2.ts create mode 100644 src/client/components/abuse-report-window.vue create mode 100644 src/client/pages/instance/abuses.vue rename src/server/api/endpoints/admin/{remove-abuse-user-report.ts => resolve-abuse-user-report.ts} (77%) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 83c7add3b2..bccb82e51b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -586,6 +586,13 @@ setMultipleBySeparatingWithSpace: "スペースで区切って複数設定でき fileIdOrUrl: "ファイルIDまたはURL" chatOpenBehavior: "チャットを開くときの動作" sample: "サンプル" +abuseReports: "通報" +reportAbuse: "通報" +reportAbuseOf: "{name}を通報する" +fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" +abuseReported: "内容が送信されました。ご報告ありがとうございました。" +send: "送信" +abuseMarkAsResolved: "対応済みにする" _serverDisconnectedBehavior: reload: "自動でリロード" diff --git a/migration/1603094348345-refine-abuse-user-report.ts b/migration/1603094348345-refine-abuse-user-report.ts new file mode 100644 index 0000000000..2c5c4b39c5 --- /dev/null +++ b/migration/1603094348345-refine-abuse-user-report.ts @@ -0,0 +1,38 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class refineAbuseUserReport1603094348345 implements MigrationInterface { + name = 'refineAbuseUserReport1603094348345' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`); + await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`); + await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "targetUserId" character varying(32) NOT NULL`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "assigneeId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolved" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(2048) NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a" ON "abuse_user_report" ("resolved") `); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de" FOREIGN KEY ("assigneeId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`); + await queryRunner.query(`DROP INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a"`); + await queryRunner.query(`DROP INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(512) NOT NULL`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolved"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "assigneeId"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "targetUserId"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "userId" character varying(32) NOT NULL`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `); + await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + +} diff --git a/migration/1603095701770-refine-abuse-user-report2.ts b/migration/1603095701770-refine-abuse-user-report2.ts new file mode 100644 index 0000000000..18e0c05ac2 --- /dev/null +++ b/migration/1603095701770-refine-abuse-user-report2.ts @@ -0,0 +1,20 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class refineAbuseUserReport21603095701770 implements MigrationInterface { + name = 'refineAbuseUserReport21603095701770' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "targetUserHost" character varying(128)`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "reporterHost" character varying(128)`); + await queryRunner.query(`CREATE INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd" ON "abuse_user_report" ("targetUserHost") `); + await queryRunner.query(`CREATE INDEX "IDX_f8d8b93740ad12c4ce8213a199" ON "abuse_user_report" ("reporterHost") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_f8d8b93740ad12c4ce8213a199"`); + await queryRunner.query(`DROP INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "reporterHost"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "targetUserHost"`); + } + +} diff --git a/src/client/components/abuse-report-window.vue b/src/client/components/abuse-report-window.vue new file mode 100644 index 0000000000..1d87cb1802 --- /dev/null +++ b/src/client/components/abuse-report-window.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/client/components/note.vue b/src/client/components/note.vue index cb364a04c9..85bdb9c6fb 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -101,7 +101,7 @@ + + diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue index 7548b136ea..383378241b 100644 --- a/src/client/components/sidebar.vue +++ b/src/client/components/sidebar.vue @@ -46,7 +46,7 @@ + + diff --git a/src/client/router.ts b/src/client/router.ts index fc67f6ecfd..c9c7a32835 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -86,6 +86,7 @@ export const router = createRouter({ { path: '/instance/federation', component: page('instance/federation') }, { path: '/instance/relays', component: page('instance/relays') }, { path: '/instance/announcements', component: page('instance/announcements') }, + { path: '/instance/abuses', component: page('instance/abuses') }, { path: '/notes/:note', name: 'note', component: page('note') }, { path: '/tags/:tag', component: page('tag') }, { path: '/auth/:token', component: page('auth') }, diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts index 63c3ae43b6..cace2e1425 100644 --- a/src/client/scripts/get-user-menu.ts +++ b/src/client/scripts/get-user-menu.ts @@ -1,4 +1,4 @@ -import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug } from '@fortawesome/free-solid-svg-icons'; +import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; @@ -102,6 +102,12 @@ export function getUserMenu(user) { }); } + async function reportAbuse() { + os.popup(await import('@/components/abuse-report-window.vue'), { + user: user, + }, {}, 'closed'); + } + async function getConfirmed(text: string): Promise { const confirm = await os.dialog({ type: 'warning', @@ -157,6 +163,12 @@ export function getUserMenu(user) { action: toggleBlock }]); + menu = menu.concat([null, { + icon: faExclamationCircle, + text: i18n.global.t('reportAbuse'), + action: reportAbuse + }]); + if (store.getters.isSignedIn && (store.state.i.isAdmin || store.state.i.isModerator)) { menu = menu.concat([null, { icon: faMicrophoneSlash, diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts index 43ab56023a..c0cff139f6 100644 --- a/src/models/entities/abuse-user-report.ts +++ b/src/models/entities/abuse-user-report.ts @@ -3,7 +3,6 @@ import { User } from './user'; import { id } from '../id'; @Entity() -@Index(['userId', 'reporterId'], { unique: true }) export class AbuseUserReport { @PrimaryColumn(id()) public id: string; @@ -16,13 +15,13 @@ export class AbuseUserReport { @Index() @Column(id()) - public userId: User['id']; + public targetUserId: User['id']; @ManyToOne(type => User, { onDelete: 'CASCADE' }) @JoinColumn() - public user: User | null; + public targetUser: User | null; @Index() @Column(id()) @@ -34,8 +33,42 @@ export class AbuseUserReport { @JoinColumn() public reporter: User | null; + @Column({ + ...id(), + nullable: true + }) + public assigneeId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public assignee: User | null; + + @Index() + @Column('boolean', { + default: false + }) + public resolved: boolean; + @Column('varchar', { - length: 512, + length: 2048, }) public comment: string; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public targetUserHost: string | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public reporterHost: string | null; + //#endregion } diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts index bff64c770c..dbdaa5ee15 100644 --- a/src/models/repositories/abuse-user-report.ts +++ b/src/models/repositories/abuse-user-report.ts @@ -15,14 +15,19 @@ export class AbuseUserReportRepository extends Repository { id: report.id, createdAt: report.createdAt, comment: report.comment, + resolved: report.resolved, reporterId: report.reporterId, - userId: report.userId, + targetUserId: report.targetUserId, + assigneeId: report.assigneeId, reporter: Users.pack(report.reporter || report.reporterId, null, { detail: true }), - user: Users.pack(report.user || report.userId, null, { + targetUser: Users.pack(report.targetUser || report.targetUserId, null, { detail: true }), + assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, { + detail: true + }) : null, }); } diff --git a/src/remote/activitypub/kernel/flag/index.ts b/src/remote/activitypub/kernel/flag/index.ts index 9b3065b112..46ea789b4b 100644 --- a/src/remote/activitypub/kernel/flag/index.ts +++ b/src/remote/activitypub/kernel/flag/index.ts @@ -19,8 +19,10 @@ export default async (actor: IRemoteUser, activity: IFlag): Promise => { await AbuseUserReports.insert({ id: genId(), createdAt: new Date(), - userId: users[0].id, + targetUserId: users[0].id, + targetUserHost: users[0].host, reporterId: actor.id, + reporterHost: actor.host, comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}` }); diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts index d5a52184d1..6a7f380e16 100644 --- a/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -23,12 +23,50 @@ export const meta = { untilId: { validator: $.optional.type(ID), }, + + state: { + validator: $.optional.nullable.str, + default: null, + }, + + reporterOrigin: { + validator: $.optional.str.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'combined' + }, + + targetUserOrigin: { + validator: $.optional.str.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'combined' + }, } }; export default define(meta, async (ps) => { const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId); + switch (ps.state) { + case 'resolved': query.andWhere('report.resolved = TRUE'); break; + case 'unresolved': query.andWhere('report.resolved = FALSE'); break; + } + + switch (ps.reporterOrigin) { + case 'local': query.andWhere('report.reporterHost IS NULL'); break; + case 'remote': query.andWhere('report.reporterHost IS NOT NULL'); break; + } + + switch (ps.targetUserOrigin) { + case 'local': query.andWhere('report.targetUserHost IS NULL'); break; + case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break; + } + const reports = await query.take(ps.limit!).getMany(); return await AbuseUserReports.packMany(reports); diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/resolve-abuse-user-report.ts similarity index 77% rename from src/server/api/endpoints/admin/remove-abuse-user-report.ts rename to src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 150de5f5d4..0a62b5f365 100644 --- a/src/server/api/endpoints/admin/remove-abuse-user-report.ts +++ b/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -16,12 +16,15 @@ export const meta = { } }; -export default define(meta, async (ps) => { +export default define(meta, async (ps, me) => { const report = await AbuseUserReports.findOne(ps.reportId); if (report == null) { throw new Error('report not found'); } - await AbuseUserReports.delete(report.id); + await AbuseUserReports.update(report.id, { + resolved: true, + assigneeId: me.id, + }); }); diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts index a9b5543f3c..eaa4cd6258 100644 --- a/src/server/api/endpoints/users/report-abuse.ts +++ b/src/server/api/endpoints/users/report-abuse.ts @@ -26,7 +26,7 @@ export const meta = { }, comment: { - validator: $.str.range(1, 3000), + validator: $.str.range(1, 2048), desc: { 'ja-JP': '迷惑行為の詳細' } @@ -72,9 +72,11 @@ export default define(meta, async (ps, me) => { const report = await AbuseUserReports.save({ id: genId(), createdAt: new Date(), - userId: user.id, + targetUserId: user.id, + targetUserHost: user.host, reporterId: me.id, - comment: ps.comment + reporterHost: null, + comment: ps.comment, }); // Publish event to moderators @@ -90,7 +92,7 @@ export default define(meta, async (ps, me) => { for (const moderator of moderators) { publishAdminStream(moderator.id, 'newAbuseUserReport', { id: report.id, - userId: report.userId, + targetUserId: report.targetUserId, reporterId: report.reporterId, comment: report.comment });