parent
							
								
									ac5d798cde
								
							
						
					
					
						commit
						048b9c295e
					
				
					 12 changed files with 486 additions and 212 deletions
				
			
		|  | @ -380,6 +380,19 @@ common/views/components/note-menu.vue: | |||
|   delete-confirm: "この投稿を削除しますか?" | ||||
|   remote: "投稿元で見る" | ||||
| 
 | ||||
| common/views/components/user-menu.vue: | ||||
|   mention: "メンション" | ||||
|   mute: "ミュート" | ||||
|   unmute: "ミュート解除" | ||||
|   block: "ブロック" | ||||
|   unblock: "ブロック解除" | ||||
|   push-to-list: "リストに追加" | ||||
|   select-list: "リストを選択してください" | ||||
|   list-pushed: "{user}を{list}に追加しました" | ||||
|   report-abuse: "スパムを報告" | ||||
|   report-abuse-detail: "どのような迷惑行為を行っていますか?" | ||||
|   report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。" | ||||
| 
 | ||||
| common/views/components/poll.vue: | ||||
|   vote-to: "「{}」に投票する" | ||||
|   vote-count: "{}票" | ||||
|  | @ -1103,6 +1116,7 @@ admin/views/index.vue: | |||
|   federation: "連合" | ||||
|   announcements: "お知らせ" | ||||
|   hashtags: "ハッシュタグ" | ||||
|   abuse: "スパム報告" | ||||
|   back-to-misskey: "Misskeyに戻る" | ||||
| 
 | ||||
| admin/views/dashboard.vue: | ||||
|  | @ -1114,6 +1128,13 @@ admin/views/dashboard.vue: | |||
|   this-instance: "このインスタンス" | ||||
|   federated: "連合" | ||||
| 
 | ||||
| admin/views/abuse.vue: | ||||
|   title: "スパム報告" | ||||
|   target: "対象" | ||||
|   reporter: "報告者" | ||||
|   details: "詳細" | ||||
|   remove-report: "削除" | ||||
| 
 | ||||
| admin/views/instance.vue: | ||||
|   instance: "インスタンス" | ||||
|   instance-name: "インスタンス名" | ||||
|  | @ -1384,20 +1405,12 @@ desktop/views/pages/user/user.profile.vue: | |||
|   stalk: "ストークする" | ||||
|   stalking: "ストーキングしています" | ||||
|   unstalk: "ストーク解除" | ||||
|   mute: "ミュートする" | ||||
|   muted: "ミュートしています" | ||||
|   unmute: "ミュート解除" | ||||
|   block: "ブロックする" | ||||
|   unblock: "ブロック解除" | ||||
|   block-confirm: "このユーザーをブロックしますか?" | ||||
|   push-to-a-list: "リストに追加" | ||||
|   list-pushed: "{user}を{list}に追加しました。" | ||||
|   menu: "メニュー" | ||||
| 
 | ||||
| desktop/views/pages/user/user.header.vue: | ||||
|   posts: "投稿" | ||||
|   following: "フォロー" | ||||
|   followers: "フォロワー" | ||||
|   mention: "メンション" | ||||
|   is-bot: "このアカウントはBotです" | ||||
|   years-old: "{age}歳" | ||||
|   year: "年" | ||||
|  | @ -1686,14 +1699,7 @@ mobile/views/pages/user.vue: | |||
|   overview: "概要" | ||||
|   timeline: "タイムライン" | ||||
|   media: "メディア" | ||||
|   mute: "ミュート" | ||||
|   unmute: "ミュート解除" | ||||
|   block: "ブロック" | ||||
|   unblock: "ブロック解除" | ||||
|   years-old: "{age}歳" | ||||
|   push-to-list: "リストに追加" | ||||
|   select-list: "リストを選択してください" | ||||
|   list-pushed: "{user}を{list}に追加しました" | ||||
| 
 | ||||
| mobile/views/pages/user/home.vue: | ||||
|   recent-notes: "最近の投稿" | ||||
|  | @ -1747,12 +1753,10 @@ deck/deck.user-column.vue: | |||
|   posts: "投稿" | ||||
|   following: "フォロー" | ||||
|   followers: "フォロワー" | ||||
|   mention: "メンション" | ||||
|   images: "画像" | ||||
|   activity: "アクティビティ" | ||||
|   timeline: "タイムライン" | ||||
|   pinned-notes: "ピン留めされた投稿" | ||||
|   push-to-a-list: "リストに追加" | ||||
| 
 | ||||
