enhance(backend): refine moderation log (#10939)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update DriveService.ts
This commit is contained in:
parent
ba6e85482e
commit
9e4d3ebe5f
32 changed files with 563 additions and 39 deletions
|
@ -28,6 +28,7 @@
|
||||||
- Feat: 二要素認証でパスキーをサポートするようになりました
|
- Feat: 二要素認証でパスキーをサポートするようになりました
|
||||||
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
|
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
|
||||||
- Feat: プロフィールでのリンク検証
|
- Feat: プロフィールでのリンク検証
|
||||||
|
- Feat: モデレーションログ機能
|
||||||
- Feat: 通知をテストできるようになりました
|
- Feat: 通知をテストできるようになりました
|
||||||
- Feat: PWAのアイコンが設定できるようになりました
|
- Feat: PWAのアイコンが設定できるようになりました
|
||||||
- Enhance: サーバー名の略称が設定できるようになりました
|
- Enhance: サーバー名の略称が設定できるようになりました
|
||||||
|
|
15
locales/index.d.ts
vendored
15
locales/index.d.ts
vendored
|
@ -421,6 +421,7 @@ export interface Locale {
|
||||||
"moderation": string;
|
"moderation": string;
|
||||||
"moderationNote": string;
|
"moderationNote": string;
|
||||||
"addModerationNote": string;
|
"addModerationNote": string;
|
||||||
|
"moderationLogs": string;
|
||||||
"nUsersMentioned": string;
|
"nUsersMentioned": string;
|
||||||
"securityKeyAndPasskey": string;
|
"securityKeyAndPasskey": string;
|
||||||
"securityKey": string;
|
"securityKey": string;
|
||||||
|
@ -2248,6 +2249,20 @@ export interface Locale {
|
||||||
"mention": string;
|
"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: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -418,6 +418,7 @@ moderator: "モデレーター"
|
||||||
moderation: "モデレーション"
|
moderation: "モデレーション"
|
||||||
moderationNote: "モデレーションノート"
|
moderationNote: "モデレーションノート"
|
||||||
addModerationNote: "モデレーションノートを追加する"
|
addModerationNote: "モデレーションノートを追加する"
|
||||||
|
moderationLogs: "モデログ"
|
||||||
nUsersMentioned: "{n}人が投稿"
|
nUsersMentioned: "{n}人が投稿"
|
||||||
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
||||||
securityKey: "セキュリティキー"
|
securityKey: "セキュリティキー"
|
||||||
|
@ -2160,3 +2161,17 @@ _webhookSettings:
|
||||||
renote: "Renoteされたとき"
|
renote: "Renoteされたとき"
|
||||||
reaction: "リアクションがあったとき"
|
reaction: "リアクションがあったとき"
|
||||||
mention: "メンションされたとき"
|
mention: "メンションされたとき"
|
||||||
|
|
||||||
|
_moderationLogTypes:
|
||||||
|
assignRole: "ロールへアサイン"
|
||||||
|
unassignRole: "ロールのアサイン解除"
|
||||||
|
updateRole: "ロール設定更新"
|
||||||
|
suspend: "凍結"
|
||||||
|
unsuspend: "凍結解除"
|
||||||
|
addCustomEmoji: "カスタム絵文字追加"
|
||||||
|
updateServerSettings: "サーバー設定更新"
|
||||||
|
updateUserNote: "モデレーションノート更新"
|
||||||
|
deleteDriveFile: "ファイルを削除"
|
||||||
|
deleteNote: "ノートを削除"
|
||||||
|
createGlobalAnnouncement: "全体のお知らせを作成"
|
||||||
|
createUserAnnouncement: "ユーザーへお知らせを作成"
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AnnouncementService {
|
export class AnnouncementService {
|
||||||
|
@ -24,6 +25,7 @@ export class AnnouncementService {
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ export class AnnouncementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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({
|
const announcement = await this.announcementsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
@ -79,10 +81,21 @@ export class AnnouncementService {
|
||||||
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
|
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
|
||||||
announcement: packed,
|
announcement: packed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.moderationLogService.log(moderator, 'createUserAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
announcement: announcement,
|
||||||
|
userId: values.userId,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.globalEventService.publishBroadcastStream('announcementCreated', {
|
this.globalEventService.publishBroadcastStream('announcementCreated', {
|
||||||
announcement: packed,
|
announcement: packed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.moderationLogService.log(moderator, 'createGlobalAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
announcement: announcement,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
|
@ -119,6 +120,7 @@ export class DriveService {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
private driveChart: DriveChart,
|
private driveChart: DriveChart,
|
||||||
private perUserDriveChart: PerUserDriveChart,
|
private perUserDriveChart: PerUserDriveChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
|
@ -648,7 +650,7 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteFile(file: MiDriveFile, isExpired = false) {
|
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
if (file.storedInternal) {
|
if (file.storedInternal) {
|
||||||
this.internalStorageService.del(file.accessKey!);
|
this.internalStorageService.del(file.accessKey!);
|
||||||
|
|
||||||
|
@ -671,11 +673,11 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deletePostProcess(file, isExpired);
|
this.deletePostProcess(file, isExpired, deleter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteFileSync(file: MiDriveFile, isExpired = false) {
|
public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
if (file.storedInternal) {
|
if (file.storedInternal) {
|
||||||
this.internalStorageService.del(file.accessKey!);
|
this.internalStorageService.del(file.accessKey!);
|
||||||
|
|
||||||
|
@ -702,11 +704,11 @@ export class DriveService {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deletePostProcess(file, isExpired);
|
this.deletePostProcess(file, isExpired, deleter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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) {
|
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||||
this.driveFilesRepository.update(file.id, {
|
this.driveFilesRepository.update(file.id, {
|
||||||
|
@ -733,6 +735,17 @@ export class DriveService {
|
||||||
this.instanceChart.updateDrive(file, false);
|
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
|
@bindThis
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ModerationLogService {
|
export class ModerationLogService {
|
||||||
|
@ -21,13 +22,13 @@ export class ModerationLogService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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({
|
await this.moderationLogsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: moderator.id,
|
userId: moderator.id,
|
||||||
type: type,
|
type: type,
|
||||||
info: info ?? {},
|
info: (info as any) ?? {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteDeleteService {
|
export class NoteDeleteService {
|
||||||
|
@ -48,6 +49,7 @@ export class NoteDeleteService {
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
private notesChart: NotesChart,
|
private notesChart: NotesChart,
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
|
@ -58,7 +60,7 @@ export class NoteDeleteService {
|
||||||
* @param user 投稿者
|
* @param user 投稿者
|
||||||
* @param note 投稿
|
* @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 deletedAt = new Date();
|
||||||
const cascadingNotes = await this.findCascadingNotes(note);
|
const cascadingNotes = await this.findCascadingNotes(note);
|
||||||
|
|
||||||
|
@ -131,6 +133,14 @@ export class NoteDeleteService {
|
||||||
id: note.id,
|
id: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (deleter && (note.userId !== deleter.id)) {
|
||||||
|
this.moderationLogService.log(deleter, 'deleteNote', {
|
||||||
|
noteId: note.id,
|
||||||
|
noteUserId: note.userId,
|
||||||
|
note: note,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -98,6 +99,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
@ -374,9 +376,11 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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 now = new Date();
|
||||||
|
|
||||||
|
const role = await this.rolesRepository.findOneByOrFail({ id: roleId });
|
||||||
|
|
||||||
const existing = await this.roleAssignmentsRepository.findOneBy({
|
const existing = await this.roleAssignmentsRepository.findOneBy({
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
@ -406,10 +410,19 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
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
|
@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 now = new Date();
|
||||||
|
|
||||||
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
||||||
|
@ -430,6 +443,15 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
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
|
@bindThis
|
||||||
|
@ -451,6 +473,26 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
redisPipeline.exec();
|
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
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
forExistingUsers: ps.forExistingUsers,
|
forExistingUsers: ps.forExistingUsers,
|
||||||
needConfirmationToRead: ps.needConfirmationToRead,
|
needConfirmationToRead: ps.needConfirmationToRead,
|
||||||
userId: ps.userId,
|
userId: ps.userId,
|
||||||
});
|
}, me);
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
});
|
});
|
||||||
|
|
|
@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
|
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
this.moderationLogService.log(me, 'addCustomEmoji', {
|
||||||
emojiId: emoji.id,
|
emojiId: emoji.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
this.queueService.destroy();
|
this.queueService.destroy();
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'clearQueue');
|
this.moderationLogService.log(me, 'clearQueue');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'promoteQueue');
|
this.moderationLogService.log(me, 'promoteQueue');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return;
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.roleService.unassign(user.id, role.id);
|
await this.roleService.unassign(user.id, role.id, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { RolesRepository } from '@/models/_.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin', 'role'],
|
tags: ['admin', 'role'],
|
||||||
|
@ -70,16 +71,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.rolesRepository)
|
@Inject(DI.rolesRepository)
|
||||||
private rolesRepository: RolesRepository,
|
private rolesRepository: RolesRepository,
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
|
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||||
if (!roleExist) {
|
if (role == null) {
|
||||||
throw new ApiError(meta.errors.noSuchRole);
|
throw new ApiError(meta.errors.noSuchRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
await this.rolesRepository.update(ps.roleId, {
|
await this.roleService.update(role, {
|
||||||
updatedAt: date,
|
updatedAt: date,
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
description: ps.description,
|
description: ps.description,
|
||||||
|
@ -95,9 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||||
displayOrder: ps.displayOrder,
|
displayOrder: ps.displayOrder,
|
||||||
policies: ps.policies,
|
policies: ps.policies,
|
||||||
});
|
}, me);
|
||||||
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
|
|
||||||
this.globalEventService.publishInternalEvent('roleUpdated', updated);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,8 @@ export const paramDef = {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { 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: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -78,6 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
|
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();
|
const reports = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
return await this.moderationLogEntityService.packMany(reports);
|
return await this.moderationLogEntityService.packMany(reports);
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
isSuspended: true,
|
isSuspended: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'suspend', {
|
this.moderationLogService.log(me, 'suspend', {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'unsuspend', {
|
this.moderationLogService.log(me, 'unsuspend', {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -441,8 +441,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.manifestJsonOverride = ps.manifestJsonOverride;
|
set.manifestJsonOverride = ps.manifestJsonOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
|
||||||
|
const after = await this.metaService.fetch(true);
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'updateServerSettings', {
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -32,6 +33,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
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');
|
throw new Error('user not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
await this.userProfilesRepository.update({ userId: user.id }, {
|
await this.userProfilesRepository.update({ userId: user.id }, {
|
||||||
moderationNote: ps.text,
|
moderationNote: ps.text,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'updateUserNote', {
|
||||||
|
userId: user.id,
|
||||||
|
before: currentProfile.moderationNote,
|
||||||
|
after: ps.text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,11 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.accessDenied);
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete
|
await this.driveService.deleteFile(file, me);
|
||||||
await this.driveService.deleteFile(file);
|
|
||||||
|
|
||||||
// Publish fileDeleted event
|
|
||||||
this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,3 +26,82 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
|
||||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||||
|
|
||||||
export const ffVisibility = ['public', 'followers', 'private'] 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { genAidx } from '@/misc/id/aidx.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
import type { MockFunctionMetadata } from 'jest-mock';
|
import type { MockFunctionMetadata } from 'jest-mock';
|
||||||
|
@ -29,6 +30,7 @@ describe('AnnouncementService', () => {
|
||||||
let announcementsRepository: AnnouncementsRepository;
|
let announcementsRepository: AnnouncementsRepository;
|
||||||
let announcementReadsRepository: AnnouncementReadsRepository;
|
let announcementReadsRepository: AnnouncementReadsRepository;
|
||||||
let globalEventService: jest.Mocked<GlobalEventService>;
|
let globalEventService: jest.Mocked<GlobalEventService>;
|
||||||
|
let moderationLogService: jest.Mocked<ModerationLogService>;
|
||||||
|
|
||||||
function createUser(data: Partial<MiUser> = {}) {
|
function createUser(data: Partial<MiUser> = {}) {
|
||||||
const un = secureRndstr(16);
|
const un = secureRndstr(16);
|
||||||
|
@ -71,8 +73,11 @@ describe('AnnouncementService', () => {
|
||||||
publishMainStream: jest.fn(),
|
publishMainStream: jest.fn(),
|
||||||
publishBroadcastStream: jest.fn(),
|
publishBroadcastStream: jest.fn(),
|
||||||
};
|
};
|
||||||
}
|
} else if (token === ModerationLogService) {
|
||||||
if (typeof token === 'function') {
|
return {
|
||||||
|
log: jest.fn(),
|
||||||
|
};
|
||||||
|
} else if (typeof token === 'function') {
|
||||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||||
return new Mock();
|
return new Mock();
|
||||||
|
@ -87,6 +92,7 @@ describe('AnnouncementService', () => {
|
||||||
announcementsRepository = app.get<AnnouncementsRepository>(DI.announcementsRepository);
|
announcementsRepository = app.get<AnnouncementsRepository>(DI.announcementsRepository);
|
||||||
announcementReadsRepository = app.get<AnnouncementReadsRepository>(DI.announcementReadsRepository);
|
announcementReadsRepository = app.get<AnnouncementReadsRepository>(DI.announcementReadsRepository);
|
||||||
globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
|
globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
|
||||||
|
moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -155,10 +161,11 @@ describe('AnnouncementService', () => {
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
test('通常', async () => {
|
test('通常', async () => {
|
||||||
|
const me = await createUser();
|
||||||
const result = await announcementService.create({
|
const result = await announcementService.create({
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
});
|
}, me);
|
||||||
|
|
||||||
expect(result.raw.title).toBe('Title');
|
expect(result.raw.title).toBe('Title');
|
||||||
expect(result.packed.title).toBe('Title');
|
expect(result.packed.title).toBe('Title');
|
||||||
|
@ -166,15 +173,17 @@ describe('AnnouncementService', () => {
|
||||||
expect(globalEventService.publishBroadcastStream).toHaveBeenCalled();
|
expect(globalEventService.publishBroadcastStream).toHaveBeenCalled();
|
||||||
expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated');
|
expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated');
|
||||||
expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed);
|
expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed);
|
||||||
|
expect(moderationLogService.log).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ユーザー指定', async () => {
|
test('ユーザー指定', async () => {
|
||||||
|
const me = await createUser();
|
||||||
const user = await createUser();
|
const user = await createUser();
|
||||||
const result = await announcementService.create({
|
const result = await announcementService.create({
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
}, me);
|
||||||
|
|
||||||
expect(result.raw.title).toBe('Title');
|
expect(result.raw.title).toBe('Title');
|
||||||
expect(result.packed.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![0]).toBe(user.id);
|
||||||
expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated');
|
expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated');
|
||||||
expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed);
|
expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed);
|
||||||
|
expect(moderationLogService.log).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -145,6 +145,11 @@ const menuDef = $computed(() => [{
|
||||||
text: i18n.ts.abuseReports,
|
text: i18n.ts.abuseReports,
|
||||||
to: '/admin/abuses',
|
to: '/admin/abuses',
|
||||||
active: currentPage?.route.name === '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,
|
title: i18n.ts.settings,
|
||||||
|
|
57
packages/frontend/src/pages/admin/modlog.ModLog.vue
Normal file
57
packages/frontend/src/pages/admin/modlog.ModLog.vue
Normal file
|
@ -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>
|
67
packages/frontend/src/pages/admin/modlog.vue
Normal file
67
packages/frontend/src/pages/admin/modlog.vue
Normal file
|
@ -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>
|
|
@ -395,6 +395,10 @@ export const routes = [{
|
||||||
path: '/abuses',
|
path: '/abuses',
|
||||||
name: 'abuses',
|
name: 'abuses',
|
||||||
component: page(() => import('./pages/admin/abuses.vue')),
|
component: page(() => import('./pages/admin/abuses.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/modlog',
|
||||||
|
name: 'modlog',
|
||||||
|
component: page(() => import('./pages/admin/modlog.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
|
|
@ -2278,7 +2278,8 @@ declare namespace entities {
|
||||||
Invite,
|
Invite,
|
||||||
InviteLimit,
|
InviteLimit,
|
||||||
UserSorting,
|
UserSorting,
|
||||||
OriginType
|
OriginType,
|
||||||
|
ModerationLog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export { entities }
|
export { entities }
|
||||||
|
@ -2516,6 +2517,50 @@ type MessagingMessage = {
|
||||||
groupId: UserGroup['id'] | null;
|
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)
|
// @public (undocumented)
|
||||||
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
|
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: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: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/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
|
// 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)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -44,3 +44,82 @@ export const permissions = [
|
||||||
'read:flash-likes',
|
'read:flash-likes',
|
||||||
'write: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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ModerationLogPayloads } from './consts.js';
|
||||||
|
|
||||||
export type ID = string;
|
export type ID = string;
|
||||||
export type DateString = string;
|
export type DateString = string;
|
||||||
|
|
||||||
|
@ -566,3 +568,43 @@ export type UserSorting =
|
||||||
| '+updatedAt'
|
| '+updatedAt'
|
||||||
| '-updatedAt';
|
| '-updatedAt';
|
||||||
export type OriginType = 'combined' | 'local' | 'remote';
|
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'];
|
||||||
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const notificationTypes = consts.notificationTypes;
|
||||||
export const noteVisibilities = consts.noteVisibilities;
|
export const noteVisibilities = consts.noteVisibilities;
|
||||||
export const mutedNoteReasons = consts.mutedNoteReasons;
|
export const mutedNoteReasons = consts.mutedNoteReasons;
|
||||||
export const ffVisibility = consts.ffVisibility;
|
export const ffVisibility = consts.ffVisibility;
|
||||||
|
export const moderationLogTypes = consts.moderationLogTypes;
|
||||||
|
|
||||||
// api extractor not supported yet
|
// api extractor not supported yet
|
||||||
//export * as api from './api.js';
|
//export * as api from './api.js';
|
||||||
|
|
Loading…
Reference in a new issue