enhance: Forward report (#8001)
* implement sending AP Flag object Optionally allow a user to select to forward a report about a remote user to the other instance. This is added in a backwards-compatible way. * add locale string * forward report only for moderators * add switch to moderator UI to forward report * fix report note url * return forwarded status from API apparently forgot to carry this over from my testing environment * object in Flag activity has to be an array For correct interoperability with Pleroma the "object" property of the Flag activity has to be an array. This array will in the future also hold the link to respective notes, so it makes sense to correct this on our side. * Update get-note-menu.ts Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		
							parent
							
								
									e2d2a4e2e4
								
							
						
					
					
						commit
						cbb7e95d82
					
				
					 10 changed files with 169 additions and 59 deletions
				
			
		|  | @ -619,8 +619,11 @@ reportAbuse: "通報" | ||||||
| reportAbuseOf: "{name}を通報する" | reportAbuseOf: "{name}を通報する" | ||||||
| fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" | fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" | ||||||
| abuseReported: "内容が送信されました。ご報告ありがとうございました。" | abuseReported: "内容が送信されました。ご報告ありがとうございました。" | ||||||
|  | reporter: "通報者" | ||||||
| reporteeOrigin: "通報先" | reporteeOrigin: "通報先" | ||||||
| reporterOrigin: "通報元" | reporterOrigin: "通報元" | ||||||
|  | forwardReport: "リモートインスタンスに通報を転送する" | ||||||
|  | forwardReportIsAnonymous: "リモートインスタンスからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" | ||||||
| send: "送信" | send: "送信" | ||||||
| abuseMarkAsResolved: "対応済みにする" | abuseMarkAsResolved: "対応済みにする" | ||||||
| openInNewTab: "新しいタブで開く" | openInNewTab: "新しいタブで開く" | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								packages/backend/migration/1637320813000-forwarded-report.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/migration/1637320813000-forwarded-report.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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"`); | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | @ -51,6 +51,11 @@ export class AbuseUserReport { | ||||||
| 	}) | 	}) | ||||||
| 	public resolved: boolean; | 	public resolved: boolean; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('boolean', { | ||||||
|  | 		default: false | ||||||
|  | 	}) | ||||||
|  | 	public forwarded: boolean; | ||||||
|  | 
 | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 2048, | 		length: 2048, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> { | ||||||
| 			assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, { | 			assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, { | ||||||
| 				detail: true, | 				detail: true, | ||||||
| 			}) : null, | 			}) : null, | ||||||
|  | 			forwarded: report.forwarded, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								packages/backend/src/remote/activitypub/renderer/flag.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/backend/src/remote/activitypub/renderer/flag.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
|  | @ -46,6 +46,11 @@ export const meta = { | ||||||
| 			]), | 			]), | ||||||
| 			default: 'combined', | 			default: 'combined', | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		forwarded: { | ||||||
|  | 			validator: $.optional.bool, | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	res: { | 	res: { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,11 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import { ID } from '@/misc/cafy-id'; | import { ID } from '@/misc/cafy-id'; | ||||||
| import define from '../../define'; | 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 = { | export const meta = { | ||||||
| 	tags: ['admin'], | 	tags: ['admin'], | ||||||
|  | @ -13,6 +17,12 @@ export const meta = { | ||||||
| 		reportId: { | 		reportId: { | ||||||
| 			validator: $.type(ID), | 			validator: $.type(ID), | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		forward: { | ||||||
|  | 			validator: $.optional.boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
|  | @ -24,8 +34,16 @@ export default define(meta, async (ps, me) => { | ||||||
| 		throw new Error('report not found'); | 		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, { | 	await AbuseUserReports.update(report.id, { | ||||||
| 		resolved: true, | 		resolved: true, | ||||||
| 		assigneeId: me.id, | 		assigneeId: me.id, | ||||||
|  | 		forwarded: ps.forward && report.targetUserHost != null, | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										102
									
								
								packages/client/src/components/abuse-report.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								packages/client/src/components/abuse-report.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  | @ -34,27 +34,7 @@ | ||||||
| 			--> | 			--> | ||||||
| 
 | 
 | ||||||
| 			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> | 			<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"> | 				<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> | ||||||
| 					<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> |  | ||||||
| 			</MkPagination> | 			</MkPagination> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -64,20 +44,19 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import MkButton from '@/components/ui/button.vue'; |  | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.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 os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import * as symbols from '@/symbols'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, |  | ||||||
| 		MkInput, | 		MkInput, | ||||||
| 		MkSelect, | 		MkSelect, | ||||||
| 		MkPagination, | 		MkPagination, | ||||||
|  | 		XAbuseReport, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  | @ -107,14 +86,8 @@ export default defineComponent({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		acct, | 		resolved(reportId) { | ||||||
| 
 | 			this.$refs.reports.removeItem(item => item.id === reportId); | ||||||
| 		resolve(report) { |  | ||||||
| 			os.apiWithDialog('admin/resolve-abuse-user-report', { |  | ||||||
| 				reportId: report.id, |  | ||||||
| 			}).then(() => { |  | ||||||
| 				this.$refs.reports.removeItem(item => item.id === report.id); |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -124,29 +97,4 @@ export default defineComponent({ | ||||||
| .lcixvhis { | .lcixvhis { | ||||||
| 	margin: var(--margin); | 	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> | </style> | ||||||
|  |  | ||||||
|  | @ -252,7 +252,7 @@ export function getNoteMenu(props: { | ||||||
| 				icon: 'fas fa-exclamation-circle', | 				icon: 'fas fa-exclamation-circle', | ||||||
| 				text: i18n.locale.reportAbuse, | 				text: i18n.locale.reportAbuse, | ||||||
| 				action: () => { | 				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'), { | 					os.popup(import('@/components/abuse-report-window.vue'), { | ||||||
| 						user: appearNote.user, | 						user: appearNote.user, | ||||||
| 						initialComment: `Note: ${u}\n-----\n` | 						initialComment: `Note: ${u}\n-----\n` | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue