add moderation logs for many endpoints

- `/admin/delete-all-files-of-a-user`
- `/admin/nsfw-user`
- `/admin/unnsfw-user`
- `/admin/silence-user`
- `/admin/unsilence-user`
- `/admin/accounts/create`
- `/admin/drive/clean-remote-files`
- `/admin/drive/cleanup`
- `/admin/emoji/set-category-bulk`
- `/admin/emoji/set-license-bulk`
- `/admin/emoji/set-aliases-bulk`
- `/admin/emoji/add-aliases-bulk`
- `/admin/emoji/remove-aliases-bulk`
- `/admin/emoji/import-zip`
- `/admin/federation/delete-all-files`
- `/admin/federation/remove-all-following`
- `/admin/promo/create`
- `/admin/relay/add`
- `/admin/relay/remove`
This commit is contained in:
Hazelnoot 2025-02-26 23:18:30 -05:00
parent 504e90c190
commit 27d43879a2
25 changed files with 480 additions and 43 deletions

72
locales/index.d.ts vendored
View file

@ -1403,7 +1403,7 @@ export interface Locale extends ILocale {
*/ */
"inputNewFileName": string; "inputNewFileName": string;
/** /**
* * Enter new alt text
*/ */
"inputNewDescription": string; "inputNewDescription": string;
/** /**
@ -2603,11 +2603,11 @@ export interface Locale extends ILocale {
*/ */
"description": string; "description": string;
/** /**
* * Add alt text
*/ */
"describeFile": string; "describeFile": string;
/** /**
* * Enter alt text
*/ */
"enterFileDescription": string; "enterFileDescription": string;
/** /**
@ -4084,7 +4084,7 @@ export interface Locale extends ILocale {
*/ */
"windowRestore": string; "windowRestore": string;
/** /**
* * Alt text
*/ */
"caption": string; "caption": string;
/** /**
@ -10254,6 +10254,66 @@ export interface Locale extends ILocale {
* Allowed quote posts from user * Allowed quote posts from user
*/ */
"allowQuotesUser": string; "allowQuotesUser": string;
/**
* Cleared a user's drive files
*/
"clearUserFiles": string;
/**
* Marked user as NSFW
*/
"nsfwUser": string;
/**
* Un-marked user as NSFW
*/
"unNsfwUser": string;
/**
* Silenced user
*/
"silenceUser": string;
/**
* Un-silenced user
*/
"unSilenceUser": string;
/**
* Created an account
*/
"createAccount": string;
/**
* Cleared remote drive files
*/
"clearRemoteFiles": string;
/**
* Cleared owner-less drive files
*/
"clearOwnerlessFiles": string;
/**
* Updated custom emojis
*/
"updateCustomEmojis": string;
/**
* Imported custom emojis
*/
"importCustomEmojis": string;
/**
* Cleared an instance's drive files
*/
"clearInstanceFiles": string;
/**
* Severed follow relations with an instance
*/
"severFollowRelations": string;
/**
* Created a note promo
*/
"createPromo": string;
/**
* Added a relay
*/
"addRelay": string;
/**
* Removed a relay
*/
"removeRelay": string;
}; };
"_fileViewer": { "_fileViewer": {
/** /**
@ -11601,6 +11661,10 @@ export interface Locale extends ILocale {
* Flash * Flash
*/ */
"flash": string; "flash": string;
/**
* Files removed:
*/
"filesRemoved": string;
"_flash": { "_flash": {
/** /**
* Flash Content Hidden * Flash Content Hidden

View file

@ -15,6 +15,7 @@ import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -96,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private signupService: SignupService, private signupService: SignupService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, _me, token) => { super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
@ -137,6 +139,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
approved: true, approved: true,
}); });
if (me) {
await this.moderationLogService.log(me, 'createAccount', {
userId: account.id,
userUsername: account.username,
});
}
const res = await this.userEntityService.pack(account, account, { const res = await this.userEntityService.pack(account, account, {
schema: 'MeDetailed', schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,

View file

@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -30,14 +32,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private driveService: DriveService, private driveService: DriveService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const user = await this.cacheService.findUserById(ps.userId);
const files = await this.driveFilesRepository.findBy({ const files = await this.driveFilesRepository.findBy({
userId: ps.userId, userId: ps.userId,
}); });
await this.moderationLogService.log(me, 'clearUserFiles', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
count: files.length,
});
for (const file of files) { for (const file of files) {
this.driveService.deleteFile(file, false, me); this.driveService.deleteFile(file, false, me);
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -25,9 +26,11 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private queueService: QueueService, private queueService: QueueService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
this.queueService.createCleanRemoteFilesJob(); await this.moderationLogService.log(me, 'clearRemoteFiles', {});
await this.queueService.createCleanRemoteFilesJob();
}); });
} }
} }

View file

@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.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'],
@ -29,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private readonly moderationLogService: ModerationLogService,
private driveService: DriveService, private driveService: DriveService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -37,6 +38,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: IsNull(), userId: IsNull(),
}); });
await this.moderationLogService.log(me, 'clearOwnerlessFiles', {
count: files.length,
});
for (const file of files) { for (const file of files) {
this.driveService.deleteFile(file); this.driveService.deleteFile(file);
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
addAliases: ps.aliases,
});
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC'))); await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
}); });
} }

View file

@ -3,9 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
secure: true, secure: true,
@ -25,9 +28,17 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private queueService: QueueService, private queueService: QueueService,
private readonly moderationLogService: ModerationLogService,
@Inject(DI.driveFilesRepository)
private readonly driveFilesRepository: DriveFilesRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
this.queueService.createImportCustomEmojisJob(me, ps.fileId); const file = await driveFilesRepository.findOneByOrFail({ id: ps.fileId });
await this.moderationLogService.log(me, 'importCustomEmojis', {
fileId: file.id,
fileName: file.name,
});
await this.queueService.createImportCustomEmojisJob(me, ps.fileId);
}); });
} }
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
delAliases: ps.aliases,
});
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC'))); await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
}); });
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
setAliases: ps.aliases,
});
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC'))); await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
}); });
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
category: ps.category,
});
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null); await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null);
}); });
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
license: ps.license,
});
await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null); await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
}); });
} }

View file

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.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'],
@ -30,7 +31,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private readonly moderationLogService: ModerationLogService,
private driveService: DriveService, private driveService: DriveService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -38,6 +39,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userHost: ps.host, userHost: ps.host,
}); });
await this.moderationLogService.log(me, 'clearInstanceFiles', {
host: ps.host,
count: files.length,
});
for (const file of files) { for (const file of files) {
this.driveService.deleteFile(file); this.driveService.deleteFile(file);
} }

View file

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -35,6 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
private queueService: QueueService, private queueService: QueueService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const followings = await this.followingsRepository.findBy([ const followings = await this.followingsRepository.findBy([
@ -51,6 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.usersRepository.findOneByOrFail({ id: f.followeeId }), this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }]))); ]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
await this.moderationLogService.log(me, 'severFollowRelations', {
host: ps.host,
});
this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true }))); this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true })));
}); });
} }

View file

@ -5,9 +5,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -28,20 +29,19 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.usersRepository)
private readonly usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private readonly userProfilesRepository: UserProfilesRepository, private readonly userProfilesRepository: UserProfilesRepository,
private readonly moderationLogService: ModerationLogService,
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
) { ) {
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.cacheService.findUserById(ps.userId);
if (user == null) { await this.moderationLogService.log(me, 'nsfwUser', {
throw new Error('user not found'); userId: ps.userId,
} userUsername: user.username,
userHost: user.host,
});
await this.userProfilesRepository.update(user.id, { await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: true, alwaysMarkNsfw: true,

View file

@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { PromoNotesRepository } from '@/models/_.js'; import type { PromoNotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -46,7 +48,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.promoNotesRepository) @Inject(DI.promoNotesRepository)
private promoNotesRepository: PromoNotesRepository, private promoNotesRepository: PromoNotesRepository,
private readonly moderationLogService: ModerationLogService,
private readonly cacheService: CacheService,
private getterService: GetterService, private getterService: GetterService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@ -61,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.alreadyPromoted); throw new ApiError(meta.errors.alreadyPromoted);
} }
const user = await this.cacheService.findUserById(note.userId);
await this.moderationLogService.log(me, 'createPromo', {
noteId: note.id,
noteUserId: user.id,
noteUserUsername: user.username,
noteUserHost: user.host,
note: note,
});
await this.promoNotesRepository.insert({ await this.promoNotesRepository.insert({
noteId: note.id, noteId: note.id,
expiresAt: new Date(ps.expiresAt), expiresAt: new Date(ps.expiresAt),

View file

@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -64,6 +65,7 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private relayService: RelayService, private relayService: RelayService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
try { try {
@ -72,6 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.invalidUrl); throw new ApiError(meta.errors.invalidUrl);
} }
await this.moderationLogService.log(me, 'addRelay', {
inbox: ps.inbox,
});
return await this.relayService.addRelay(ps.inbox); return await this.relayService.addRelay(ps.inbox);
}); });
} }

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -27,9 +28,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private relayService: RelayService, private relayService: RelayService,
private readonly moderationLogService: ModerationLogService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
return await this.relayService.removeRelay(ps.inbox); await this.moderationLogService.log(me, 'removeRelay', {
inbox: ps.inbox,
});
await this.relayService.removeRelay(ps.inbox);
}); });
} }
} }

View file

@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -29,24 +32,32 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private readonly usersRepository: UsersRepository,
private readonly cacheService: CacheService,
private roleService: RoleService, private readonly moderationLogService: ModerationLogService,
private readonly roleService: RoleService,
private readonly globalEventService: GlobalEventService,
) { ) {
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.cacheService.findUserById(ps.userId);
if (user == null) {
throw new Error('user not found');
}
if (await this.roleService.isModerator(user)) { if (await this.roleService.isModerator(user)) {
throw new Error('cannot silence moderator account'); throw new Error('cannot silence moderator account');
} }
await this.moderationLogService.log(me, 'silenceUser', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
});
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isSilenced: true, isSilenced: true,
}); });
this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
id: user.id,
});
}); });
} }
} }

View file

@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -27,18 +29,19 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.usersRepository) private readonly cacheService: CacheService,
private usersRepository: UsersRepository, private readonly moderationLogService: ModerationLogService,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private readonly userProfilesRepository: UserProfilesRepository,
) { ) {
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.cacheService.findUserById(ps.userId);
if (user == null) { await this.moderationLogService.log(me, 'unNsfwUser', {
throw new Error('user not found'); userId: ps.userId,
} userUsername: user.username,
userHost: user.host,
});
await this.userProfilesRepository.update(user.id, { await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: false, alwaysMarkNsfw: false,

View file

@ -7,6 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -28,18 +31,27 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private readonly usersRepository: UsersRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private readonly globalEventService: GlobalEventService,
) { ) {
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.cacheService.findUserById(ps.userId);
if (user == null) { await this.moderationLogService.log(me, 'unSilenceUser', {
throw new Error('user not found'); userId: ps.userId,
} userUsername: user.username,
userHost: user.host,
});
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isSilenced: false, isSilenced: false,
}); });
this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
id: user.id,
});
}); });
} }
} }

View file

@ -136,6 +136,21 @@ export const moderationLogTypes = [
'rejectQuotesUser', 'rejectQuotesUser',
'acceptQuotesInstance', 'acceptQuotesInstance',
'rejectQuotesInstance', 'rejectQuotesInstance',
'clearUserFiles',
'nsfwUser',
'unNsfwUser',
'silenceUser',
'unSilenceUser',
'createAccount',
'clearRemoteFiles',
'clearOwnerlessFiles',
'updateCustomEmojis',
'importCustomEmojis',
'clearInstanceFiles',
'severFollowRelations',
'createPromo',
'addRelay',
'removeRelay',
] as const; ] as const;
export type ModerationLogPayloads = { export type ModerationLogPayloads = {
@ -439,6 +454,72 @@ export type ModerationLogPayloads = {
id: string; id: string;
host: string; host: string;
}; };
clearUserFiles: {
userId: string;
userUsername: string;
userHost: string | null;
count: number;
};
nsfwUser: {
userId: string;
userUsername: string;
userHost: string | null;
};
unNsfwUser: {
userId: string;
userUsername: string;
userHost: string | null;
};
silenceUser: {
userId: string;
userUsername: string;
userHost: string | null;
};
unSilenceUser: {
userId: string;
userUsername: string;
userHost: string | null;
};
createAccount: {
userId: string;
userUsername: string;
};
clearRemoteFiles: Record<string, never>;
clearOwnerlessFiles: {
count: number;
};
updateCustomEmojis: {
ids: string[],
category?: string | null,
license?: string | null,
setAliases?: string[],
addAliases?: string[],
delAliases?: string[],
},
importCustomEmojis: {
fileId: string,
fileName: string,
},
clearInstanceFiles: {
host: string;
count: number;
},
severFollowRelations: {
host: string;
},
createPromo: {
noteId: string,
noteUserId: string;
noteUserUsername: string;
noteUserHost: string | null;
note: any;
},
addRelay: {
inbox: string;
},
removeRelay: {
inbox: string;
},
}; };
export type Serialized<T> = { export type Serialized<T> = {

View file

@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
'createAvatarDecoration', 'createAvatarDecoration',
'createSystemWebhook', 'createSystemWebhook',
'createAbuseReportNotificationRecipient', 'createAbuseReportNotificationRecipient',
'createAccount',
'importCustomEmojis',
'createPromo',
'addRelay',
].includes(log.type), ].includes(log.type),
[$style.logYellow]: [ [$style.logYellow]: [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
@ -30,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
'acceptRemoteInstanceReports', 'acceptRemoteInstanceReports',
'rejectQuotesUser', 'rejectQuotesUser',
'acceptQuotesUser', 'acceptQuotesUser',
'nsfwUser',
'unNsfwUser',
'silenceUser',
'unSilenceUser',
'updateCustomEmojis',
].includes(log.type), ].includes(log.type),
[$style.logRed]: [ [$style.logRed]: [
'suspend', 'suspend',
@ -49,6 +58,12 @@ SPDX-License-Identifier: AGPL-3.0-only
'deletePage', 'deletePage',
'deleteFlash', 'deleteFlash',
'deleteGalleryPost', 'deleteGalleryPost',
'clearUserFiles',
'clearRemoteFiles',
'clearOwnerlessFiles',
'clearInstanceFiles',
'severFollowRelations',
'removeRelay',
].includes(log.type) ].includes(log.type)
}" }"
>{{ i18n.ts._moderationLogTypes[log.type] }}</b> >{{ i18n.ts._moderationLogTypes[log.type] }}</b>
@ -100,6 +115,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span> <span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span> <span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span> <span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
<span v-else-if="log.type === 'clearUserFiles'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'nsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unNsfwUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'silenceUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unSilenceUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'createAccount'">: @{{ log.info.userUsername }}</span>
<span v-else-if="log.type === 'importCustomEmojis'">: {{ log.info.fileName }}</span>
<span v-else-if="log.type === 'clearInstanceFiles'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'severFollowRelations'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'addRelay'">: {{ log.info.inbox }}</span>
<span v-else-if="log.type === 'removeRelay'">: {{ log.info.inbox }}</span>
</template> </template>
<template v-if="log.user" #icon> <template v-if="log.user" #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/> <MkAvatar :user="log.user" :class="$style.avatar"/>
@ -205,6 +231,35 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/> <CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div> </div>
</template> </template>
<template v-else-if="log.type === 'clearUserFiles'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
<div>{{ i18n.ts.filesRemoved }}: {{ log.info.count }}</div>
</template>
<template v-else-if="log.type === 'nsfwUser'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>
<template v-else-if="log.type === 'unNsfwUser'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>
<template v-else-if="log.type === 'silenceUser'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>
<template v-else-if="log.type === 'unSilenceUser'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>
<template v-else-if="log.type === 'createAccount'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}</MkA></div>
</template>
<template v-else-if="log.type === 'clearOwnerlessFiles'">
<div>{{ i18n.ts.filesRemoved }}: {{ log.info.count }}</div>
</template>
<template v-else-if="log.type === 'clearInstanceFiles'">
<div>{{ i18n.ts.host }}: <MkA :to="`/instance-info/${log.info.host}`" class="_link">{{ log.info.host }}</MkA></div>
<div>{{ i18n.ts.filesRemoved }}: {{ log.info.count }}</div>
</template>
<template v-else-if="log.type === 'severFollowRelations'">
<div>{{ i18n.ts.host }}: <MkA :to="`/instance-info/${log.info.host}`" class="_link">{{ log.info.host }}</MkA></div>
</template>
<details> <details>
<summary>raw</summary> <summary>raw</summary>

View file

@ -2728,6 +2728,51 @@ type ModerationLog = {
} | { } | {
type: 'deleteGalleryPost'; type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost']; info: ModerationLogPayloads['deleteGalleryPost'];
} | {
type: 'clearUserFiles';
info: ModerationLogPayloads['clearUserFiles'];
} | {
type: 'nsfwUser';
info: ModerationLogPayloads['nsfwUser'];
} | {
type: 'unNsfwUser';
info: ModerationLogPayloads['unNsfwUser'];
} | {
type: 'silenceUser';
info: ModerationLogPayloads['silenceUser'];
} | {
type: 'unSilenceUser';
info: ModerationLogPayloads['unSilenceUser'];
} | {
type: 'createAccount';
info: ModerationLogPayloads['createAccount'];
} | {
type: 'clearRemoteFiles';
info: ModerationLogPayloads['clearRemoteFiles'];
} | {
type: 'clearOwnerlessFiles';
info: ModerationLogPayloads['clearOwnerlessFiles'];
} | {
type: 'updateCustomEmojis';
info: ModerationLogPayloads['updateCustomEmojis'];
} | {
type: 'importCustomEmojis';
info: ModerationLogPayloads['importCustomEmojis'];
} | {
type: 'clearInstanceFiles';
info: ModerationLogPayloads['clearInstanceFiles'];
} | {
type: 'severFollowRelations';
info: ModerationLogPayloads['severFollowRelations'];
} | {
type: 'createPromo';
info: ModerationLogPayloads['createPromo'];
} | {
type: 'addRelay';
info: ModerationLogPayloads['addRelay'];
} | {
type: 'removeRelay';
info: ModerationLogPayloads['removeRelay'];
}); });
// @public (undocumented) // @public (undocumented)

View file

@ -228,6 +228,51 @@ export type ModerationLog = {
} | { } | {
type: 'deleteGalleryPost'; type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost']; info: ModerationLogPayloads['deleteGalleryPost'];
} | {
type: 'clearUserFiles';
info: ModerationLogPayloads['clearUserFiles'];
} | {
type: 'nsfwUser';
info: ModerationLogPayloads['nsfwUser'];
} | {
type: 'unNsfwUser';
info: ModerationLogPayloads['unNsfwUser'];
} | {
type: 'silenceUser';
info: ModerationLogPayloads['silenceUser'];
} | {
type: 'unSilenceUser';
info: ModerationLogPayloads['unSilenceUser'];
} | {
type: 'createAccount';
info: ModerationLogPayloads['createAccount'];
} | {
type: 'clearRemoteFiles';
info: ModerationLogPayloads['clearRemoteFiles'];
} | {
type: 'clearOwnerlessFiles';
info: ModerationLogPayloads['clearOwnerlessFiles'];
} | {
type: 'updateCustomEmojis';
info: ModerationLogPayloads['updateCustomEmojis'];
} | {
type: 'importCustomEmojis';
info: ModerationLogPayloads['importCustomEmojis'];
} | {
type: 'clearInstanceFiles';
info: ModerationLogPayloads['clearInstanceFiles'];
} | {
type: 'severFollowRelations';
info: ModerationLogPayloads['severFollowRelations'];
} | {
type: 'createPromo';
info: ModerationLogPayloads['createPromo'];
} | {
type: 'addRelay';
info: ModerationLogPayloads['addRelay'];
} | {
type: 'removeRelay';
info: ModerationLogPayloads['removeRelay'];
}); });
export type ServerStats = { export type ServerStats = {

View file

@ -169,6 +169,7 @@ pinnedOnly: "Pinned"
blockingYou: "Blocking you" blockingYou: "Blocking you"
warnExternalUrl: "Show warning when opening external URLs" warnExternalUrl: "Show warning when opening external URLs"
flash: "Flash" flash: "Flash"
filesRemoved: "Files removed:"
_flash: _flash:
contentHidden: "Flash Content Hidden" contentHidden: "Flash Content Hidden"
poweredByRuffle: "Powered by Ruffle." poweredByRuffle: "Powered by Ruffle."
@ -320,6 +321,22 @@ _moderationLogTypes:
acceptRemoteInstanceReports: "Accepted reports from remote instance" acceptRemoteInstanceReports: "Accepted reports from remote instance"
rejectQuotesUser: "Blocked/Stripped quote posts from user" rejectQuotesUser: "Blocked/Stripped quote posts from user"
allowQuotesUser: "Allowed quote posts from user" allowQuotesUser: "Allowed quote posts from user"
clearUserFiles: "Cleared a user's drive files"
nsfwUser: "Marked user as NSFW"
unNsfwUser: "Un-marked user as NSFW"
silenceUser: "Silenced user"
unSilenceUser: "Un-silenced user"
createAccount: "Created an account"
clearRemoteFiles: "Cleared remote drive files"
clearOwnerlessFiles: "Cleared owner-less drive files"
updateCustomEmojis: "Updated custom emojis"
importCustomEmojis: "Imported custom emojis"
clearInstanceFiles: "Cleared an instance's drive files"
severFollowRelations: "Severed follow relations with an instance"
createPromo: "Created a note promo"
addRelay: "Added a relay"
removeRelay: "Removed a relay"
_mfm: _mfm:
uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks" uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax." intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."