This commit is contained in:
syuilo 2019-07-14 03:18:45 +09:00
parent 0fc965f342
commit b34c1379e9
18 changed files with 234 additions and 11 deletions

View File

@ -1529,6 +1529,12 @@ admin/views/moderators.vue:
added: "モデレーターを登録しました" added: "モデレーターを登録しました"
remove: "解除" remove: "解除"
removed: "モデレーター登録を解除しました" removed: "モデレーター登録を解除しました"
logs:
title: "ログ"
moderator: "モデレーター"
type: "操作"
at: "日時"
info: "情報"
admin/views/emoji.vue: admin/views/emoji.vue:
add-emoji: add-emoji:

View 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"`);
}
}

View File

@ -12,6 +12,31 @@
</ui-horizon-group> </ui-horizon-group>
</section> </section>
</ui-card> </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> </div>
</template> </template>
@ -26,10 +51,17 @@ export default Vue.extend({
data() { data() {
return { return {
username: '', username: '',
changing: false changing: false,
logs: [],
untilLogId: null,
existMoreLogs: false
}; };
}, },
created() {
this.fetchLogs();
},
methods: { methods: {
async add() { async add() {
this.changing = true; this.changing = true;
@ -74,6 +106,22 @@ export default Vue.extend({
this.changing = false; 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> </script>

View File

@ -47,6 +47,7 @@ import { UserSecurityKey } from '../models/entities/user-security-key';
import { AttestationChallenge } from '../models/entities/attestation-challenge'; import { AttestationChallenge } from '../models/entities/attestation-challenge';
import { Page } from '../models/entities/page'; import { Page } from '../models/entities/page';
import { PageLike } from '../models/entities/page-like'; import { PageLike } from '../models/entities/page-like';
import { ModerationLog } from '../models/entities/moderation-log';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@ -124,6 +125,7 @@ export const entities = [
RegistrationTicket, RegistrationTicket,
MessagingMessage, MessagingMessage,
Signin, Signin,
ModerationLog,
ReversiGame, ReversiGame,
ReversiMatching, ReversiMatching,
...charts as any ...charts as any

View 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>;
}

View File

@ -42,6 +42,7 @@ import { UserSecurityKey } from './entities/user-security-key';
import { HashtagRepository } from './repositories/hashtag'; import { HashtagRepository } from './repositories/hashtag';
import { PageRepository } from './repositories/page'; import { PageRepository } from './repositories/page';
import { PageLikeRepository } from './repositories/page-like'; import { PageLikeRepository } from './repositories/page-like';
import { ModerationLogRepository } from './repositories/moderation-logs';
export const Apps = getCustomRepository(AppRepository); export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository); export const Notes = getCustomRepository(NoteRepository);
@ -86,3 +87,4 @@ export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
export const Logs = getRepository(Log); export const Logs = getRepository(Log);
export const Pages = getCustomRepository(PageRepository); export const Pages = getCustomRepository(PageRepository);
export const PageLikes = getCustomRepository(PageLikeRepository); export const PageLikes = getCustomRepository(PageLikeRepository);
export const ModerationLogs = getCustomRepository(ModerationLogRepository);

View 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)));
}
}

View File

@ -4,6 +4,7 @@ import { detectUrlMine } from '../../../../../misc/detect-url-mine';
import { Emojis } from '../../../../../models'; import { Emojis } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id'; import { genId } from '../../../../../misc/gen-id';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { insertModerationLog } from '../../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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 type = await detectUrlMine(ps.url);
const emoji = await Emojis.save({ const emoji = await Emojis.save({
@ -46,6 +47,10 @@ export default define(meta, async (ps) => {
await getConnection().queryResultCache!.remove(['meta_emojis']); await getConnection().queryResultCache!.remove(['meta_emojis']);
insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id
});
return { return {
id: emoji.id id: emoji.id
}; };

View File

@ -3,6 +3,7 @@ import define from '../../../define';
import { ID } from '../../../../../misc/cafy-id'; import { ID } from '../../../../../misc/cafy-id';
import { Emojis } from '../../../../../models'; import { Emojis } from '../../../../../models';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { insertModerationLog } from '../../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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); const emoji = await Emojis.findOne(ps.id);
if (emoji == null) throw new Error('emoji not found'); 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 Emojis.delete(emoji.id);
await getConnection().queryResultCache!.remove(['meta_emojis']); await getConnection().queryResultCache!.remove(['meta_emojis']);
insertModerationLog(me, 'removeEmoji', {
emoji: emoji
});
}); });

View File

@ -1,5 +1,6 @@
import define from '../../../define'; import define from '../../../define';
import { destroy } from '../../../../../queue'; import { destroy } from '../../../../../queue';
import { insertModerationLog } from '../../../../../services/insert-moderation-log';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -10,8 +11,8 @@ export const meta = {
params: {} params: {}
}; };
export default define(meta, async (ps) => { export default define(meta, async (ps, me) => {
destroy(); destroy();
return; insertModerationLog(me, 'clearQueue');
}); });

View 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);
});

View File

@ -2,6 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { Users } from '../../../../models'; import { Users } from '../../../../models';
import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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); const user = await Users.findOne(ps.userId as string);
if (user == null) { if (user == null) {
@ -39,4 +40,8 @@ export default define(meta, async (ps) => {
await Users.update(user.id, { await Users.update(user.id, {
isSilenced: true isSilenced: true
}); });
insertModerationLog(me, 'silence', {
targetId: user.id,
});
}); });

View File

@ -4,6 +4,7 @@ import define from '../../define';
import deleteFollowing from '../../../../services/following/delete'; import deleteFollowing from '../../../../services/following/delete';
import { Users, Followings } from '../../../../models'; import { Users, Followings } from '../../../../models';
import { User } from '../../../../models/entities/user'; import { User } from '../../../../models/entities/user';
import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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); const user = await Users.findOne(ps.userId as string);
if (user == null) { if (user == null) {
@ -46,6 +47,10 @@ export default define(meta, async (ps) => {
isSuspended: true isSuspended: true
}); });
insertModerationLog(me, 'suspend', {
targetId: user.id,
});
unFollowAll(user); unFollowAll(user);
}); });

View File

@ -2,6 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { Users } from '../../../../models'; import { Users } from '../../../../models';
import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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); const user = await Users.findOne(ps.userId as string);
if (user == null) { if (user == null) {
@ -35,4 +36,8 @@ export default define(meta, async (ps) => {
await Users.update(user.id, { await Users.update(user.id, {
isSilenced: false isSilenced: false
}); });
insertModerationLog(me, 'unsilence', {
targetId: user.id,
});
}); });

View File

@ -2,6 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { Users } from '../../../../models'; import { Users } from '../../../../models';
import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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); const user = await Users.findOne(ps.userId as string);
if (user == null) { if (user == null) {
@ -35,4 +36,8 @@ export default define(meta, async (ps) => {
await Users.update(user.id, { await Users.update(user.id, {
isSuspended: false isSuspended: false
}); });
insertModerationLog(me, 'unsuspend', {
targetId: user.id,
});
}); });

View File

@ -2,6 +2,7 @@ import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { Meta } from '../../../../models/entities/meta'; import { Meta } from '../../../../models/entities/meta';
import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = { export const meta = {
desc: { 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>; const set = {} as Partial<Meta>;
if (ps.announcements) { if (ps.announcements) {
@ -653,4 +654,6 @@ export default define(meta, async (ps) => {
await transactionalEntityManager.save(Meta, set); await transactionalEntityManager.save(Meta, set);
} }
}); });
insertModerationLog(me, 'updateMeta');
}); });

View File

@ -1,6 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = { export const meta = {
tags: ['admin'], 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[] = []; const params: string[] = [];
if (ps.full) { if (ps.full) {
@ -30,4 +31,6 @@ export default define(meta, async (ps) => {
} }
getConnection().query('VACUUM ' + params.join(' ')); getConnection().query('VACUUM ' + params.join(' '));
insertModerationLog(me, 'vacuum', ps);
}); });

View 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 || {}
});
}