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…
	
	Add table
		Add a link
		
	
		Reference in a new issue