| docs: | ||||
|   edit-this-page-on-github: "間違いや改善点を見つけましたか?" | ||||
|  |  | |||
							
								
								
									
										87
									
								
								src/client/app/admin/views/abuse.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/client/app/admin/views/abuse.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | |||
| <template> | ||||
| <div class="wbjusose"> | ||||
| 	<ui-card> | ||||
| 		<div slot="title"><fa :icon="faExclamationCircle"/> {{ $t('title') }}</div> | ||||
| 		<section class="fit-top"> | ||||
| 			<sequential-entrance animation="entranceFromTop" delay="25"> | ||||
| 				<div v-for="report in userReports" :key="report.id" class="haexwsjc"> | ||||
| 					<ui-horizon-group inputs> | ||||
| 						<ui-input :value="report.user | acct" type="text"> | ||||
| 							<span>{{ $t('target') }}</span> | ||||
| 						</ui-input> | ||||
| 						<ui-input :value="report.reporter | acct" type="text"> | ||||
| 							<span>{{ $t('reporter') }}</span> | ||||
| 						</ui-input> | ||||
| 					</ui-horizon-group> | ||||
| 					<ui-textarea :value="report.comment" readonly> | ||||
| 						<span>{{ $t('details') }}</span> | ||||
| 					</ui-textarea> | ||||
| 					<ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button> | ||||
| 				</div> | ||||
| 			</sequential-entrance> | ||||
| 			<ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../i18n'; | ||||
| import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('admin/views/abuse.vue'), | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			limit: 10, | ||||
| 			untilId: undefined, | ||||
| 			userReports: [], | ||||
| 			existMore: false, | ||||
| 			faExclamationCircle | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetchUserReports(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetchUserReports() { | ||||
| 			this.$root.api('admin/abuse-user-reports', { | ||||
| 				untilId: this.untilId, | ||||
| 				limit: this.limit + 1 | ||||
| 			}).then(reports => { | ||||
| 				if (reports.length == this.limit + 1) { | ||||
| 					reports.pop(); | ||||
| 					this.existMore = true; | ||||
| 				} else { | ||||
| 					this.existMore = false; | ||||
| 				} | ||||
| 				this.userReports = this.userReports.concat(reports); | ||||
| 				this.untilId = this.userReports[this.userReports.length - 1].id; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		removeReport(report) { | ||||
| 			this.$root.api('admin/remove-abuse-user-report', { | ||||
| 				reportId: report.id | ||||
| 			}).then(() => { | ||||
| 				this.userReports = this.userReports.filter(r => r.id != report.id); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| .wbjusose | ||||
| 	@media (min-width 500px) | ||||
| 		padding 16px | ||||
| 
 | ||||
| 	.haexwsjc | ||||
| 		padding-bottom 16px | ||||
| 		border-bottom solid 1px var(--faceDivider) | ||||
| 
 | ||||
| </style> | ||||
|  | @ -27,6 +27,7 @@ | |||
| 			<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li> | ||||
| 			<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li> | ||||
| 			<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li> | ||||
| 			<li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li> | ||||
| 		</ul> | ||||
| 		<div class="back-to-misskey"> | ||||
| 			<a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a> | ||||
|  | @ -45,7 +46,7 @@ | |||
| 			<div v-if="page == 'announcements'"><x-announcements/></div> | ||||
| 			<div v-if="page == 'hashtags'"><x-hashtags/></div> | ||||
| 			<div v-if="page == 'drive'"><x-drive/></div> | ||||
| 			<div v-if="page == 'update'"></div> | ||||
| 			<div v-if="page == 'abuse'"><x-abuse/></div> | ||||
| 		</div> | ||||
| 	</main> | ||||
| </div> | ||||
|  | @ -63,7 +64,8 @@ import XAnnouncements from "./announcements.vue"; | |||
| import XHashtags from "./hashtags.vue"; | ||||
| import XUsers from "./users.vue"; | ||||
| import XDrive from "./drive.vue"; | ||||
| import { faHeadset, faArrowLeft, faShareAlt } from '@fortawesome/free-solid-svg-icons'; | ||||
| import XAbuse from "./abuse.vue"; | ||||
| import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faGrin } from '@fortawesome/free-regular-svg-icons'; | ||||
| 
 | ||||
| // Detect the user agent | ||||
|  | @ -81,6 +83,7 @@ export default Vue.extend({ | |||
| 		XHashtags, | ||||
| 		XUsers, | ||||
| 		XDrive, | ||||
| 		XAbuse, | ||||
| 	}, | ||||
| 	provide: { | ||||
| 		isMobile | ||||
|  | @ -94,7 +97,8 @@ export default Vue.extend({ | |||
| 			faGrin, | ||||
| 			faArrowLeft, | ||||
| 			faHeadset, | ||||
| 			faShareAlt | ||||
| 			faShareAlt, | ||||
| 			faExclamationCircle | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
|  |  | |||
							
								
								
									
										157
									
								
								src/client/app/common/views/components/user-menu.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/client/app/common/views/components/user-menu.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,157 @@ | |||
| <template> | ||||
| <div style="position:initial"> | ||||
| 	<mk-menu :source="source" :items="items" @closed="closed"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; | ||||
| import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/user-menu.vue'), | ||||
| 
 | ||||
| 	props: ['user', 'source'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		let menu = [{ | ||||
| 			icon: ['fas', 'at'], | ||||
| 			text: this.$t('mention'), | ||||
| 			action: () => { | ||||
| 				this.$post({ mention: this.user }); | ||||
| 			} | ||||
| 		}, null, { | ||||
| 			icon: ['fas', 'list'], | ||||
| 			text: this.$t('push-to-list'), | ||||
| 			action: this.pushList | ||||
| 		}, null, { | ||||
| 			icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], | ||||
| 			text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), | ||||
| 			action: this.toggleMute | ||||
| 		}, { | ||||
| 			icon: 'ban', | ||||
| 			text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), | ||||
| 			action: this.toggleBlock | ||||
| 		}, null, { | ||||
| 			icon: faExclamationCircle, | ||||
| 			text: this.$t('report-abuse'), | ||||
| 			action: this.reportAbuse | ||||
| 		}]; | ||||
| 
 | ||||
| 		return { | ||||
| 			items: menu | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		closed() { | ||||
| 			this.$nextTick(() => { | ||||
| 				this.destroyDom(); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async pushList() { | ||||
| 			const lists = await this.$root.api('users/lists/list'); | ||||
| 			const { canceled, result: listId } = await this.$root.dialog({ | ||||
| 				type: null, | ||||
| 				title: this.$t('select-list'), | ||||
| 				select: { | ||||
| 					items: lists.map(list => ({ | ||||
| 						value: list.id, text: list.title | ||||
| 					})) | ||||
| 				}, | ||||
| 				showCancelButton: true | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			await this.$root.api('users/lists/push', { | ||||
| 				listId: listId, | ||||
| 				userId: this.user.id | ||||
| 			}); | ||||
| 			this.$root.dialog({ | ||||
| 				type: 'success', | ||||
| 				text: this.$t('list-pushed', { | ||||
| 					user: this.user.name, | ||||
| 					list: lists.find(l => l.id === listId).title | ||||
| 				}) | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		toggleMute() { | ||||
| 			if (this.user.isMuted) { | ||||
| 				this.$root.api('mute/delete', { | ||||
| 					userId: this.user.id | ||||
| 				}).then(() => { | ||||
| 					this.user.isMuted = false; | ||||
| 				}, () => { | ||||
| 					this.$root.dialog({ | ||||
| 						type: 'error', | ||||
| 						text: e | ||||
| 					}); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				this.$root.api('mute/create', { | ||||
| 					userId: this.user.id | ||||
| 				}).then(() => { | ||||
| 					this.user.isMuted = true; | ||||
| 				}, () => { | ||||
| 					this.$root.dialog({ | ||||
| 						type: 'error', | ||||
| 						text: e | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		toggleBlock() { | ||||
| 			if (this.user.isBlocking) { | ||||
| 				this.$root.api('blocking/delete', { | ||||
| 					userId: this.user.id | ||||
| 				}).then(() => { | ||||
| 					this.user.isBlocking = false; | ||||
| 				}, () => { | ||||
| 					this.$root.dialog({ | ||||
| 						type: 'error', | ||||
| 						text: e | ||||
| 					}); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				this.$root.api('blocking/create', { | ||||
| 					userId: this.user.id | ||||
| 				}).then(() => { | ||||
| 					this.user.isBlocking = true; | ||||
| 				}, () => { | ||||
| 					this.$root.dialog({ | ||||
| 						type: 'error', | ||||
| 						text: e | ||||
| 					}); | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		async reportAbuse() { | ||||
| 			const reported = this.$t('report-abuse-reported'); // なぜか後で参照すると null になるので最初にメモリに確保しておく | ||||
| 			const { canceled, result: comment } = await this.$root.dialog({ | ||||
| 				title: this.$t('report-abuse-detail'), | ||||
| 				input: true | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			this.$root.api('users/report-abuse', { | ||||
| 				userId: this.user.id, | ||||
| 				comment: comment | ||||
| 			}).then(() => { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: reported | ||||
| 				}); | ||||
| 			}, e => { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -49,9 +49,6 @@ | |||
| 					<b>{{ user.followersCount | number }}</b> | ||||
| 					<span>{{ $t('followers') }}</span> | ||||
| 				</div> | ||||
| 				<div class="mention"> | ||||
| 					<button @click="mention" :title="$t('mention')"><fa icon="at"/></button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0"> | ||||
|  | @ -100,8 +97,7 @@ import parseAcct from '../../../../../../misc/acct/parse'; | |||
| import XColumn from './deck.column.vue'; | ||||
| import XNotes from './deck.notes.vue'; | ||||
| import XNote from '../../components/note.vue'; | ||||
| import Menu from '../../../../common/views/components/menu.vue'; | ||||
| import MkUserListsWindow from '../../components/user-lists-window.vue'; | ||||
| import XUserMenu from '../../../../common/views/components/user-menu.vue'; | ||||
| import { concat } from '../../../../../../prelude/array'; | ||||
| import * as ApexCharts from 'apexcharts'; | ||||
| 
 | ||||
|  | @ -306,33 +302,10 @@ export default Vue.extend({ | |||
| 			return promise; | ||||
| 		}, | ||||
| 
 | ||||
| 		mention() { | ||||
| 			this.$post({ mention: this.user }); | ||||
| 		}, | ||||
| 
 | ||||
| 		menu() { | ||||
| 			let menu = [{ | ||||
| 				icon: 'list', | ||||
| 				text: this.$t('push-to-a-list'), | ||||
| 				action: () => { | ||||
| 					const w = this.$root.new(MkUserListsWindow); | ||||
| 					w.$once('choosen', async list => { | ||||
| 						w.close(); | ||||
| 						await this.$root.api('users/lists/push', { | ||||
| 							listId: list.id, | ||||
| 							userId: this.user.id | ||||
| 						}); | ||||
| 						this.$root.dialog({ | ||||
| 							type: 'success', | ||||
| 							splash: true | ||||
| 						}); | ||||
| 					}); | ||||
| 				} | ||||
| 			}]; | ||||
| 
 | ||||
| 			this.$root.new(Menu, { | ||||
| 			this.$root.new(XUserMenu, { | ||||
| 				source: this.$refs.menu, | ||||
| 				items: menu | ||||
| 				user: this.user | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -459,7 +432,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		> .counts | ||||
| 			display grid | ||||
| 			grid-template-columns 2fr 2fr 2fr 1fr | ||||
| 			grid-template-columns 2fr 2fr 2fr | ||||
| 			margin-top 8px | ||||
| 			border-top solid var(--lineWidth) var(--faceDivider) | ||||
| 
 | ||||
|  | @ -476,9 +449,6 @@ export default Vue.extend({ | |||
| 					font-size 80% | ||||
| 					opacity 0.7 | ||||
| 
 | ||||
| 			> .mention | ||||
| 				display flex | ||||
| 
 | ||||
| 	> * | ||||
| 		> p.caption | ||||
| 			margin 0 | ||||
|  |  | |||
|  | @ -36,7 +36,6 @@ | |||
| 			<span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span> | ||||
| 			<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link> | ||||
| 			<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link> | ||||
| 			<button @click="mention" :title="$t('mention')"><fa icon="at"/></button> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -9,15 +9,7 @@ | |||
| 		</p> | ||||
| 	</div> | ||||
| 	<div class="action-form"> | ||||
| 		<ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id"> | ||||
| 			<span v-if="user.isMuted"><fa icon="eye"/> {{ $t('unmute') }}</span> | ||||
| 			<span v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mute') }}</span> | ||||
| 		</ui-button> | ||||
| 		<ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id"> | ||||
| 			<span v-if="user.isBlocking"><fa icon="ban"/> {{ $t('unblock') }}</span> | ||||
| 			<span v-else><fa icon="ban"/> {{ $t('block') }}</span> | ||||
| 		</ui-button> | ||||
| 		<ui-button @click="list"><fa icon="list"/> {{ $t('push-to-a-list') }}</ui-button> | ||||
| 		<ui-button @click="menu" ref="menu">{{ $t('menu') }}</ui-button> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -25,7 +17,7 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../../i18n'; | ||||
| import MkUserListsWindow from '../../components/user-lists-window.vue'; | ||||
| import XUserMenu from '../../../../common/views/components/user-menu.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('desktop/views/pages/user/user.profile.vue'), | ||||
|  | @ -52,72 +44,12 @@ export default Vue.extend({ | |||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		mute() { | ||||
| 			this.$root.api('mute/create', { | ||||
| 				userId: this.user.id | ||||
| 			}).then(() => { | ||||
| 				this.user.isMuted = true; | ||||
| 			}, () => { | ||||
| 				alert('error'); | ||||
| 		menu() { | ||||
| 			this.$root.new(XUserMenu, { | ||||
| 				source: this.$refs.menu.$el, | ||||
| 				user: this.user | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		unmute() { | ||||
| 			this.$root.api('mute/delete', { | ||||
| 				userId: this.user.id | ||||
| 			}).then(() => { | ||||
| 				this.user.isMuted = false; | ||||
| 			}, () => { | ||||
| 				alert('error'); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		block() { | ||||
| 			this.$root.dialog({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$t('block-confirm'), | ||||
| 				showCancelButton: true | ||||
| 			}).then(({ canceled }) => { | ||||
| 				if (canceled) return; | ||||
| 
 | ||||
| 				this.$root.api('blocking/create', { | ||||
| 					userId: this.user.id | ||||
| 				}).then(() => { | ||||
| 					this.user.isBlocking = true; | ||||
| 				}, () => { | ||||
| 					alert('error'); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		unblock() { | ||||
| 			this.$root.api('blocking/delete', { | ||||
| 				userId: this.user.id | ||||
| 			}).then(() => { | ||||
| 				this.user.isBlocking = false; | ||||
| 			}, () => { | ||||
| 				alert('error'); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		list() { | ||||
| 			const w = this.$root.new(MkUserListsWindow); | ||||
| 			w.$once('choosen', async list => { | ||||
| 				w.close(); | ||||
| 				await this.$root.api('users/lists/push', { | ||||
| 					listId: list.id, | ||||
| 					userId: this.user.id | ||||
| 				}); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					title: 'Done!', | ||||
| 					text: this.$t('list-pushed', { | ||||
| 						user: this.user.name, | ||||
| 						list: list.title | ||||
| 					}) | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -55,7 +55,6 @@ | |||
| 						<b>{{ user.followersCount | number }}</b> | ||||
| 						<i>{{ $t('followers') }}</i> | ||||
| 					</a> | ||||
| 					<button @click="mention"><fa icon="at"/></button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</header> | ||||
|  | @ -81,7 +80,7 @@ import i18n from '../../../i18n'; | |||
| import * as age from 's-age'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import Progress from '../../../common/scripts/loading'; | ||||
| import Menu from '../../../common/views/components/menu.vue'; | ||||
| import XUserMenu from '../../../common/views/components/user-menu.vue'; | ||||
| import XHome from './user/home.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
|  | @ -127,88 +126,10 @@ export default Vue.extend({ | |||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		mention() { | ||||
| 			this.$post({ mention: this.user }); | ||||
| 		}, | ||||
| 
 | ||||
| 		menu() { | ||||
| 			let menu = [{ | ||||
| 				icon: ['fas', 'list'], | ||||
| 				text: this.$t('push-to-list'), | ||||
| 				action: async () => { | ||||
| 					const lists = await this.$root.api('users/lists/list'); | ||||
| 					const { canceled, result: listId } = await this.$root.dialog({ | ||||
| 						type: null, | ||||
| 						title: this.$t('select-list'), | ||||
| 						select: { | ||||
| 							items: lists.map(list => ({ | ||||
| 								value: list.id, text: list.title | ||||
| 							})) | ||||
| 						}, | ||||
| 						showCancelButton: true | ||||
| 					}); | ||||
| 					if (canceled) return; | ||||
| 					await this.$root.api('users/lists/push', { | ||||
| 						listId: listId, | ||||
| 						userId: this.user.id | ||||
| 					}); | ||||
| 					this.$root.dialog({ | ||||
| 						type: 'success', | ||||
| 						text: this.$t('list-pushed', { | ||||
| 							user: this.user.name, | ||||
| 							list: lists.find(l => l.id === listId).title | ||||
| 						}) | ||||
| 					}); | ||||
| 				} | ||||
| 			}, null, { | ||||
| 				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], | ||||
| 				text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'), | ||||
| 				action: () => { | ||||
| 					if (this.user.isMuted) { | ||||
| 						this.$root.api('mute/delete', { | ||||
| 							userId: this.user.id | ||||
| 						}).then(() => { | ||||
| 							this.user.isMuted = false; | ||||
| 						}, () => { | ||||
| 							alert('error'); | ||||
| 						}); | ||||
| 					} else { | ||||
| 						this.$root.api('mute/create', { | ||||
| 							userId: this.user.id | ||||
| 						}).then(() => { | ||||
| 							this.user.isMuted = true; | ||||
| 						}, () => { | ||||
| 							alert('error'); | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 			}, { | ||||
| 				icon: 'ban', | ||||
| 				text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'), | ||||
| 				action: () => { | ||||
| 					if (this.user.isBlocking) { | ||||
| 						this.$root.api('blocking/delete', { | ||||
| 							userId: this.user.id | ||||
| 						}).then(() => { | ||||
| 							this.user.isBlocking = false; | ||||
| 						}, () => { | ||||
| 							alert('error'); | ||||
| 						}); | ||||
| 					} else { | ||||
| 						this.$root.api('blocking/create', { | ||||
| 							userId: this.user.id | ||||
| 						}).then(() => { | ||||
| 							this.user.isBlocking = true; | ||||
| 						}, () => { | ||||
| 							alert('error'); | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 			}]; | ||||
| 
 | ||||
| 			this.$root.new(Menu, { | ||||
| 			this.$root.new(XUserMenu, { | ||||
| 				source: this.$refs.menu, | ||||
| 				items: menu | ||||
| 				user: this.user | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										52
									
								
								src/models/abuse-user-report.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/models/abuse-user-report.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| const deepcopy = require('deepcopy'); | ||||
| import db from '../db/mongodb'; | ||||
| import isObjectId from '../misc/is-objectid'; | ||||
| import { pack as packUser } from './user'; | ||||
| 
 | ||||
| const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports'); | ||||
| AbuseUserReport.createIndex('userId'); | ||||
| AbuseUserReport.createIndex('reporterId'); | ||||
| AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true }); | ||||
| export default AbuseUserReport; | ||||
| 
 | ||||
| export interface IAbuseUserReport { | ||||
| 	_id: mongo.ObjectID; | ||||
| 	createdAt: Date; | ||||
| 	userId: mongo.ObjectID; | ||||
| 	reporterId: mongo.ObjectID; | ||||
| 	comment: string; | ||||
| } | ||||
| 
 | ||||
| export const packMany = ( | ||||
| 	reports: (string | mongo.ObjectID | IAbuseUserReport)[] | ||||
| ) => { | ||||
| 	return Promise.all(reports.map(x => pack(x))); | ||||
| }; | ||||
| 
 | ||||
| export const pack = ( | ||||
| 	report: any | ||||
| ) => new Promise<any>(async (resolve, reject) => { | ||||
| 	let _report: any; | ||||
| 
 | ||||
| 	if (isObjectId(report)) { | ||||
| 		_report = await AbuseUserReport.findOne({ | ||||
| 			_id: report | ||||
| 		}); | ||||
| 	} else if (typeof report === 'string') { | ||||
| 		_report = await AbuseUserReport.findOne({ | ||||
| 			_id: new mongo.ObjectID(report) | ||||
| 		}); | ||||
| 	} else { | ||||
| 		_report = deepcopy(report); | ||||
| 	} | ||||
| 
 | ||||
| 	// Rename _id to id
 | ||||
| 	_report.id = _report._id; | ||||
| 	delete _report._id; | ||||
| 
 | ||||
| 	_report.reporter = await packUser(_report.reporterId, null, { detail: true }); | ||||
| 	_report.user = await packUser(_report.userId, null, { detail: true }); | ||||
| 
 | ||||
| 	resolve(_report); | ||||
| }); | ||||
							
								
								
									
										54
									
								
								src/server/api/endpoints/admin/abuse-user-reports.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/server/api/endpoints/admin/abuse-user-reports.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; | ||||
| import Report, { packMany } from '../../../../models/abuse-user-report'; | ||||
| import define from '../../define'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
| 	requireModerator: true, | ||||
| 
 | ||||
| 	params: { | ||||
| 		limit: { | ||||
| 			validator: $.num.optional.range(1, 100), | ||||
| 			default: 10 | ||||
| 		}, | ||||
| 
 | ||||
| 		sinceId: { | ||||
| 			validator: $.type(ID).optional, | ||||
| 			transform: transform, | ||||
| 		}, | ||||
| 
 | ||||
| 		untilId: { | ||||
| 			validator: $.type(ID).optional, | ||||
| 			transform: transform, | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps) => new Promise(async (res, rej) => { | ||||
| 	if (ps.sinceId && ps.untilId) { | ||||
| 		return rej('cannot set sinceId and untilId'); | ||||
| 	} | ||||
| 
 | ||||
| 	const sort = { | ||||
| 		_id: -1 | ||||
| 	}; | ||||
| 	const query = {} as any; | ||||
| 	if (ps.sinceId) { | ||||
| 		sort._id = 1; | ||||
| 		query._id = { | ||||
| 			$gt: ps.sinceId | ||||
| 		}; | ||||
| 	} else if (ps.untilId) { | ||||
| 		query._id = { | ||||
| 			$lt: ps.untilId | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	const reports = await Report | ||||
| 		.find(query, { | ||||
| 			limit: ps.limit, | ||||
| 			sort: sort | ||||
| 		}); | ||||
| 
 | ||||
| 	res(await packMany(reports)); | ||||
| })); | ||||
							
								
								
									
										32
									
								
								src/server/api/endpoints/admin/remove-abuse-user-report.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/server/api/endpoints/admin/remove-abuse-user-report.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import $ from 'cafy'; | ||||
| import ID, { transform } from '../../../../misc/cafy-id'; | ||||
| import define from '../../define'; | ||||
| import AbuseUserReport from '../../../../models/abuse-user-report'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
| 	requireModerator: true, | ||||
| 
 | ||||
| 	params: { | ||||
| 		reportId: { | ||||
| 			validator: $.type(ID), | ||||
| 			transform: transform | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps) => new Promise(async (res, rej) => { | ||||
| 	const report = await AbuseUserReport.findOne({ | ||||
| 		_id: ps.reportId | ||||
| 	}); | ||||
| 
 | ||||
| 	if (report == null) { | ||||
| 		return rej('report not found'); | ||||
| 	} | ||||
| 
 | ||||
| 	await AbuseUserReport.remove({ | ||||
| 		_id: report._id | ||||
| 	}); | ||||
| 
 | ||||
| 	res(); | ||||
| })); | ||||
							
								
								
									
										62
									
								
								src/server/api/endpoints/users/report-abuse.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/server/api/endpoints/users/report-abuse.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id'; | ||||
| import define from '../../define'; | ||||
| import User from '../../../../models/user'; | ||||
| import AbuseUserReport from '../../../../models/abuse-user-report'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定したユーザーを迷惑なユーザーであると報告します。' | ||||
| 	}, | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 
 | ||||
| 	params: { | ||||
| 		userId: { | ||||
| 			validator: $.type(ID), | ||||
| 			transform: transform, | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のユーザーのID', | ||||
| 				'en-US': 'Target user ID' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		comment: { | ||||
| 			validator: $.str.range(1, 3000), | ||||
| 			desc: { | ||||
| 				'ja-JP': '迷惑行為の詳細' | ||||
| 			} | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 	// Lookup user
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: ps.userId | ||||
| 	}, { | ||||
| 		fields: { | ||||
| 			_id: true | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		return rej('user not found'); | ||||
| 	} | ||||
| 
 | ||||
| 	if (user._id.equals(me._id)) { | ||||
| 		return rej('cannot report yourself'); | ||||
| 	} | ||||
| 
 | ||||
| 	if (user.isAdmin) { | ||||
| 		return rej('cannot report admin'); | ||||
| 	} | ||||
| 
 | ||||
| 	await AbuseUserReport.insert({ | ||||
| 		createdAt: new Date(), | ||||
| 		userId: user._id, | ||||
| 		reporterId: me._id, | ||||
| 		comment: ps.comment | ||||
| 	}); | ||||
| 
 | ||||
| 	res(); | ||||
| })); | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue