diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index dd853aa83a..59ce5ee84f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -619,8 +619,11 @@ reportAbuse: "通報" reportAbuseOf: "{name}を通報する" fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" abuseReported: "内容が送信されました。ご報告ありがとうございました。" +reporter: "通報者" reporteeOrigin: "通報先" reporterOrigin: "通報元" +forwardReport: "リモートインスタンスに通報を転送する" +forwardReportIsAnonymous: "リモートインスタンスからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" send: "送信" abuseMarkAsResolved: "対応済みにする" openInNewTab: "新しいタブで開く" diff --git a/packages/backend/migration/1637320813000-forwarded-report.js b/packages/backend/migration/1637320813000-forwarded-report.js new file mode 100644 index 0000000000..4056f7b5f4 --- /dev/null +++ b/packages/backend/migration/1637320813000-forwarded-report.js @@ -0,0 +1,13 @@ +const { QueryRunner } = require('typeorm'); + +module.exports = class forwardedReport1637320813000 { + name = 'forwardedReport1637320813000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "forwarded" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "forwarded"`); + } +}; diff --git a/packages/backend/src/models/entities/abuse-user-report.ts b/packages/backend/src/models/entities/abuse-user-report.ts index 019d613f76..27c1e47fd8 100644 --- a/packages/backend/src/models/entities/abuse-user-report.ts +++ b/packages/backend/src/models/entities/abuse-user-report.ts @@ -51,6 +51,11 @@ export class AbuseUserReport { }) public resolved: boolean; + @Column('boolean', { + default: false + }) + public forwarded: boolean; + @Column('varchar', { length: 2048, }) diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts index 5e267b3c2b..943b65eb64 100644 --- a/packages/backend/src/models/repositories/abuse-user-report.ts +++ b/packages/backend/src/models/repositories/abuse-user-report.ts @@ -27,6 +27,7 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> { assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, { detail: true, }) : null, + forwarded: report.forwarded, }); } diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts new file mode 100644 index 0000000000..60ac496509 --- /dev/null +++ b/packages/backend/src/remote/activitypub/renderer/flag.ts @@ -0,0 +1,15 @@ +import config from '@/config/index'; +import { IObject, IActivity } from '@/remote/activitypub/type'; +import { ILocalUser, IRemoteUser } from '@/models/entities/user'; +import { getInstanceActor } from '@/services/instance-actor'; + +// to anonymise reporters, the reporting actor must be a system user +// object has to be a uri or array of uris +export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => { + return { + type: 'Flag', + actor: `${config.url}/users/${user.id}`, + content, + object, + }; +}; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 807afd2690..ed7b146d03 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -46,6 +46,11 @@ export const meta = { ]), default: 'combined', }, + + forwarded: { + validator: $.optional.bool, + default: false, + }, }, res: { diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 3b47d7d35e..b00457f092 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -1,7 +1,11 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; -import { AbuseUserReports } from '@/models/index'; +import { AbuseUserReports, Users } from '@/models/index'; +import { getInstanceActor } from '@/services/instance-actor'; +import { deliver } from '@/queue/index'; +import { renderActivity } from '@/remote/activitypub/renderer/index'; +import { renderFlag } from '@/remote/activitypub/renderer/flag'; export const meta = { tags: ['admin'], @@ -13,6 +17,12 @@ export const meta = { reportId: { validator: $.type(ID), }, + + forward: { + validator: $.optional.boolean, + required: false, + default: false, + }, }, } as const; @@ -24,8 +34,16 @@ export default define(meta, async (ps, me) => { throw new Error('report not found'); } + if (ps.forward && report.targetUserHost != null) { + const actor = await getInstanceActor(); + const targetUser = await Users.findOne(report.targetUserId); + + deliver(actor, renderActivity(renderFlag(actor, [targetUser.uri], report.comment)), targetUser.inbox); + } + await AbuseUserReports.update(report.id, { resolved: true, assigneeId: me.id, + forwarded: ps.forward && report.targetUserHost != null, }); }); diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue new file mode 100644 index 0000000000..b67cef209b --- /dev/null +++ b/packages/client/src/components/abuse-report.vue @@ -0,0 +1,102 @@ +<template> +<div class="bcekxzvu _card _gap"> + <div class="_content target"> + <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> + <MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId"> + <MkUserName class="name" :user="report.targetUser"/> + <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> + </MkA> + </div> + <div class="_content"> + <div> + <Mfm :text="report.comment"/> + </div> + <hr/> + <div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div> + <div v-if="report.assignee"> + {{ $ts.moderator }}: + <MkAcct :user="report.assignee"/> + </div> + <div><MkTime :time="report.createdAt"/></div> + </div> + <div class="_footer"> + <MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved"> + {{ $ts.forwardReport }} + <template #caption>{{ $ts.forwardReportIsAnonymous }}</template> + </MkSwitch> + <MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; + +import MkButton from '@/components/ui/button.vue'; +import MkSwitch from '@/components/form/switch.vue'; +import { acct, userPage } from '@/filters/user'; +import * as os from '@/os'; + +export default defineComponent({ + components: { + MkButton, + MkSwitch, + }, + + emits: ['resolved'], + + props: { + report: { + type: Object, + required: true, + } + } + + data() { + return { + forward: this.report.forwarded, + }; + } + + methods: { + acct, + userPage, + + resolve() { + os.apiWithDialog('admin/resolve-abuse-user-report', { + forward: this.forward, + reportId: this.report.id, + }).then(() => { + this.$emit('resolved', this.report.id); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.bcekxzvu { + > .target { + display: flex; + width: 100%; + box-sizing: border-box; + text-align: left; + align-items: center; + + > .avatar { + width: 42px; + height: 42px; + } + + > .info { + margin-left: 0.3em; + padding: 0 8px; + flex: 1; + + > .name { + font-weight: bold; + } + } + } +} +</style> diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index 31cdef492a..92f93797ce 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -34,27 +34,7 @@ --> <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> - <div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap"> - <div class="_content target"> - <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/> - <div class="info"> - <MkUserName class="name" :user="report.targetUser"/> - <div class="acct">@{{ acct(report.targetUser) }}</div> - </div> - </div> - <div class="_content"> - <div> - <Mfm :text="report.comment"/> - </div> - <hr> - <div>Reporter: <MkAcct :user="report.reporter"/></div> - <div><MkTime :time="report.createdAt"/></div> - </div> - <div class="_footer"> - <div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div> - <MkButton v-if="!report.resolved" primary @click="resolve(report)">{{ $ts.abuseMarkAsResolved }}</MkButton> - </div> - </div> + <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> </MkPagination> </div> </div> @@ -64,20 +44,19 @@ <script lang="ts"> import { computed, defineComponent } from 'vue'; -import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; import MkPagination from '@/components/ui/pagination.vue'; -import { acct } from '@/filters/user'; +import XAbuseReport from '@/components/abuse-report.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; export default defineComponent({ components: { - MkButton, MkInput, MkSelect, MkPagination, + XAbuseReport, }, emits: ['info'], @@ -107,14 +86,8 @@ export default defineComponent({ }, methods: { - acct, - - resolve(report) { - os.apiWithDialog('admin/resolve-abuse-user-report', { - reportId: report.id, - }).then(() => { - this.$refs.reports.removeItem(item => item.id === report.id); - }); + resolved(reportId) { + this.$refs.reports.removeItem(item => item.id === reportId); }, } }); @@ -124,29 +97,4 @@ export default defineComponent({ .lcixvhis { margin: var(--margin); } - -.bcekxzvu { - > .target { - display: flex; - width: 100%; - box-sizing: border-box; - text-align: left; - align-items: center; - - > .avatar { - width: 42px; - height: 42px; - } - - > .info { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - > .name { - font-weight: bold; - } - } - } -} </style> diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 61120d53ba..3634f39632 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -252,7 +252,7 @@ export function getNoteMenu(props: { icon: 'fas fa-exclamation-circle', text: i18n.locale.reportAbuse, action: () => { - const u = `${url}/notes/${appearNote.id}`; + const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; os.popup(import('@/components/abuse-report-window.vue'), { user: appearNote.user, initialComment: `Note: ${u}\n-----\n`