Resolve #3238
This commit is contained in:
		
							parent
							
								
									0fc965f342
								
							
						
					
					
						commit
						b34c1379e9
					
				
					 18 changed files with 234 additions and 11 deletions
				
			
		|  | @ -1529,6 +1529,12 @@ admin/views/moderators.vue: | |||
|     added: "モデレーターを登録しました" | ||||
|     remove: "解除" | ||||
|     removed: "モデレーター登録を解除しました" | ||||
|   logs: | ||||
|     title: "ログ" | ||||
|     moderator: "モデレーター" | ||||
|     type: "操作" | ||||
|     at: "日時" | ||||
|     info: "情報" | ||||
| 
 | ||||
| admin/views/emoji.vue: | ||||
|   add-emoji: | ||||
|  |  | |||
							
								
								
									
										17
									
								
								migration/1562869971568-ModerationLog.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								migration/1562869971568-ModerationLog.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| import {MigrationInterface, QueryRunner} from "typeorm"; | ||||
| 
 | ||||
| export class ModerationLog1562869971568 implements MigrationInterface { | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<any> { | ||||
|         await queryRunner.query(`CREATE TABLE "moderation_log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(128) NOT NULL, "info" jsonb NOT NULL, CONSTRAINT "PK_d0adca6ecfd068db83e4526cc26" PRIMARY KEY ("id"))`); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_a08ad074601d204e0f69da9a95" ON "moderation_log" ("userId") `); | ||||
|         await queryRunner.query(`ALTER TABLE "moderation_log" ADD CONSTRAINT "FK_a08ad074601d204e0f69da9a954" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<any> { | ||||
|         await queryRunner.query(`ALTER TABLE "moderation_log" DROP CONSTRAINT "FK_a08ad074601d204e0f69da9a954"`); | ||||
|         await queryRunner.query(`DROP INDEX "IDX_a08ad074601d204e0f69da9a95"`); | ||||
|         await queryRunner.query(`DROP TABLE "moderation_log"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -12,6 +12,31 @@ | |||
| 			</ui-horizon-group> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| 
 | ||||
| 	<ui-card> | ||||
| 		<template #title>{{ $t('logs.title') }}</template> | ||||
| 		<section class="fit-top"> | ||||
| 			<sequential-entrance animation="entranceFromTop" delay="25"> | ||||
| 				<div v-for="log in logs" :key="log.id" class=""> | ||||
| 					<ui-horizon-group inputs> | ||||
| 						<ui-input :value="log.user | acct" type="text" readonly> | ||||
| 							<span>{{ $t('logs.moderator') }}</span> | ||||
| 						</ui-input> | ||||
| 						<ui-input :value="log.type" type="text" readonly> | ||||
| 							<span>{{ $t('logs.type') }}</span> | ||||
| 						</ui-input> | ||||
| 						<ui-input :value="log.createdAt | date" type="text" readonly> | ||||
| 							<span>{{ $t('logs.at') }}</span> | ||||
| 						</ui-input> | ||||
| 					</ui-horizon-group> | ||||
| 					<ui-textarea :value="JSON.stringify(log.info, null, 4)" readonly> | ||||
| 						<span>{{ $t('logs.info') }}</span> | ||||
| 					</ui-textarea> | ||||
| 				</div> | ||||
| 			</sequential-entrance> | ||||
| 			<ui-button v-if="existMoreLogs" @click="fetchLogs">{{ $t('@.load-more') }}</ui-button> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -26,10 +51,17 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			username: '', | ||||
| 			changing: false | ||||
| 			changing: false, | ||||
| 			logs: [], | ||||
| 			untilLogId: null, | ||||
| 			existMoreLogs: false | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.fetchLogs(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async add() { | ||||
| 			this.changing = true; | ||||
|  | @ -74,6 +106,22 @@ export default Vue.extend({ | |||
| 
 | ||||
| 			this.changing = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		fetchLogs() { | ||||
| 			this.$root.api('admin/show-moderation-logs', { | ||||
| 				untilId: this.untilId, | ||||
| 				limit: 10 + 1 | ||||
| 			}).then(logs => { | ||||
| 				if (logs.length == 10 + 1) { | ||||
| 					logs.pop(); | ||||
| 					this.existMoreLogs = true; | ||||
| 				} else { | ||||
| 					this.existMoreLogs = false; | ||||
| 				} | ||||
| 				this.logs = this.logs.concat(logs); | ||||
| 				this.untilLogId = this.logs[this.logs.length - 1].id; | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -47,6 +47,7 @@ import { UserSecurityKey } from '../models/entities/user-security-key'; | |||
| import { AttestationChallenge } from '../models/entities/attestation-challenge'; | ||||
| import { Page } from '../models/entities/page'; | ||||
| import { PageLike } from '../models/entities/page-like'; | ||||
| import { ModerationLog } from '../models/entities/moderation-log'; | ||||
| 
 | ||||
| const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | ||||
| 
 | ||||
|  | @ -124,6 +125,7 @@ export const entities = [ | |||
| 	RegistrationTicket, | ||||
| 	MessagingMessage, | ||||
| 	Signin, | ||||
| 	ModerationLog, | ||||
| 	ReversiGame, | ||||
| 	ReversiMatching, | ||||
| 	...charts as any | ||||
|  |  | |||
							
								
								
									
										32
									
								
								src/models/entities/moderation-log.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/models/entities/moderation-log.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; | ||||
| import { User } from './user'; | ||||
| import { id } from '../id'; | ||||
| 
 | ||||
| @Entity() | ||||
| export class ModerationLog { | ||||
| 	@PrimaryColumn(id()) | ||||
| 	public id: string; | ||||
| 
 | ||||
| 	@Column('timestamp with time zone', { | ||||
| 		comment: 'The created date of the ModerationLog.' | ||||
| 	}) | ||||
| 	public createdAt: Date; | ||||
| 
 | ||||
| 	@Index() | ||||
| 	@Column(id()) | ||||
| 	public userId: User['id']; | ||||
| 
 | ||||
| 	@ManyToOne(type => User, { | ||||
| 		onDelete: 'CASCADE' | ||||
| 	}) | ||||
| 	@JoinColumn() | ||||
| 	public user: User | null; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 128, | ||||
| 	}) | ||||
| 	public type: string; | ||||
| 
 | ||||
| 	@Column('jsonb') | ||||
| 	public info: Record<string, any>; | ||||
| } | ||||
|  | @ -42,6 +42,7 @@ import { UserSecurityKey } from './entities/user-security-key'; | |||
| import { HashtagRepository } from './repositories/hashtag'; | ||||
| import { PageRepository } from './repositories/page'; | ||||
| import { PageLikeRepository } from './repositories/page-like'; | ||||
| import { ModerationLogRepository } from './repositories/moderation-logs'; | ||||
| 
 | ||||
| export const Apps = getCustomRepository(AppRepository); | ||||
| export const Notes = getCustomRepository(NoteRepository); | ||||
|  | @ -86,3 +87,4 @@ export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository); | |||
| export const Logs = getRepository(Log); | ||||
| export const Pages = getCustomRepository(PageRepository); | ||||
| export const PageLikes = getCustomRepository(PageLikeRepository); | ||||
| export const ModerationLogs = getCustomRepository(ModerationLogRepository); | ||||
|  |  | |||
							
								
								
									
										31
									
								
								src/models/repositories/moderation-logs.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/models/repositories/moderation-logs.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| import { EntityRepository, Repository } from 'typeorm'; | ||||
| import { Users } from '..'; | ||||
| import { ModerationLog } from '../entities/moderation-log'; | ||||
| import { ensure } from '../../prelude/ensure'; | ||||
| import { awaitAll } from '../../prelude/await-all'; | ||||
| 
 | ||||
| @EntityRepository(ModerationLog) | ||||
| export class ModerationLogRepository extends Repository<ModerationLog> { | ||||
| 	public async pack( | ||||
| 		src: ModerationLog['id'] | ModerationLog, | ||||
| 	) { | ||||
| 		const log = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | ||||
| 
 | ||||
| 		return await awaitAll({ | ||||
| 			id: log.id, | ||||
| 			createdAt: log.createdAt, | ||||
| 			type: log.type, | ||||
| 			info: log.info, | ||||
| 			userId: log.userId, | ||||
| 			user: Users.pack(log.user || log.userId, null, { | ||||
| 				detail: true | ||||
| 			}), | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public packMany( | ||||
| 		reports: any[], | ||||
| 	) { | ||||
| 		return Promise.all(reports.map(x => this.pack(x))); | ||||
| 	} | ||||
| } | ||||
|  | @ -4,6 +4,7 @@ import { detectUrlMine } from '../../../../../misc/detect-url-mine'; | |||
| import { Emojis } from '../../../../../models'; | ||||
| import { genId } from '../../../../../misc/gen-id'; | ||||
| import { getConnection } from 'typeorm'; | ||||
| import { insertModerationLog } from '../../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -31,7 +32,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const type = await detectUrlMine(ps.url); | ||||
| 
 | ||||
| 	const emoji = await Emojis.save({ | ||||
|  | @ -46,6 +47,10 @@ export default define(meta, async (ps) => { | |||
| 
 | ||||
| 	await getConnection().queryResultCache!.remove(['meta_emojis']); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'addEmoji', { | ||||
| 		emojiId: emoji.id | ||||
| 	}); | ||||
| 
 | ||||
| 	return { | ||||
| 		id: emoji.id | ||||
| 	}; | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import define from '../../../define'; | |||
| import { ID } from '../../../../../misc/cafy-id'; | ||||
| import { Emojis } from '../../../../../models'; | ||||
| import { getConnection } from 'typeorm'; | ||||
| import { insertModerationLog } from '../../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -21,7 +22,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const emoji = await Emojis.findOne(ps.id); | ||||
| 
 | ||||
| 	if (emoji == null) throw new Error('emoji not found'); | ||||
|  | @ -29,4 +30,8 @@ export default define(meta, async (ps) => { | |||
| 	await Emojis.delete(emoji.id); | ||||
| 
 | ||||
| 	await getConnection().queryResultCache!.remove(['meta_emojis']); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'removeEmoji', { | ||||
| 		emoji: emoji | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import define from '../../../define'; | ||||
| import { destroy } from '../../../../../queue'; | ||||
| import { insertModerationLog } from '../../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['admin'], | ||||
|  | @ -10,8 +11,8 @@ export const meta = { | |||
| 	params: {} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	destroy(); | ||||
| 
 | ||||
| 	return; | ||||
| 	insertModerationLog(me, 'clearQueue'); | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										35
									
								
								src/server/api/endpoints/admin/show-moderation-logs.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/server/api/endpoints/admin/show-moderation-logs.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| import $ from 'cafy'; | ||||
| import { ID } from '../../../../misc/cafy-id'; | ||||
| import define from '../../define'; | ||||
| import { ModerationLogs } from '../../../../models'; | ||||
| import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['admin'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requireModerator: true, | ||||
| 
 | ||||
| 	params: { | ||||
| 		limit: { | ||||
| 			validator: $.optional.num.range(1, 100), | ||||
| 			default: 10 | ||||
| 		}, | ||||
| 
 | ||||
| 		sinceId: { | ||||
| 			validator: $.optional.type(ID), | ||||
| 		}, | ||||
| 
 | ||||
| 		untilId: { | ||||
| 			validator: $.optional.type(ID), | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| 	const query = makePaginationQuery(ModerationLogs.createQueryBuilder('report'), ps.sinceId, ps.untilId); | ||||
| 
 | ||||
| 	const reports = await query.take(ps.limit!).getMany(); | ||||
| 
 | ||||
| 	return await ModerationLogs.packMany(reports); | ||||
| }); | ||||
|  | @ -2,6 +2,7 @@ import $ from 'cafy'; | |||
| import { ID } from '../../../../misc/cafy-id'; | ||||
| import define from '../../define'; | ||||
| import { Users } from '../../../../models'; | ||||
| import { insertModerationLog } from '../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -25,7 +26,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const user = await Users.findOne(ps.userId as string); | ||||
| 
 | ||||
| 	if (user == null) { | ||||
|  | @ -39,4 +40,8 @@ export default define(meta, async (ps) => { | |||
| 	await Users.update(user.id, { | ||||
| 		isSilenced: true | ||||
| 	}); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'silence', { | ||||
| 		targetId: user.id, | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import define from '../../define'; | |||
| import deleteFollowing from '../../../../services/following/delete'; | ||||
| import { Users, Followings } from '../../../../models'; | ||||
| import { User } from '../../../../models/entities/user'; | ||||
| import { insertModerationLog } from '../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -27,7 +28,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const user = await Users.findOne(ps.userId as string); | ||||
| 
 | ||||
| 	if (user == null) { | ||||
|  | @ -46,6 +47,10 @@ export default define(meta, async (ps) => { | |||
| 		isSuspended: true | ||||
| 	}); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'suspend', { | ||||
| 		targetId: user.id, | ||||
| 	}); | ||||
| 
 | ||||
| 	unFollowAll(user); | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import $ from 'cafy'; | |||
| import { ID } from '../../../../misc/cafy-id'; | ||||
| import define from '../../define'; | ||||
| import { Users } from '../../../../models'; | ||||
| import { insertModerationLog } from '../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -25,7 +26,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const user = await Users.findOne(ps.userId as string); | ||||
| 
 | ||||
| 	if (user == null) { | ||||
|  | @ -35,4 +36,8 @@ export default define(meta, async (ps) => { | |||
| 	await Users.update(user.id, { | ||||
| 		isSilenced: false | ||||
| 	}); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'unsilence', { | ||||
| 		targetId: user.id, | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import $ from 'cafy'; | |||
| import { ID } from '../../../../misc/cafy-id'; | ||||
| import define from '../../define'; | ||||
| import { Users } from '../../../../models'; | ||||
| import { insertModerationLog } from '../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -25,7 +26,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const user = await Users.findOne(ps.userId as string); | ||||
| 
 | ||||
| 	if (user == null) { | ||||
|  | @ -35,4 +36,8 @@ export default define(meta, async (ps) => { | |||
| 	await Users.update(user.id, { | ||||
| 		isSuspended: false | ||||
| 	}); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'unsuspend', { | ||||
| 		targetId: user.id, | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import $ from 'cafy'; | |||
| import define from '../../define'; | ||||
| import { getConnection } from 'typeorm'; | ||||
| import { Meta } from '../../../../models/entities/meta'; | ||||
| import { insertModerationLog } from '../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	desc: { | ||||
|  | @ -401,7 +402,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const set = {} as Partial<Meta>; | ||||
| 
 | ||||
| 	if (ps.announcements) { | ||||
|  | @ -653,4 +654,6 @@ export default define(meta, async (ps) => { | |||
| 			await transactionalEntityManager.save(Meta, set); | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'updateMeta'); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import $ from 'cafy'; | ||||
| import define from '../../define'; | ||||
| import { getConnection } from 'typeorm'; | ||||
| import { insertModerationLog } from '../../../../services/insert-moderation-log'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['admin'], | ||||
|  | @ -18,7 +19,7 @@ export const meta = { | |||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const params: string[] = []; | ||||
| 
 | ||||
| 	if (ps.full) { | ||||
|  | @ -30,4 +31,6 @@ export default define(meta, async (ps) => { | |||
| 	} | ||||
| 
 | ||||
| 	getConnection().query('VACUUM ' + params.join(' ')); | ||||
| 
 | ||||
| 	insertModerationLog(me, 'vacuum', ps); | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										13
									
								
								src/services/insert-moderation-log.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/services/insert-moderation-log.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| import { ILocalUser } from '../models/entities/user'; | ||||
| import { ModerationLogs } from '../models'; | ||||
| import { genId } from '../misc/gen-id'; | ||||
| 
 | ||||
| export async function insertModerationLog(moderator: ILocalUser, type: string, info?: Record<string, any>) { | ||||
| 	await ModerationLogs.save({ | ||||
| 		id: genId(), | ||||
| 		createdAt: new Date(), | ||||
| 		userId: moderator.id, | ||||
| 		type: type, | ||||
| 		info: info || {} | ||||
| 	}); | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue