From 5318532a8d1208b4f286856e98ff199bfdeacb9c Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 25 Sep 2023 10:29:12 +0900 Subject: [PATCH] enhance: improve moderation log --- CHANGELOG.md | 4 +- locales/index.d.ts | 5 +- locales/ja-JP.yml | 5 +- .../backend/src/core/AnnouncementService.ts | 12 ++++- .../backend/src/core/CustomEmojiService.ts | 7 +-- packages/backend/src/core/DriveService.ts | 8 ++++ .../backend/src/core/NoteDeleteService.ts | 3 ++ packages/backend/src/core/RoleService.ts | 46 +++++++++++++++++- .../api/endpoints/admin/reset-password.ts | 4 +- .../api/endpoints/admin/roles/create.ts | 35 ++------------ .../api/endpoints/admin/roles/update.ts | 2 - .../api/endpoints/admin/suspend-user.ts | 4 +- .../api/endpoints/admin/unsuspend-user.ts | 4 +- .../api/endpoints/admin/update-user-note.ts | 2 + packages/backend/src/types.ts | 36 ++++++++++++-- .../src/pages/admin/modlog.ModLog.vue | 48 ++++++++++++++++--- packages/misskey-js/etc/misskey-js.api.md | 5 +- packages/misskey-js/src/consts.ts | 36 ++++++++++++-- packages/misskey-js/src/entities.ts | 3 ++ 19 files changed, 209 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d502957..ae5d90fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ ## (unreleased) ### General -- +- Enhance: モデレーションログ機能の強化 ### Client - @@ -22,7 +22,7 @@ ### Server - Fix: お知らせのページネーションが機能しない -## 2023.9.0 (unreleased) +## 2023.9.0 ### Note - meilisearchを使用する場合、v1.2以上が必要です diff --git a/locales/index.d.ts b/locales/index.d.ts index aa63c0385..5e24ecffa 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1123,6 +1123,7 @@ export interface Locale { "unnotifyNotes": string; "authentication": string; "authenticationRequiredToContinue": string; + "dateAndTime": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -2250,9 +2251,11 @@ export interface Locale { }; }; "_moderationLogTypes": { + "createRole": string; + "deleteRole": string; + "updateRole": string; "assignRole": string; "unassignRole": string; - "updateRole": string; "suspend": string; "unsuspend": string; "addCustomEmoji": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 69c48a5e6..1af73c620 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1120,6 +1120,7 @@ notifyNotes: "投稿を通知" unnotifyNotes: "投稿の通知を解除" authentication: "認証" authenticationRequiredToContinue: "続けるには認証を行ってください" +dateAndTime: "日時" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -2163,9 +2164,11 @@ _webhookSettings: mention: "メンションされたとき" _moderationLogTypes: + createRole: "ロールを作成" + deleteRole: "ロールを削除" + updateRole: "ロールを更新" assignRole: "ロールへアサイン" unassignRole: "ロールのアサイン解除" - updateRole: "ロール設定更新" suspend: "凍結" unsuspend: "凍結解除" addCustomEmoji: "カスタム絵文字追加" diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 2b4877788..ddacc0936 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; -import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead } from '@/models/_.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; @@ -23,6 +23,9 @@ export class AnnouncementService { @Inject(DI.announcementReadsRepository) private announcementReadsRepository: AnnouncementReadsRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private idService: IdService, private globalEventService: GlobalEventService, private moderationLogService: ModerationLogService, @@ -83,10 +86,13 @@ export class AnnouncementService { }); if (moderator) { + const user = await this.usersRepository.findOneByOrFail({ id: values.userId }); this.moderationLogService.log(moderator, 'createUserAnnouncement', { announcementId: announcement.id, announcement: announcement, userId: values.userId, + userUsername: user.username, + userHost: user.host, }); } } else { @@ -127,10 +133,14 @@ export class AnnouncementService { if (moderator) { if (announcement.userId) { + const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId }); this.moderationLogService.log(moderator, 'updateUserAnnouncement', { announcementId: announcement.id, before: announcement, after: after, + userId: announcement.userId, + userUsername: user.username, + userHost: user.host, }); } else { this.moderationLogService.log(moderator, 'updateGlobalAnnouncement', { diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index b14a8666e..1b545a124 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -134,11 +134,11 @@ export class CustomEmojiService implements OnApplicationShutdown { this.localEmojisCache.refresh(); - const updated = await this.emojiEntityService.packDetailed(emoji.id); + const packed = await this.emojiEntityService.packDetailed(emoji.id); if (emoji.name === data.name) { this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: [updated], + emojis: [packed], }); } else { this.globalEventService.publishBroadcastStream('emojiDeleted', { @@ -146,11 +146,12 @@ export class CustomEmojiService implements OnApplicationShutdown { }); this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: updated, + emoji: packed, }); } if (moderator) { + const updated = await this.emojisRepository.findOneByOrFail({ id: id }); this.moderationLogService.log(moderator, 'updateCustomEmoji', { emojiId: emoji.id, before: emoji, diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 366205f58..cecbec963 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -686,15 +686,20 @@ export class DriveService { if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) { if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) { + const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null; if (values.isSensitive) { this.moderationLogService.log(updater, 'markSensitiveDriveFile', { fileId: file.id, fileUserId: file.userId, + fileUserUsername: user?.username ?? null, + fileUserHost: user?.host ?? null, }); } else { this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', { fileId: file.id, fileUserId: file.userId, + fileUserUsername: user?.username ?? null, + fileUserHost: user?.host ?? null, }); } } @@ -795,9 +800,12 @@ export class DriveService { } if (deleter && await this.roleService.isModerator(deleter) && (file.userId !== deleter.id)) { + const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null; this.moderationLogService.log(deleter, 'deleteDriveFile', { fileId: file.id, fileUserId: file.userId, + fileUserUsername: user?.username ?? null, + fileUserHost: user?.host ?? null, }); } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index c99f92b9c..87979f22a 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -135,9 +135,12 @@ export class NoteDeleteService { }); if (deleter && (note.userId !== deleter.id)) { + const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); this.moderationLogService.log(deleter, 'deleteNote', { noteId: note.id, noteUserId: note.userId, + noteUserUsername: user.username, + noteUserHost: user.host, note: note, }); } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index dea6dc68c..934b7d676 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -412,10 +412,13 @@ export class RoleService implements OnApplicationShutdown { this.globalEventService.publishInternalEvent('userRoleAssigned', created); if (moderator) { + const user = await this.usersRepository.findOneByOrFail({ id: userId }); this.moderationLogService.log(moderator, 'assignRole', { roleId: roleId, roleName: role.name, userId: userId, + userUsername: user.username, + userHost: user.host, expiresAt: expiresAt ? expiresAt.toISOString() : null, }); } @@ -445,11 +448,16 @@ export class RoleService implements OnApplicationShutdown { this.globalEventService.publishInternalEvent('userRoleUnassigned', existing); if (moderator) { - const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); + const [user, role] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: userId }), + this.rolesRepository.findOneByOrFail({ id: roleId }), + ]); this.moderationLogService.log(moderator, 'unassignRole', { roleId: roleId, roleName: role.name, userId: userId, + userUsername: user.username, + userHost: user.host, }); } } @@ -473,6 +481,42 @@ export class RoleService implements OnApplicationShutdown { redisPipeline.exec(); } + @bindThis + public async create(values: Partial, moderator?: MiUser): Promise { + const date = new Date(); + const created = await this.rolesRepository.insert({ + id: this.idService.genId(), + createdAt: date, + updatedAt: date, + lastUsedAt: date, + name: values.name, + description: values.description, + color: values.color, + iconUrl: values.iconUrl, + target: values.target, + condFormula: values.condFormula, + isPublic: values.isPublic, + isAdministrator: values.isAdministrator, + isModerator: values.isModerator, + isExplorable: values.isExplorable, + asBadge: values.asBadge, + canEditMembersByModerator: values.canEditMembersByModerator, + displayOrder: values.displayOrder, + policies: values.policies, + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('roleCreated', created); + + if (moderator) { + this.moderationLogService.log(moderator, 'createRole', { + roleId: created.id, + role: created, + }); + } + + return created; + } + @bindThis public async update(role: MiRole, params: Partial, moderator?: MiUser): Promise { const date = new Date(); diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 6ce758327..13e9c30ed 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -73,7 +73,9 @@ export default class extends Endpoint { // eslint- }); this.moderationLogService.log(me, 'resetPassword', { - targetId: user.id, + userId: user.id, + userUsername: user.username, + userHost: user.host, }); return { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index f567b0d38..8451b1955 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -5,11 +5,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/_.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['admin', 'role'], @@ -58,37 +55,11 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.rolesRepository) - private rolesRepository: RolesRepository, - - private globalEventService: GlobalEventService, - private idService: IdService, private roleEntityService: RoleEntityService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const date = new Date(); - const created = await this.rolesRepository.insert({ - id: this.idService.genId(), - createdAt: date, - updatedAt: date, - lastUsedAt: date, - name: ps.name, - description: ps.description, - color: ps.color, - iconUrl: ps.iconUrl, - target: ps.target, - condFormula: ps.condFormula, - isPublic: ps.isPublic, - isAdministrator: ps.isAdministrator, - isModerator: ps.isModerator, - isExplorable: ps.isExplorable, - asBadge: ps.asBadge, - canEditMembersByModerator: ps.canEditMembersByModerator, - displayOrder: ps.displayOrder, - policies: ps.policies, - }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); - - this.globalEventService.publishInternalEvent('roleCreated', created); + const created = await this.roleService.create(ps, me); return await this.roleEntityService.pack(created, me); }); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index e4e59e487..6031e2363 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -79,9 +79,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRole); } - const date = new Date(); await this.roleService.update(role, { - updatedAt: date, name: ps.name, description: ps.description, color: ps.color, diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 89199f8bf..9464f4b67 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -61,7 +61,9 @@ export default class extends Endpoint { // eslint- }); this.moderationLogService.log(me, 'suspend', { - targetId: user.id, + userId: user.id, + userUsername: user.username, + userHost: user.host, }); (async () => { diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index a2779148e..5e523bbc3 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -46,7 +46,9 @@ export default class extends Endpoint { // eslint- }); this.moderationLogService.log(me, 'unsuspend', { - targetId: user.id, + userId: user.id, + userUsername: user.username, + userHost: user.host, }); this.userSuspendService.doPostUnsuspend(user); diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index 2e9fd5ad2..bfccc2a2a 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -51,6 +51,8 @@ export default class extends Endpoint { // eslint- this.moderationLogService.log(me, 'updateUserNote', { userId: user.id, + userUsername: user.username, + userHost: user.host, before: currentProfile.moderationNote, after: ps.text, }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 16654edd8..b458c0fbc 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -37,6 +37,7 @@ export const moderationLogTypes = [ 'deleteCustomEmoji', 'assignRole', 'unassignRole', + 'createRole', 'updateRole', 'deleteRole', 'clearQueue', @@ -62,13 +63,19 @@ export type ModerationLogPayloads = { after: any | null; }; suspend: { - targetId: string; + userId: string; + userUsername: string; + userHost: string | null; }; unsuspend: { - targetId: string; + userId: string; + userUsername: string; + userHost: string | null; }; updateUserNote: { userId: string; + userUsername: string; + userHost: string | null; before: string | null; after: string | null; }; @@ -87,15 +94,23 @@ export type ModerationLogPayloads = { }; assignRole: { userId: string; + userUsername: string; + userHost: string | null; roleId: string; roleName: string; expiresAt: string | null; }; unassignRole: { userId: string; + userUsername: string; + userHost: string | null; roleId: string; roleName: string; }; + createRole: { + roleId: string; + role: any; + }; updateRole: { roleId: string; before: any; @@ -110,10 +125,14 @@ export type ModerationLogPayloads = { deleteDriveFile: { fileId: string; fileUserId: string | null; + fileUserUsername: string | null; + fileUserHost: string | null; }; deleteNote: { noteId: string; noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; note: any; }; createGlobalAnnouncement: { @@ -124,6 +143,8 @@ export type ModerationLogPayloads = { announcementId: string; announcement: any; userId: string; + userUsername: string; + userHost: string | null; }; updateGlobalAnnouncement: { announcementId: string; @@ -134,6 +155,9 @@ export type ModerationLogPayloads = { announcementId: string; before: any; after: any; + userId: string; + userUsername: string; + userHost: string | null; }; deleteGlobalAnnouncement: { announcementId: string; @@ -144,7 +168,9 @@ export type ModerationLogPayloads = { announcement: any; }; resetPassword: { - targetId: string; + userId: string; + userUsername: string; + userHost: string | null; }; suspendRemoteInstance: { id: string; @@ -157,9 +183,13 @@ export type ModerationLogPayloads = { markSensitiveDriveFile: { fileId: string; fileUserId: string | null; + fileUserUsername: string | null; + fileUserHost: string | null; }; unmarkSensitiveDriveFile: { fileId: string; fileUserId: string | null; + fileUserUsername: string | null; + fileUserHost: string | null; }; }; diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index ce7b89f8f..f0de026ad 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -5,33 +5,61 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index b3806754a..f72532f29 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2550,6 +2550,9 @@ type ModerationLog = { } | { type: 'unassignRole'; info: ModerationLogPayloads['unassignRole']; +} | { + type: 'createRole'; + info: ModerationLogPayloads['createRole']; } | { type: 'updateRole'; info: ModerationLogPayloads['updateRole']; @@ -2604,7 +2607,7 @@ type ModerationLog = { }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile"]; // @public (undocumented) export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 63137dcc8..d2751c447 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -55,6 +55,7 @@ export const moderationLogTypes = [ 'deleteCustomEmoji', 'assignRole', 'unassignRole', + 'createRole', 'updateRole', 'deleteRole', 'clearQueue', @@ -80,13 +81,19 @@ export type ModerationLogPayloads = { after: any | null; }; suspend: { - targetId: string; + userId: string; + userUsername: string; + userHost: string | null; }; unsuspend: { - targetId: string; + userId: string; + userUsername: string; + userHost: string | null; }; updateUserNote: { userId: string; + userUsername: string; + userHost: string | null; before: string | null; after: string | null; }; @@ -105,15 +112,23 @@ export type ModerationLogPayloads = { }; assignRole: { userId: string; + userUsername: string; + userHost: string | null; roleId: string; roleName: string; expiresAt: string | null; }; unassignRole: { userId: string; + userUsername: string; + userHost: string | null; roleId: string; roleName: string; }; + createRole: { + roleId: string; + role: any; + }; updateRole: { roleId: string; before: any; @@ -128,10 +143,14 @@ export type ModerationLogPayloads = { deleteDriveFile: { fileId: string; fileUserId: string | null; + fileUserUsername: string | null; + fileUserHost: string | null; }; deleteNote: { noteId: string; noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; note: any; }; createGlobalAnnouncement: { @@ -142,6 +161,8 @@ export type ModerationLogPayloads = { announcementId: string; announcement: any; userId: string; + userUsername: string; + userHost: string | null; }; updateGlobalAnnouncement: { announcementId: string; @@ -152,6 +173,9 @@ export type ModerationLogPayloads = { announcementId: string; before: any; after: any; + userId: string; + userUsername: string; + userHost: string | null; }; deleteGlobalAnnouncement: { announcementId: string; @@ -162,7 +186,9 @@ export type ModerationLogPayloads = { announcement: any; }; resetPassword: { - targetId: string; + userId: string; + userUsername: string; + userHost: string | null; }; suspendRemoteInstance: { id: string; @@ -175,9 +201,13 @@ export type ModerationLogPayloads = { markSensitiveDriveFile: { fileId: string; fileUserId: string | null; + fileUserUsername: string | null; + fileUserHost: string | null; }; unmarkSensitiveDriveFile: { fileId: string; fileUserId: string | null; + fileUserUsername: string | null; + fileUserHost: string | null; }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index f377f1a5e..41c9bdef6 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -601,6 +601,9 @@ export type ModerationLog = { } | { type: 'unassignRole'; info: ModerationLogPayloads['unassignRole']; +} | { + type: 'createRole'; + info: ModerationLogPayloads['createRole']; } | { type: 'updateRole'; info: ModerationLogPayloads['updateRole'];