diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc99ee33fe..62810ebf44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@
 - Feat: 二要素認証でパスキーをサポートするようになりました
 - Feat: 指定したユーザーが投稿したときに通知できるようになりました
 - Feat: プロフィールでのリンク検証
+- Feat: モデレーションログ機能
 - Feat: 通知をテストできるようになりました
 - Feat: PWAのアイコンが設定できるようになりました
 - Enhance: サーバー名の略称が設定できるようになりました
diff --git a/locales/index.d.ts b/locales/index.d.ts
index da60550193..fd99f10b69 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -421,6 +421,7 @@ export interface Locale {
     "moderation": string;
     "moderationNote": string;
     "addModerationNote": string;
+    "moderationLogs": string;
     "nUsersMentioned": string;
     "securityKeyAndPasskey": string;
     "securityKey": string;
@@ -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;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 0869c0c455..b396014ee2 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -418,6 +418,7 @@ moderator: "モデレーター"
 moderation: "モデレーション"
 moderationNote: "モデレーションノート"
 addModerationNote: "モデレーションノートを追加する"
+moderationLogs: "モデログ"
 nUsersMentioned: "{n}人が投稿"
 securityKeyAndPasskey: "セキュリティキー・パスキー"
 securityKey: "セキュリティキー"
@@ -2160,3 +2161,17 @@ _webhookSettings:
     renote: "Renoteされたとき"
     reaction: "リアクションがあったとき"
     mention: "メンションされたとき"
+
+_moderationLogTypes:
+  assignRole: "ロールへアサイン"
+  unassignRole: "ロールのアサイン解除"
+  updateRole: "ロール設定更新"
+  suspend: "凍結"
+  unsuspend: "凍結解除"
+  addCustomEmoji: "カスタム絵文字追加"
+  updateServerSettings: "サーバー設定更新"
+  updateUserNote: "モデレーションノート更新"
+  deleteDriveFile: "ファイルを削除"
+  deleteNote: "ノートを削除"
+  createGlobalAnnouncement: "全体のお知らせを作成"
+  createUserAnnouncement: "ユーザーへお知らせを作成"
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index 70f37516a4..31fcb139ea 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -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 {
@@ -24,6 +25,7 @@ export class AnnouncementService {
 
 		private idService: IdService,
 		private globalEventService: GlobalEventService,
+		private moderationLogService: ModerationLogService,
 	) {
 	}
 
@@ -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(),
@@ -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 {
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index e015d3dc41..2ff062142c 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -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 */
@@ -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,
@@ -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!);
 
@@ -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!);
 
@@ -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, {
@@ -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
diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts
index b0e5b794d0..f7f9063d92 100644
--- a/packages/backend/src/core/ModerationLogService.ts
+++ b/packages/backend/src/core/ModerationLogService.ts
@@ -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 {
@@ -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) ?? {},
 		});
 	}
 }
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index 69fff36a02..c99f92b9cb 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -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 {
@@ -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,
@@ -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);
 
@@ -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
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 518f283695..39f21ecec4 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -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';
 
@@ -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);
 
@@ -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,
@@ -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 });
@@ -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
@@ -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);
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index c2f69bb159..262b36b9a4 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -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;
 		});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index 7bd920c312..fc297c4702 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -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,
 			});
 
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
index b61c580034..c9142e9885 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
@@ -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');
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts
index 8d16cddd00..0cba5b4e25 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts
@@ -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');
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts
index 9a005982d4..a0f3edd867 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts
@@ -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);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts
index 0a79296c05..4c27583111 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts
@@ -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);
 		});
 	}
 }
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 65fdb4b4b4..e4e59e487c 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -9,6 +9,7 @@ import type { RolesRepository } from '@/models/_.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '@/server/api/error.js';
+import { RoleService } from '@/core/RoleService.js';
 
 export const meta = {
 	tags: ['admin', 'role'],
@@ -70,16 +71,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.rolesRepository)
 		private rolesRepository: RolesRepository,
 
