Resolve #6087
This commit is contained in:
parent
5762e2d9ba
commit
87f61e714a
18 changed files with 461 additions and 23 deletions
|
@ -586,6 +586,13 @@ setMultipleBySeparatingWithSpace: "スペースで区切って複数設定でき
|
||||||
fileIdOrUrl: "ファイルIDまたはURL"
|
fileIdOrUrl: "ファイルIDまたはURL"
|
||||||
chatOpenBehavior: "チャットを開くときの動作"
|
chatOpenBehavior: "チャットを開くときの動作"
|
||||||
sample: "サンプル"
|
sample: "サンプル"
|
||||||
|
abuseReports: "通報"
|
||||||
|
reportAbuse: "通報"
|
||||||
|
reportAbuseOf: "{name}を通報する"
|
||||||
|
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
|
||||||
|
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
|
||||||
|
send: "送信"
|
||||||
|
abuseMarkAsResolved: "対応済みにする"
|
||||||
|
|
||||||
_serverDisconnectedBehavior:
|
_serverDisconnectedBehavior:
|
||||||
reload: "自動でリロード"
|
reload: "自動でリロード"
|
||||||
|
|
38
migration/1603094348345-refine-abuse-user-report.ts
Normal file
38
migration/1603094348345-refine-abuse-user-report.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class refineAbuseUserReport1603094348345 implements MigrationInterface {
|
||||||
|
name = 'refineAbuseUserReport1603094348345'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
migration/1603095701770-refine-abuse-user-report2.ts
Normal file
20
migration/1603095701770-refine-abuse-user-report2.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class refineAbuseUserReport21603095701770 implements MigrationInterface {
|
||||||
|
name = 'refineAbuseUserReport21603095701770'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
85
src/client/components/abuse-report-window.vue
Normal file
85
src/client/components/abuse-report-window.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
|
||||||
|
<template #header>
|
||||||
|
<Fa :icon="faExclamationCircle" style="margin-right: 0.5em;"/>
|
||||||
|
<i18n-t keypath="reportAbuseOf" tag="span">
|
||||||
|
<template #name>
|
||||||
|
<b><MkAcct :user="user"/></b>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
<div class="dpvffvvy">
|
||||||
|
<div class="_section">
|
||||||
|
<div class="_content">
|
||||||
|
<MkTextarea v-model:value="comment">
|
||||||
|
<span>{{ $t('details') }}</span>
|
||||||
|
<template #desc>{{ $t('fillAbuseReportDescription') }}</template>
|
||||||
|
</MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_section">
|
||||||
|
<div class="_content">
|
||||||
|
<MkButton @click="send" primary full :disabled="comment.length === 0">{{ $t('send') }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</XWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, markRaw } from 'vue';
|
||||||
|
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import XWindow from '@/components/ui/window.vue';
|
||||||
|
import MkTextarea from '@/components/ui/textarea.vue';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XWindow,
|
||||||
|
MkTextarea,
|
||||||
|
MkButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
initialComment: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['closed'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
comment: this.initialComment || '',
|
||||||
|
faExclamationCircle,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
send() {
|
||||||
|
os.apiWithDialog('users/report-abuse', {
|
||||||
|
userId: this.user.id,
|
||||||
|
comment: this.comment,
|
||||||
|
}, undefined, res => {
|
||||||
|
os.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('abuseReported')
|
||||||
|
});
|
||||||
|
this.$refs.window.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dpvffvvy {
|
||||||
|
--section-padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -101,7 +101,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
|
import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
|
||||||
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
|
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { parse } from '../../mfm/parse';
|
import { parse } from '../../mfm/parse';
|
||||||
import { sum, unique } from '../../prelude/array';
|
import { sum, unique } from '../../prelude/array';
|
||||||
|
@ -637,6 +637,21 @@ export default defineComponent({
|
||||||
}]
|
}]
|
||||||
: []
|
: []
|
||||||
),
|
),
|
||||||
|
...(this.appearNote.userId != this.$store.state.i.id ? [
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
icon: faExclamationCircle,
|
||||||
|
text: this.$t('reportAbuse'),
|
||||||
|
action: () => {
|
||||||
|
const u = `${url}/notes/${this.appearNote.id}`;
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
|
||||||
|
user: this.appearNote.user,
|
||||||
|
initialComment: `Note: ${u}\n-----\n`
|
||||||
|
}, {}, 'closed');
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
: []
|
||||||
|
),
|
||||||
...(this.appearNote.userId == this.$store.state.i.id || this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
|
...(this.appearNote.userId == this.$store.state.i.id || this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
|
||||||
null,
|
null,
|
||||||
this.appearNote.userId == this.$store.state.i.id ? {
|
this.appearNote.userId == this.$store.state.i.id ? {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<button class="_button" @click="expand" v-tooltip="$t('showInPage')"><Fa :icon="faExpandAlt"/></button>
|
<button class="_button" @click="expand" v-tooltip="$t('showInPage')"><Fa :icon="faExpandAlt"/></button>
|
||||||
<button class="_button" @click="popout" v-tooltip="$t('popout')"><Fa :icon="faExternalLinkAlt"/></button>
|
<button class="_button" @click="popout" v-tooltip="$t('popout')"><Fa :icon="faExternalLinkAlt"/></button>
|
||||||
</template>
|
</template>
|
||||||
<div style="min-height: 100%; background: var(--bg);">
|
<div class="yrolvcoq" style="min-height: 100%; background: var(--bg);">
|
||||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||||
</div>
|
</div>
|
||||||
</XWindow>
|
</XWindow>
|
||||||
|
@ -84,3 +84,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.yrolvcoq {
|
||||||
|
--section-padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram, faStream } from '@fortawesome/free-solid-svg-icons';
|
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram, faStream, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
|
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { host, instanceName } from '@/config';
|
import { host, instanceName } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
import { search } from '@/scripts/search';
|
||||||
|
@ -217,6 +217,11 @@ export default defineComponent({
|
||||||
text: this.$t('announcements'),
|
text: this.$t('announcements'),
|
||||||
to: '/instance/announcements',
|
to: '/instance/announcements',
|
||||||
icon: faBroadcastTower,
|
icon: faBroadcastTower,
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: this.$t('abuseReports'),
|
||||||
|
to: '/instance/abuses',
|
||||||
|
icon: faExclamationCircle,
|
||||||
}, {
|
}, {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: this.$t('logs'),
|
text: this.$t('logs'),
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</span>
|
</span>
|
||||||
<slot name="buttons"></slot>
|
<slot name="buttons">
|
||||||
|
<button class="_button" style="pointer-events: none;"></button>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="body" v-if="padding">
|
<div class="body" v-if="padding">
|
||||||
<div class="_section">
|
<div class="_section">
|
||||||
|
@ -371,8 +373,6 @@ export default defineComponent({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
--section-padding: 16px;
|
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
$height: 50px;
|
$height: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -380,7 +380,6 @@ export default defineComponent({
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
box-shadow: 0px 1px var(--divider);
|
box-shadow: 0px 1px var(--divider);
|
||||||
cursor: move;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
height: $height;
|
height: $height;
|
||||||
|
|
||||||
|
@ -400,6 +399,8 @@ export default defineComponent({
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
cursor: move;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Stream from '@/scripts/stream';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { apiUrl } from '@/config';
|
import { apiUrl } from '@/config';
|
||||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
||||||
|
import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
|
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
|
||||||
|
@ -73,7 +74,7 @@ export function apiWithDialog(
|
||||||
promiseDialog(promise, onSuccess, onFailure ? onFailure : (e) => {
|
promiseDialog(promise, onSuccess, onFailure ? onFailure : (e) => {
|
||||||
dialog({
|
dialog({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: e.message + '\n' + (e as any).id,
|
text: e.message + '<br>' + (e as any).id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -111,7 +112,8 @@ export function promiseDialog<T extends Promise<any>>(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
|
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
|
||||||
|
popup(MkWaitingDialog, {
|
||||||
success: success,
|
success: success,
|
||||||
showing: showing,
|
showing: showing,
|
||||||
text: text,
|
text: text,
|
||||||
|
|
163
src/client/pages/instance/abuses.vue
Normal file
163
src/client/pages/instance/abuses.vue
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<div class="_section reports">
|
||||||
|
<div class="_content">
|
||||||
|
<div class="inputs" style="display: flex;">
|
||||||
|
<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
|
||||||
|
<template #label>{{ $t('state') }}</template>
|
||||||
|
<option value="all">{{ $t('all') }}</option>
|
||||||
|
<option value="unresolved">{{ $t('unresolved') }}</option>
|
||||||
|
<option value="resolved">{{ $t('resolved') }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSelect v-model:value="targetUserOrigin" style="margin: 0; flex: 1;">
|
||||||
|
<template #label>{{ $t('targetUserOrigin') }}</template>
|
||||||
|
<option value="combined">{{ $t('all') }}</option>
|
||||||
|
<option value="local">{{ $t('local') }}</option>
|
||||||
|
<option value="remote">{{ $t('remote') }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSelect v-model:value="reporterOrigin" style="margin: 0; flex: 1;">
|
||||||
|
<template #label>{{ $t('reporterOrigin') }}</template>
|
||||||
|
<option value="combined">{{ $t('all') }}</option>
|
||||||
|
<option value="local">{{ $t('local') }}</option>
|
||||||
|
<option value="remote">{{ $t('remote') }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
</div>
|
||||||
|
<!-- TODO
|
||||||
|
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||||
|
<MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.reports.reload()">
|
||||||
|
<span>{{ $t('username') }}</span>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model:value="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
|
||||||
|
<span>{{ $t('host') }}</span>
|
||||||
|
</MkInput>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<MkPagination :pagination="pagination" #default="{items}" ref="reports" :auto-margin="false" style="margin-top: var(--margin);">
|
||||||
|
<div class="bcekxzvu _card _vMargin" v-for="report in items" :key="report.id">
|
||||||
|
<div class="_content target">
|
||||||
|
<MkAvatar class="avatar" :user="report.targetUser"/>
|
||||||
|
<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 @click="resolve(report)" primary v-if="!report.resolved">{{ $t('abuseMarkAsResolved') }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { faPlus, faUsers, faSearch, faBookmark, faMicrophoneSlash, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { faSnowflake, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import parseAcct from '../../../misc/acct/parse';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
import MkInput from '@/components/ui/input.vue';
|
||||||
|
import MkSelect from '@/components/ui/select.vue';
|
||||||
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
|
import { acct } from '../../filters/user';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
MkButton,
|
||||||
|
MkInput,
|
||||||
|
MkSelect,
|
||||||
|
MkPagination,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
INFO: {
|
||||||
|
header: [{
|
||||||
|
title: this.$t('abuseReports'),
|
||||||
|
icon: faExclamationCircle
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
searchUsername: '',
|
||||||
|
searchHost: '',
|
||||||
|
state: 'unresolved',
|
||||||
|
reporterOrigin: 'combined',
|
||||||
|
targetUserOrigin: 'combined',
|
||||||
|
pagination: {
|
||||||
|
endpoint: 'admin/abuse-user-reports',
|
||||||
|
limit: 10,
|
||||||
|
params: () => ({
|
||||||
|
state: this.state,
|
||||||
|
reporterOrigin: this.reporterOrigin,
|
||||||
|
targetUserOrigin: this.targetUserOrigin,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
faPlus, faUsers, faSearch, faBookmark, farBookmark, faMicrophoneSlash, faSnowflake
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
state() {
|
||||||
|
this.$refs.reports.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
reporterOrigin() {
|
||||||
|
this.$refs.reports.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
targetUserOrigin() {
|
||||||
|
this.$refs.reports.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
acct,
|
||||||
|
|
||||||
|
resolve(report) {
|
||||||
|
os.apiWithDialog('admin/resolve-abuse-user-report', {
|
||||||
|
reportId: report.id,
|
||||||
|
}).then(() => {
|
||||||
|
this.$refs.reports.removeItem(item => item.id === 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>
|
|
@ -86,6 +86,7 @@ export const router = createRouter({
|
||||||
{ path: '/instance/federation', component: page('instance/federation') },
|
{ path: '/instance/federation', component: page('instance/federation') },
|
||||||
{ path: '/instance/relays', component: page('instance/relays') },
|
{ path: '/instance/relays', component: page('instance/relays') },
|
||||||
{ path: '/instance/announcements', component: page('instance/announcements') },
|
{ path: '/instance/announcements', component: page('instance/announcements') },
|
||||||
|
{ path: '/instance/abuses', component: page('instance/abuses') },
|
||||||
{ path: '/notes/:note', name: 'note', component: page('note') },
|
{ path: '/notes/:note', name: 'note', component: page('note') },
|
||||||
{ path: '/tags/:tag', component: page('tag') },
|
{ path: '/tags/:tag', component: page('tag') },
|
||||||
{ path: '/auth/:token', component: page('auth') },
|
{ path: '/auth/:token', component: page('auth') },
|
||||||
|
|
|
@ -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 { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
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<boolean> {
|
async function getConfirmed(text: string): Promise<boolean> {
|
||||||
const confirm = await os.dialog({
|
const confirm = await os.dialog({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -157,6 +163,12 @@ export function getUserMenu(user) {
|
||||||
action: toggleBlock
|
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)) {
|
if (store.getters.isSignedIn && (store.state.i.isAdmin || store.state.i.isModerator)) {
|
||||||
menu = menu.concat([null, {
|
menu = menu.concat([null, {
|
||||||
icon: faMicrophoneSlash,
|
icon: faMicrophoneSlash,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { User } from './user';
|
||||||
import { id } from '../id';
|
import { id } from '../id';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['userId', 'reporterId'], { unique: true })
|
|
||||||
export class AbuseUserReport {
|
export class AbuseUserReport {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
@ -16,13 +15,13 @@ export class AbuseUserReport {
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column(id())
|
@Column(id())
|
||||||
public userId: User['id'];
|
public targetUserId: User['id'];
|
||||||
|
|
||||||
@ManyToOne(type => User, {
|
@ManyToOne(type => User, {
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public user: User | null;
|
public targetUser: User | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column(id())
|
@Column(id())
|
||||||
|
@ -34,8 +33,42 @@ export class AbuseUserReport {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public reporter: User | null;
|
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', {
|
@Column('varchar', {
|
||||||
length: 512,
|
length: 2048,
|
||||||
})
|
})
|
||||||
public comment: string;
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,19 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
|
||||||
id: report.id,
|
id: report.id,
|
||||||
createdAt: report.createdAt,
|
createdAt: report.createdAt,
|
||||||
comment: report.comment,
|
comment: report.comment,
|
||||||
|
resolved: report.resolved,
|
||||||
reporterId: report.reporterId,
|
reporterId: report.reporterId,
|
||||||
userId: report.userId,
|
targetUserId: report.targetUserId,
|
||||||
|
assigneeId: report.assigneeId,
|
||||||
reporter: Users.pack(report.reporter || report.reporterId, null, {
|
reporter: Users.pack(report.reporter || report.reporterId, null, {
|
||||||
detail: true
|
detail: true
|
||||||
}),
|
}),
|
||||||
user: Users.pack(report.user || report.userId, null, {
|
targetUser: Users.pack(report.targetUser || report.targetUserId, null, {
|
||||||
detail: true
|
detail: true
|
||||||
}),
|
}),
|
||||||
|
assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, {
|
||||||
|
detail: true
|
||||||
|
}) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,10 @@ export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
|
||||||
await AbuseUserReports.insert({
|
await AbuseUserReports.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: users[0].id,
|
targetUserId: users[0].id,
|
||||||
|
targetUserHost: users[0].host,
|
||||||
reporterId: actor.id,
|
reporterId: actor.id,
|
||||||
|
reporterHost: actor.host,
|
||||||
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`
|
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,50 @@ export const meta = {
|
||||||
untilId: {
|
untilId: {
|
||||||
validator: $.optional.type(ID),
|
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) => {
|
export default define(meta, async (ps) => {
|
||||||
const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
|
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();
|
const reports = await query.take(ps.limit!).getMany();
|
||||||
|
|
||||||
return await AbuseUserReports.packMany(reports);
|
return await AbuseUserReports.packMany(reports);
|
||||||
|
|
|
@ -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);
|
const report = await AbuseUserReports.findOne(ps.reportId);
|
||||||
|
|
||||||
if (report == null) {
|
if (report == null) {
|
||||||
throw new Error('report not found');
|
throw new Error('report not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await AbuseUserReports.delete(report.id);
|
await AbuseUserReports.update(report.id, {
|
||||||
|
resolved: true,
|
||||||
|
assigneeId: me.id,
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -26,7 +26,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
comment: {
|
comment: {
|
||||||
validator: $.str.range(1, 3000),
|
validator: $.str.range(1, 2048),
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '迷惑行為の詳細'
|
'ja-JP': '迷惑行為の詳細'
|
||||||
}
|
}
|
||||||
|
@ -72,9 +72,11 @@ export default define(meta, async (ps, me) => {
|
||||||
const report = await AbuseUserReports.save({
|
const report = await AbuseUserReports.save({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: user.id,
|
targetUserId: user.id,
|
||||||
|
targetUserHost: user.host,
|
||||||
reporterId: me.id,
|
reporterId: me.id,
|
||||||
comment: ps.comment
|
reporterHost: null,
|
||||||
|
comment: ps.comment,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish event to moderators
|
// Publish event to moderators
|
||||||
|
@ -90,7 +92,7 @@ export default define(meta, async (ps, me) => {
|
||||||
for (const moderator of moderators) {
|
for (const moderator of moderators) {
|
||||||
publishAdminStream(moderator.id, 'newAbuseUserReport', {
|
publishAdminStream(moderator.id, 'newAbuseUserReport', {
|
||||||
id: report.id,
|
id: report.id,
|
||||||
userId: report.userId,
|
targetUserId: report.targetUserId,
|
||||||
reporterId: report.reporterId,
|
reporterId: report.reporterId,
|
||||||
comment: report.comment
|
comment: report.comment
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue