mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-23 05:23:09 +02:00
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…
Reference in a new issue