-		private globalEventService: GlobalEventService,
+		private roleService: RoleService,
 	) {
-		super(meta, paramDef, async (ps) => {
-			const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
-			if (!roleExist) {
+		super(meta, paramDef, async (ps, me) => {
+			const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
+			if (role == null) {
 				throw new ApiError(meta.errors.noSuchRole);
 			}
 
 			const date = new Date();
-			await this.rolesRepository.update(ps.roleId, {
+			await this.roleService.update(role, {
 				updatedAt: date,
 				name: ps.name,
 				description: ps.description,
@@ -95,9 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				canEditMembersByModerator: ps.canEditMembersByModerator,
 				displayOrder: ps.displayOrder,
 				policies: ps.policies,
-			});
-			const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
-			this.globalEventService.publishInternalEvent('roleUpdated', updated);
+			}, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
index d5f97ab149..f87a5a3574 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
@@ -62,6 +62,8 @@ export const paramDef = {
 		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 		sinceId: { type: 'string', format: 'misskey:id' },
 		untilId: { type: 'string', format: 'misskey:id' },
+		type: { type: 'string', nullable: true },
+		userId: { type: 'string', format: 'misskey:id', nullable: true },
 	},
 	required: [],
 } as const;
@@ -78,6 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		super(meta, paramDef, async (ps, me) => {
 			const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
 
+			if (ps.type != null) {
+				query.andWhere('report.type = :type', { type: ps.type });
+			}
+
+			if (ps.userId != null) {
+				query.andWhere('report.userId = :userId', { userId: ps.userId });
+			}
+
 			const reports = await query.limit(ps.limit).getMany();
 
 			return await this.moderationLogEntityService.packMany(reports);
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 bcf12fa4e8..89199f8bff 100644
--- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
@@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				isSuspended: true,
 			});
 
-			this.moderationLogService.insertModerationLog(me, 'suspend', {
+			this.moderationLogService.log(me, 'suspend', {
 				targetId: user.id,
 			});
 
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 59e89e15bd..a2779148ed 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				isSuspended: false,
 			});
 
-			this.moderationLogService.insertModerationLog(me, 'unsuspend', {
+			this.moderationLogService.log(me, 'unsuspend', {
 				targetId: user.id,
 			});
 
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index eabf1f306c..ea6ebdd1fe 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -441,8 +441,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.manifestJsonOverride = ps.manifestJsonOverride;
 			}
 
+			const before = await this.metaService.fetch(true);
+
 			await this.metaService.update(set);
-			this.moderationLogService.insertModerationLog(me, 'updateMeta');
+
+			const after = await this.metaService.fetch(true);
+
+			this.moderationLogService.log(me, 'updateServerSettings', {
+				before,
+				after,
+			});
 		});
 	}
 }
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 c86a43977e..2e9fd5ad29 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
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -32,6 +33,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 		@Inject(DI.userProfilesRepository)
 		private userProfilesRepository: UserProfilesRepository,
+
+		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy({ id: ps.userId });
@@ -40,9 +43,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new Error('user not found');
 			}
 
+			const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+
 			await this.userProfilesRepository.update({ userId: user.id }, {
 				moderationNote: ps.text,
 			});
+
+			this.moderationLogService.log(me, 'updateUserNote', {
+				userId: user.id,
+				before: currentProfile.moderationNote,
+				after: ps.text,
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
index d7fdc81cdb..7b67a31e08 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -65,11 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.accessDenied);
 			}
 
-			// Delete
-			await this.driveService.deleteFile(file);
-
-			// Publish fileDeleted event
-			this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id);
+			await this.driveService.deleteFile(file, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts
index 74062a58f5..55aaaf4f78 100644
--- a/packages/backend/src/server/api/endpoints/notes/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/delete.ts
@@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
-			await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note);
+			await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note, false, me);
 		});
 	}
 }
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 0a28d88d08..7946e66b82 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -26,3 +26,82 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
 export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
 
 export const ffVisibility = ['public', 'followers', 'private'] as const;
+
+export const moderationLogTypes = [
+	'updateServerSettings',
+	'suspend',
+	'unsuspend',
+	'updateUserNote',
+	'addCustomEmoji',
+	'assignRole',
+	'unassignRole',
+	'updateRole',
+	'deleteRole',
+	'clearQueue',
+	'promoteQueue',
+	'deleteDriveFile',
+	'deleteNote',
+	'createGlobalAnnouncement',
+	'createUserAnnouncement',
+] as const;
+
+export type ModerationLogPayloads = {
+	updateServerSettings: {
+		before: any | null;
+		after: any | null;
+	};
+	suspend: {
+		targetId: string;
+	};
+	unsuspend: {
+		targetId: string;
+	};
+	updateUserNote: {
+		userId: string;
+		before: string | null;
+		after: string | null;
+	};
+	addCustomEmoji: {
+		emojiId: string;
+	};
+	assignRole: {
+		userId: string;
+		roleId: string;
+		roleName: string;
+		expiresAt: string | null;
+	};
+	unassignRole: {
+		userId: string;
+		roleId: string;
+		roleName: string;
+	};
+	updateRole: {
+		roleId: string;
+		before: any;
+		after: any;
+	};
+	deleteRole: {
+		roleId: string;
+		roleName: string;
+	};
+	clearQueue: Record<string, never>;
+	promoteQueue: Record<string, never>;
+	deleteDriveFile: {
+		fileId: string;
+		fileUserId: string | null;
+	};
+	deleteNote: {
+		noteId: string;
+		noteUserId: string;
+		note: any;
+	};
+	createGlobalAnnouncement: {
+		announcementId: string;
+		announcement: any;
+	};
+	createUserAnnouncement: {
+		announcementId: string;
+		announcement: any;
+		userId: string;
+	};
+};
diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts
index 721fbb7345..8f61d91ba9 100644
--- a/packages/backend/test/unit/AnnouncementService.ts
+++ b/packages/backend/test/unit/AnnouncementService.ts
@@ -16,6 +16,7 @@ import { genAidx } from '@/misc/id/aidx.js';
 import { CacheService } from '@/core/CacheService.js';
 import { IdService } from '@/core/IdService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { secureRndstr } from '@/misc/secure-rndstr.js';
 import type { TestingModule } from '@nestjs/testing';
 import type { MockFunctionMetadata } from 'jest-mock';
