Skip to content

Commit

Permalink
enhance(backend): refine moderation log (#10939)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update DriveService.ts
  • Loading branch information
syuilo authored Sep 23, 2023
1 parent ba6e854 commit 9e4d3eb
Show file tree
Hide file tree
Showing 32 changed files with 563 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- Feat: 二要素認証でパスキーをサポートするようになりました
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
- Feat: プロフィールでのリンク検証
- Feat: モデレーションログ機能
- Feat: 通知をテストできるようになりました
- Feat: PWAのアイコンが設定できるようになりました
- Enhance: サーバー名の略称が設定できるようになりました
Expand Down
15 changes: 15 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ export interface Locale {
"moderation": string;
"moderationNote": string;
"addModerationNote": string;
"moderationLogs": string;
"nUsersMentioned": string;
"securityKeyAndPasskey": string;
"securityKey": string;
Expand Down Expand Up @@ -2248,6 +2249,20 @@ export interface Locale {
"mention": string;
};
};
"_moderationLogTypes": {
"assignRole": string;
"unassignRole": string;
"updateRole": string;
"suspend": string;
"unsuspend": string;
"addCustomEmoji": string;
"updateServerSettings": string;
"updateUserNote": string;
"deleteDriveFile": string;
"deleteNote": string;
"createGlobalAnnouncement": string;
"createUserAnnouncement": string;
};
}
declare const locales: {
[lang: string]: Locale;
Expand Down
15 changes: 15 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ moderator: "モデレーター"
moderation: "モデレーション"
moderationNote: "モデレーションノート"
addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー"
Expand Down Expand Up @@ -2160,3 +2161,17 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

_moderationLogTypes:
assignRole: "ロールへアサイン"
unassignRole: "ロールのアサイン解除"
updateRole: "ロール設定更新"
suspend: "凍結"
unsuspend: "凍結解除"
addCustomEmoji: "カスタム絵文字追加"
updateServerSettings: "サーバー設定更新"
updateUserNote: "モデレーションノート更新"
deleteDriveFile: "ファイルを削除"
deleteNote: "ノートを削除"
createGlobalAnnouncement: "全体のお知らせを作成"
createUserAnnouncement: "ユーザーへお知らせを作成"
15 changes: 14 additions & 1 deletion packages/backend/src/core/AnnouncementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';

@Injectable()
export class AnnouncementService {
Expand All @@ -24,6 +25,7 @@ export class AnnouncementService {

private idService: IdService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) {
}

Expand Down Expand Up @@ -58,7 +60,7 @@ export class AnnouncementService {
}

@bindThis
public async create(values: Partial<MiAnnouncement>): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
public async create(values: Partial<MiAnnouncement>, moderator: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
const announcement = await this.announcementsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
Expand All @@ -79,10 +81,21 @@ export class AnnouncementService {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
announcement: packed,
});

this.moderationLogService.log(moderator, 'createUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
userId: values.userId,
});
} else {
this.globalEventService.publishBroadcastStream('announcementCreated', {
announcement: packed,
});

this.moderationLogService.log(moderator, 'createGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}

return {
Expand Down
23 changes: 18 additions & 5 deletions packages/backend/src/core/DriveService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';

type AddFileArgs = {
/** User who wish to add file */
Expand Down Expand Up @@ -119,6 +120,7 @@ export class DriveService {
private globalEventService: GlobalEventService,
private queueService: QueueService,
private roleService: RoleService,
private moderationLogService: ModerationLogService,
private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart,
Expand Down Expand Up @@ -648,7 +650,7 @@ export class DriveService {
}

@bindThis
public async deleteFile(file: MiDriveFile, isExpired = false) {
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!);

Expand All @@ -671,11 +673,11 @@ export class DriveService {
}
}

this.deletePostProcess(file, isExpired);
this.deletePostProcess(file, isExpired, deleter);
}

@bindThis
public async deleteFileSync(file: MiDriveFile, isExpired = false) {
public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!);

Expand All @@ -702,11 +704,11 @@ export class DriveService {
await Promise.all(promises);
}

this.deletePostProcess(file, isExpired);
this.deletePostProcess(file, isExpired, deleter);
}

@bindThis
private async deletePostProcess(file: MiDriveFile, isExpired = false) {
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, {
Expand All @@ -733,6 +735,17 @@ export class DriveService {
this.instanceChart.updateDrive(file, false);
}
}

if (file.userId) {
this.globalEventService.publishDriveStream(file.userId, 'fileDeleted', file.id);
}

if (deleter && await this.roleService.isModerator(deleter) && (file.userId !== deleter.id)) {
this.moderationLogService.log(deleter, 'deleteDriveFile', {
fileId: file.id,
fileUserId: file.userId,
});
}
}

@bindThis
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/core/ModerationLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';

@Injectable()
export class ModerationLogService {
Expand All @@ -21,13 +22,13 @@ export class ModerationLogService {
}

@bindThis
public async insertModerationLog(moderator: { id: MiUser['id'] }, type: string, info?: Record<string, any>) {
public async log<T extends typeof moderationLogTypes[number]>(moderator: { id: MiUser['id'] }, type: T, info?: ModerationLogPayloads[T]) {
await this.moderationLogsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: moderator.id,
type: type,
info: info ?? {},
info: (info as any) ?? {},
});
}
}
12 changes: 11 additions & 1 deletion packages/backend/src/core/NoteDeleteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';

@Injectable()
export class NoteDeleteService {
Expand All @@ -48,6 +49,7 @@ export class NoteDeleteService {
private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private searchService: SearchService,
private moderationLogService: ModerationLogService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart,
Expand All @@ -58,7 +60,7 @@ export class NoteDeleteService {
* @param user 投稿者
* @param note 投稿
*/
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false) {
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note);

Expand Down Expand Up @@ -131,6 +133,14 @@ export class NoteDeleteService {
id: note.id,
userId: user.id,
});

if (deleter && (note.userId !== deleter.id)) {
this.moderationLogService.log(deleter, 'deleteNote', {
noteId: note.id,
noteUserId: note.userId,
note: note,
});
}
}

@bindThis
Expand Down
46 changes: 44 additions & 2 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import type { OnApplicationShutdown } from '@nestjs/common';

Expand Down Expand Up @@ -98,6 +99,7 @@ export class RoleService implements OnApplicationShutdown {
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
private moderationLogService: ModerationLogService,
) {
//this.onMessage = this.onMessage.bind(this);

Expand Down Expand Up @@ -374,9 +376,11 @@ export class RoleService implements OnApplicationShutdown {
}

@bindThis
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null): Promise<void> {
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise<void> {
const now = new Date();

const role = await this.rolesRepository.findOneByOrFail({ id: roleId });

const existing = await this.roleAssignmentsRepository.findOneBy({
roleId: roleId,
userId: userId,
Expand Down Expand Up @@ -406,10 +410,19 @@ export class RoleService implements OnApplicationShutdown {
});

this.globalEventService.publishInternalEvent('userRoleAssigned', created);

if (moderator) {
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
expiresAt: expiresAt ? expiresAt.toISOString() : null,
});
}
}

@bindThis
public async unassign(userId: MiUser['id'], roleId: MiRole['id']): Promise<void> {
public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise<void> {
const now = new Date();

const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
Expand All @@ -430,6 +443,15 @@ export class RoleService implements OnApplicationShutdown {
});

this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);

if (moderator) {
const role = await this.rolesRepository.findOneByOrFail({ id: roleId });
this.moderationLogService.log(moderator, 'unassignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
});
}
}

@bindThis
Expand All @@ -451,6 +473,26 @@ export class RoleService implements OnApplicationShutdown {
redisPipeline.exec();
}

@bindThis
public async update(role: MiRole, params: Partial<MiRole>, moderator?: MiUser): Promise<void> {
const date = new Date();
await this.rolesRepository.update(role.id, {
updatedAt: date,
...params,
});

const updated = await this.rolesRepository.findOneByOrFail({ id: role.id });
this.globalEventService.publishInternalEvent('roleUpdated', updated);

if (moderator) {
this.moderationLogService.log(moderator, 'updateRole', {
roleId: role.id,
before: role,
after: updated,
});
}
}

@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
userId: ps.userId,
});
}, me);

return packed;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});

this.moderationLogService.insertModerationLog(me, 'addEmoji', {
this.moderationLogService.log(me, 'addCustomEmoji', {
emojiId: emoji.id,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
this.queueService.destroy();

this.moderationLogService.insertModerationLog(me, 'clearQueue');
this.moderationLogService.log(me, 'clearQueue');
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}

this.moderationLogService.insertModerationLog(me, 'promoteQueue');
this.moderationLogService.log(me, 'promoteQueue');
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return;
}

await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null);
await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null, me);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchUser);
}

await this.roleService.unassign(user.id, role.id);
await this.roleService.unassign(user.id, role.id, me);
});
}
}
Loading

0 comments on commit 9e4d3eb

Please sign in to comment.