@@ -29,6 +30,7 @@ describe('AnnouncementService', () => {
 	let announcementsRepository: AnnouncementsRepository;
 	let announcementReadsRepository: AnnouncementReadsRepository;
 	let globalEventService: jest.Mocked<GlobalEventService>;
+	let moderationLogService: jest.Mocked<ModerationLogService>;
 
 	function createUser(data: Partial<MiUser> = {}) {
 		const un = secureRndstr(16);
@@ -71,8 +73,11 @@ describe('AnnouncementService', () => {
 						publishMainStream: jest.fn(),
 						publishBroadcastStream: jest.fn(),
 					};
-				}
-				if (typeof token === 'function') {
+				} else if (token === ModerationLogService) {
+					return {
+						log: jest.fn(),
+					};
+				} else if (typeof token === 'function') {
 					const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
 					const Mock = moduleMocker.generateFromMetadata(mockMetadata);
 					return new Mock();
@@ -87,6 +92,7 @@ describe('AnnouncementService', () => {
 		announcementsRepository = app.get<AnnouncementsRepository>(DI.announcementsRepository);
 		announcementReadsRepository = app.get<AnnouncementReadsRepository>(DI.announcementReadsRepository);
 		globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
+		moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
 	});
 
 	afterEach(async () => {
@@ -155,10 +161,11 @@ describe('AnnouncementService', () => {
 
 	describe('create', () => {
 		test('通常', async () => {
+			const me = await createUser();
 			const result = await announcementService.create({
 				title: 'Title',
 				text: 'Text',
-			});
+			}, me);
 
 			expect(result.raw.title).toBe('Title');
 			expect(result.packed.title).toBe('Title');
@@ -166,15 +173,17 @@ describe('AnnouncementService', () => {
 			expect(globalEventService.publishBroadcastStream).toHaveBeenCalled();
 			expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated');
 			expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed);
+			expect(moderationLogService.log).toHaveBeenCalled();
 		});
 
 		test('ユーザー指定', async () => {
+			const me = await createUser();
 			const user = await createUser();
 			const result = await announcementService.create({
 				title: 'Title',
 				text: 'Text',
 				userId: user.id,
-			});
+			}, me);
 
 			expect(result.raw.title).toBe('Title');
 			expect(result.packed.title).toBe('Title');
@@ -184,6 +193,7 @@ describe('AnnouncementService', () => {
 			expect(globalEventService.publishMainStream.mock.lastCall![0]).toBe(user.id);
 			expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated');
 			expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed);
+			expect(moderationLogService.log).toHaveBeenCalled();
 		});
 	});
 
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index ab4e7620dd..944ba7b950 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -145,6 +145,11 @@ const menuDef = $computed(() => [{
 		text: i18n.ts.abuseReports,
 		to: '/admin/abuses',
 		active: currentPage?.route.name === 'abuses',
+	}, {
+		icon: 'ti ti-list-search',
+		text: i18n.ts.moderationLogs,
+		to: '/admin/modlog',
+		active: currentPage?.route.name === 'modlog',
 	}],
 }, {
 	title: i18n.ts.settings,
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
new file mode 100644
index 0000000000..3a474f73a8
--- /dev/null
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -0,0 +1,57 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkFolder>
+	<template #label>{{ i18n.ts._moderationLogTypes[log.type] }}</template>
+	<template #icon>
+		<MkAvatar :user="log.user" :class="$style.avatar"/>
+	</template>
+	<template #suffix>
+		<MkTime :time="log.createdAt" mode="detail"/>
+	</template>
+
+	<div :class="$style.root">
+		<div>{{ i18n.ts.moderator }}: {{ log.userId }}</div>
+
+		<template v-if="log.type === 'suspend'">
+			<div>{{ i18n.ts.user }}: {{ log.info.targetId }}</div>
+		</template>
+		<template v-else-if="log.type === 'unsuspend'">
+			<div>{{ i18n.ts.user }}: {{ log.info.targetId }}</div>
+		</template>
+		<template v-else-if="log.type === 'assignRole'">
+			<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
+			<div>{{ i18n.ts.role }}: {{ log.info.roleName }} [{{ log.info.roleId }}]</div>
+		</template>
+		<template v-else-if="log.type === 'unassignRole'">
+			<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
+			<div>{{ i18n.ts.role }}: {{ log.info.roleName }} [{{ log.info.roleId }}]</div>
+		</template>
+	</div>
+</MkFolder>
+</template>
+
+<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { dateString } from '@/filters/date.js';
+import MkFolder from '@/components/MkFolder.vue';
+
+const props = defineProps<{
+	log: Misskey.entities.ModerationLog;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+}
+
+.avatar {
+	width: 18px;
+	height: 18px;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
new file mode 100644
index 0000000000..da043f1b8f
--- /dev/null
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -0,0 +1,67 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :contentMax="900">
+		<div>
+			<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<MkSelect v-model="type" style="margin: 0; flex: 1;">
+					<template #label>{{ i18n.ts.type }}</template>
+					<option :value="null">{{ i18n.ts.all }}</option>
+					<option v-for="t in Misskey.moderationLogTypes" :key="t" :value="t">{{ t }}</option>
+				</MkSelect>
+				<MkInput v-model="moderatorId" style="margin: 0; flex: 1;">
+					<template #label>{{ i18n.ts.moderator }}(ID)</template>
+				</MkInput>
+			</div>
+
+			<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
+				<div class="_gaps_s">
+					<XModLog v-for="item in items" :key="item.id" :log="item"/>
+				</div>
+			</MkPagination>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import XHeader from './_header_.vue';
+import XModLog from './modlog.ModLog.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkPagination from '@/components/MkPagination.vue';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+let logs = $shallowRef<InstanceType<typeof MkPagination>>();
+
+let type = $ref(null);
+let moderatorId = $ref('');
+
+const pagination = {
+	endpoint: 'admin/show-moderation-logs' as const,
+	limit: 30,
+	params: computed(() => ({
+		type,
+		userId: moderatorId === '' ? null : moderatorId,
+	})),
+};
+
+console.log(Misskey);
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+	title: i18n.ts.moderationLogs,
+	icon: 'ti ti-list-search',
+});
+</script>
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index e658477bbc..415d2f1974 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -395,6 +395,10 @@ export const routes = [{
 		path: '/abuses',
 		name: 'abuses',
 		component: page(() => import('./pages/admin/abuses.vue')),
+	}, {
+		path: '/modlog',
+		name: 'modlog',
+		component: page(() => import('./pages/admin/modlog.vue')),
 	}, {
 		path: '/settings',
 		name: 'settings',
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 5cd679bce5..804733c066 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2278,7 +2278,8 @@ declare namespace entities {
         Invite,
         InviteLimit,
         UserSorting,
-        OriginType
+        OriginType,
+        ModerationLog
     }
 }
 export { entities }
@@ -2516,6 +2517,50 @@ type MessagingMessage = {
     groupId: UserGroup['id'] | null;
 };
 
+// @public (undocumented)
+type ModerationLog = {
+    id: ID;
+    createdAt: DateString;
+    userId: User['id'];
+    user: UserDetailed | null;
+} & ({
+    type: 'updateServerSettings';
+    info: ModerationLogPayloads['updateServerSettings'];
+} | {
+    type: 'suspend';
+    info: ModerationLogPayloads['suspend'];
+} | {
+    type: 'unsuspend';
+    info: ModerationLogPayloads['unsuspend'];
+} | {
+    type: 'updateUserNote';
+    info: ModerationLogPayloads['updateUserNote'];
+} | {
+    type: 'addCustomEmoji';
+    info: ModerationLogPayloads['addCustomEmoji'];
+} | {
+    type: 'assignRole';
+    info: ModerationLogPayloads['assignRole'];
+} | {
+    type: 'unassignRole';
+    info: ModerationLogPayloads['unassignRole'];
+} | {
+    type: 'updateRole';
+    info: ModerationLogPayloads['updateRole'];
+} | {
+    type: 'deleteRole';
+    info: ModerationLogPayloads['deleteRole'];
+} | {
+    type: 'clearQueue';
+    info: ModerationLogPayloads['clearQueue'];
+} | {
+    type: 'promoteQueue';
+    info: ModerationLogPayloads['promoteQueue'];
+});
+
+// @public (undocumented)
+export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "assignRole", "unassignRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement"];
+
 // @public (undocumented)
 export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
 
@@ -2861,6 +2906,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
 // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
 // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
 // src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
+// src/entities.ts:579:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
 // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index 6cf6dc07e7..346affc6a5 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -44,3 +44,82 @@ export const permissions = [
 	'read:flash-likes',
 	'write:flash-likes',
 ];
+
+export const moderationLogTypes = [
+	'updateServerSettings',
+	'suspend',
+	'unsuspend',
+	'updateUserNote',
+	'addCustomEmoji',
+	'assignRole',
+	'unassignRole',
+	'updateRole',
+	'deleteRole',
+	'clearQueue',
+	'promoteQueue',
+	'deleteDriveFile',
+	'deleteNote',
+	'createGlobalAnnouncement',
+	'createUserAnnouncement',
+] as const;
+
+export type ModerationLogPayloads = {
+	updateServerSettings: {
+		before: any | null;
+		after: any | null;
+	};
+	suspend: {
+		targetId: string;
+	};
+	unsuspend: {
+		targetId: string;
+	};
+	updateUserNote: {
+		userId: string;
+		before: string | null;
+		after: string | null;
+	};
+	addCustomEmoji: {
+		emojiId: string;
+	};
+	assignRole: {
+		userId: string;
+		roleId: string;
+		roleName: string;
+		expiresAt: string | null;
+	};
+	unassignRole: {
+		userId: string;
+		roleId: string;
+		roleName: string;
+	};
+	updateRole: {
+		roleId: string;
+		before: any;
+		after: any;
+	};
+	deleteRole: {
+		roleId: string;
+		roleName: string;
+	};
+	clearQueue: Record<string, never>;
+	promoteQueue: Record<string, never>;
+	deleteDriveFile: {
+		fileId: string;
+		fileUserId: string | null;
+	};
+	deleteNote: {
+		noteId: string;
+		noteUserId: string;
+		note: any;
+	};
+	createGlobalAnnouncement: {
+		announcementId: string;
+		announcement: any;
+	};
+	createUserAnnouncement: {
+		announcementId: string;
+		announcement: any;
+		userId: string;
+	};
+};
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 034201f9b9..0e6604cbaa 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -1,3 +1,5 @@
+import { ModerationLogPayloads } from './consts.js';
+
 export type ID = string;
 export type DateString = string;
 
@@ -566,3 +568,43 @@ export type UserSorting =
 	| '+updatedAt'
 	| '-updatedAt';
 export type OriginType = 'combined' | 'local' | 'remote';
+
+export type ModerationLog = {
+	id: ID;
+	createdAt: DateString;
+	userId: User['id'];
+	user: UserDetailed | null;
+} & ({
+	type: 'updateServerSettings';
+	info: ModerationLogPayloads['updateServerSettings'];
+} | {
+	type: 'suspend';
+	info: ModerationLogPayloads['suspend'];
+} | {
+	type: 'unsuspend';
+	info: ModerationLogPayloads['unsuspend'];
+} | {
+	type: 'updateUserNote';
+	info: ModerationLogPayloads['updateUserNote'];
+} | {
+	type: 'addCustomEmoji';
+	info: ModerationLogPayloads['addCustomEmoji'];
+} | {
+	type: 'assignRole';
+	info: ModerationLogPayloads['assignRole'];
+} | {
+	type: 'unassignRole';
+	info: ModerationLogPayloads['unassignRole'];
+} | {
+	type: 'updateRole';
+	info: ModerationLogPayloads['updateRole'];
+} | {
+	type: 'deleteRole';
+	info: ModerationLogPayloads['deleteRole'];
+} | {
+	type: 'clearQueue';
+	info: ModerationLogPayloads['clearQueue'];
+} | {
+	type: 'promoteQueue';
+	info: ModerationLogPayloads['promoteQueue'];
+});
diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts
index ae4dd31fe0..e78501fdfd 100644
--- a/packages/misskey-js/src/index.ts
+++ b/packages/misskey-js/src/index.ts
@@ -17,6 +17,7 @@ export const notificationTypes = consts.notificationTypes;
 export const noteVisibilities = consts.noteVisibilities;
 export const mutedNoteReasons = consts.mutedNoteReasons;
 export const ffVisibility = consts.ffVisibility;
+export const moderationLogTypes = consts.moderationLogTypes;
 
 // api extractor not supported yet
 //export * as api from './api.js';