diff --git a/locales/index.d.ts b/locales/index.d.ts index 6a2790c9af..27c8a36842 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1403,7 +1403,7 @@ export interface Locale extends ILocale { */ "inputNewFileName": string; /** - * 新しいキャプションを入力してください + * Enter new alt text */ "inputNewDescription": string; /** @@ -2603,11 +2603,11 @@ export interface Locale extends ILocale { */ "description": string; /** - * キャプションを付ける + * Add alt text */ "describeFile": string; /** - * キャプションを入力 + * Enter alt text */ "enterFileDescription": string; /** @@ -4084,7 +4084,7 @@ export interface Locale extends ILocale { */ "windowRestore": string; /** - * キャプション + * Alt text */ "caption": string; /** @@ -10254,6 +10254,66 @@ export interface Locale extends ILocale { * Allowed quote posts from user */ "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": { /** @@ -11601,6 +11661,18 @@ export interface Locale extends ILocale { * Flash */ "flash": string; + /** + * Files removed + */ + "filesRemoved": string; + /** + * File imported + */ + "fileImported": string; + /** + * Failed to load note + */ + "cannotLoadNote": string; "_flash": { /** * Flash Content Hidden diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index cb258a4f5a..1f94e65809 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -162,7 +162,6 @@ export class NoteDeleteService { noteUserId: note.userId, noteUserUsername: user.username, noteUserHost: user.host, - note: note, }); } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 5843457676..1a47f56bc6 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -15,6 +15,7 @@ import type { Config } from '@/config.js'; import { ApiError } from '@/server/api/error.js'; import { Packed } from '@/misc/json-schema.js'; import { RoleService } from '@/core/RoleService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -96,6 +97,7 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, private signupService: SignupService, private instanceActorService: InstanceActorService, + private readonly moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, _me, token) => { const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; @@ -137,6 +139,13 @@ export default class extends Endpoint { // eslint- approved: true, }); + if (me) { + await this.moderationLogService.log(me, 'createAccount', { + userId: account.id, + userUsername: account.username, + }); + } + const res = await this.userEntityService.pack(account, account, { schema: 'MeDetailed', includeSecrets: true, diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index 747c9f48d0..8b4a450ccb 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { CacheService } from '@/core/CacheService.js'; export const meta = { tags: ['admin'], @@ -30,14 +32,23 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - + private readonly cacheService: CacheService, + private readonly moderationLogService: ModerationLogService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me) => { + const user = await this.cacheService.findUserById(ps.userId); const files = await this.driveFilesRepository.findBy({ 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) { this.driveService.deleteFile(file, false, me); } diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts index d420a929bd..9a7b3d5d62 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -25,9 +26,11 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, + private readonly moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - this.queueService.createCleanRemoteFilesJob(); + await this.moderationLogService.log(me, 'clearRemoteFiles', {}); + await this.queueService.createCleanRemoteFilesJob(); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index d612572e2e..f5d20366cf 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -29,7 +30,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - + private readonly moderationLogService: ModerationLogService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me) => { @@ -37,6 +38,10 @@ export default class extends Endpoint { // eslint- userId: IsNull(), }); + await this.moderationLogService.log(me, 'clearOwnerlessFiles', { + count: files.length, + }); + for (const file of files) { this.driveService.deleteFile(file); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index f4fc79bdb3..795b579041 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -32,8 +33,13 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, + private readonly moderationLogService: ModerationLogService, ) { 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'))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index 8e5f69c894..ee7706f31a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -3,9 +3,12 @@ * 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 { 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 = { secure: true, @@ -25,9 +28,16 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, + private readonly moderationLogService: ModerationLogService, + @Inject(DI.driveFilesRepository) + private readonly driveFilesRepository: DriveFilesRepository, ) { 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', { + fileName: file.name, + }); + await this.queueService.createImportCustomEmojisJob(me, ps.fileId); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index e78620eac1..066eb1c7d9 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -32,8 +33,13 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, + private readonly moderationLogService: ModerationLogService, ) { 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'))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 69fc8e0cb5..8980ef0c86 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -32,8 +33,13 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, + private readonly moderationLogService: ModerationLogService, ) { 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'))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 679a9f9c42..2510349210 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -34,8 +35,13 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, + private readonly moderationLogService: ModerationLogService, ) { 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); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index 4ba99faab7..a0205ae24a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -34,8 +35,13 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, + private readonly moderationLogService: ModerationLogService, ) { 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); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index 4a54c26009..89fd4be99c 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -30,7 +31,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - + private readonly moderationLogService: ModerationLogService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me) => { @@ -38,6 +39,11 @@ export default class extends Endpoint { // eslint- userHost: ps.host, }); + await this.moderationLogService.log(me, 'clearInstanceFiles', { + host: ps.host, + count: files.length, + }); + for (const file of files) { this.driveService.deleteFile(file); } diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 601c898f52..e5d85e1d57 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -35,6 +36,7 @@ export default class extends Endpoint { // eslint- private followingsRepository: FollowingsRepository, private queueService: QueueService, + private readonly moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const followings = await this.followingsRepository.findBy([ @@ -51,6 +53,10 @@ export default class extends Endpoint { // eslint- this.usersRepository.findOneByOrFail({ id: f.followeeId }), ]).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 }))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts index f64ba7f48a..194e793eda 100644 --- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts @@ -5,9 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; 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 { CacheService } from '@/core/CacheService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -28,20 +29,19 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private readonly usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) private readonly userProfilesRepository: UserProfilesRepository, - + private readonly moderationLogService: ModerationLogService, private readonly cacheService: CacheService, ) { 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'); - } + await this.moderationLogService.log(me, 'nsfwUser', { + userId: ps.userId, + userUsername: user.username, + userHost: user.host, + }); await this.userProfilesRepository.update(user.id, { alwaysMarkNsfw: true, diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 1d32c6cc00..63fe2988ee 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { PromoNotesRepository } from '@/models/_.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -46,7 +48,8 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.promoNotesRepository) private promoNotesRepository: PromoNotesRepository, - + private readonly moderationLogService: ModerationLogService, + private readonly cacheService: CacheService, private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { @@ -61,6 +64,14 @@ export default class extends Endpoint { // eslint- 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, + }); + await this.promoNotesRepository.insert({ noteId: note.id, expiresAt: new Date(ps.expiresAt), diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index 3d7bc4567e..129f69aca9 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; import { ApiError } from '../../../error.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -64,6 +65,7 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private relayService: RelayService, + private readonly moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { try { @@ -72,6 +74,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.invalidUrl); } + await this.moderationLogService.log(me, 'addRelay', { + inbox: ps.inbox, + }); + return await this.relayService.addRelay(ps.inbox); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts index 1f6e773cd4..292f21fde9 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -27,9 +28,13 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private relayService: RelayService, + private readonly moderationLogService: ModerationLogService, ) { 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); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index 7e6045049a..eed21c6576 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.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 = { tags: ['admin'], @@ -29,24 +32,32 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private roleService: RoleService, + private readonly usersRepository: UsersRepository, + private readonly cacheService: CacheService, + private readonly moderationLogService: ModerationLogService, + private readonly roleService: RoleService, + private readonly globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } + const user = await this.cacheService.findUserById(ps.userId); if (await this.roleService.isModerator(user)) { 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, { isSilenced: true, }); + + this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', { + id: user.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts index 26588365e1..52a0c076be 100644 --- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts @@ -5,8 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; 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 { CacheService } from '@/core/CacheService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -27,18 +29,19 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - + private readonly cacheService: CacheService, + private readonly moderationLogService: ModerationLogService, @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, + private readonly userProfilesRepository: UserProfilesRepository, ) { 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'); - } + await this.moderationLogService.log(me, 'unNsfwUser', { + userId: ps.userId, + userUsername: user.username, + userHost: user.host, + }); await this.userProfilesRepository.update(user.id, { alwaysMarkNsfw: false, diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index f92be0d8e0..9318943785 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -7,6 +7,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.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 = { tags: ['admin'], @@ -28,18 +31,27 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @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) => { - 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'); - } + await this.moderationLogService.log(me, 'unSilenceUser', { + userId: ps.userId, + userUsername: user.username, + userHost: user.host, + }); await this.usersRepository.update(user.id, { isSilenced: false, }); + + this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', { + id: user.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index 5b7d936b1c..271a44f8d5 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -78,7 +78,6 @@ export default class extends Endpoint { // eslint- flashId: flash.id, flashUserId: flash.userId, flashUserUsername: user.username, - flash, }); } }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index 28c8237761..9854358e3e 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -78,7 +78,6 @@ export default class extends Endpoint { // eslint- postId: post.id, postUserId: post.userId, postUserUsername: user.username, - post, }); } }); diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index 0ad7a3633a..c95f8ecf6b 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -79,7 +79,6 @@ export default class extends Endpoint { // eslint- pageId: page.id, pageUserId: page.userId, pageUserUsername: user.username, - page, }); } }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index b5d982e3a5..2c6ef731b8 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -136,6 +136,21 @@ export const moderationLogTypes = [ 'rejectQuotesUser', 'acceptQuotesInstance', 'rejectQuotesInstance', + 'clearUserFiles', + 'nsfwUser', + 'unNsfwUser', + 'silenceUser', + 'unSilenceUser', + 'createAccount', + 'clearRemoteFiles', + 'clearOwnerlessFiles', + 'updateCustomEmojis', + 'importCustomEmojis', + 'clearInstanceFiles', + 'severFollowRelations', + 'createPromo', + 'addRelay', + 'removeRelay', ] as const; export type ModerationLogPayloads = { @@ -224,7 +239,6 @@ export type ModerationLogPayloads = { noteUserId: string; noteUserUsername: string; noteUserHost: string | null; - note: any; }; createGlobalAnnouncement: { announcementId: string; @@ -407,19 +421,16 @@ export type ModerationLogPayloads = { pageId: string; pageUserId: string; pageUserUsername: string; - page: any; }; deleteFlash: { flashId: string; flashUserId: string; flashUserUsername: string; - flash: any; }; deleteGalleryPost: { postId: string; postUserId: string; postUserUsername: string; - post: any; }; acceptQuotesUser: { userId: string, @@ -439,6 +450,70 @@ export type ModerationLogPayloads = { id: 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; + clearOwnerlessFiles: { + count: number; + }; + updateCustomEmojis: { + ids: string[], + category?: string | null, + license?: string | null, + setAliases?: string[], + addAliases?: string[], + delAliases?: string[], + }, + importCustomEmojis: { + fileName: string, + }, + clearInstanceFiles: { + host: string; + count: number; + }, + severFollowRelations: { + host: string; + }, + createPromo: { + noteId: string, + noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; + }, + addRelay: { + inbox: string; + }, + removeRelay: { + inbox: string; + }, }; export type Serialized = { diff --git a/packages/frontend/src/components/DynamicNote.vue b/packages/frontend/src/components/DynamicNote.vue new file mode 100644 index 0000000000..6703099591 --- /dev/null +++ b/packages/frontend/src/components/DynamicNote.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/frontend/src/components/SkFetchNote.vue b/packages/frontend/src/components/SkFetchNote.vue new file mode 100644 index 0000000000..ab702c28f8 --- /dev/null +++ b/packages/frontend/src/components/SkFetchNote.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue index f35932ae77..29908f303d 100644 --- a/packages/frontend/src/components/global/MkLazy.vue +++ b/packages/frontend/src/components/global/MkLazy.vue @@ -16,10 +16,20 @@ import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } fr const rootEl = shallowRef(); const showing = ref(false); +const emit = defineEmits<{ + (ev: 'show'): void, +}>(); + const observer = new IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting)) { showing.value = true; + + // Disconnect to avoid observer soft-leaks + observer.disconnect(); + + // Notify containing element to trigger edge logic + emit('show'); } }, ); diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 306a873173..9ce6499e2d 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only 'createAvatarDecoration', 'createSystemWebhook', 'createAbuseReportNotificationRecipient', + 'createAccount', + 'importCustomEmojis', + 'createPromo', + 'addRelay', ].includes(log.type), [$style.logYellow]: [ 'markSensitiveDriveFile', @@ -30,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only 'acceptRemoteInstanceReports', 'rejectQuotesUser', 'acceptQuotesUser', + 'nsfwUser', + 'unNsfwUser', + 'silenceUser', + 'unSilenceUser', + 'updateCustomEmojis', ].includes(log.type), [$style.logRed]: [ 'suspend', @@ -49,6 +58,12 @@ SPDX-License-Identifier: AGPL-3.0-only 'deletePage', 'deleteFlash', 'deleteGalleryPost', + 'clearUserFiles', + 'clearRemoteFiles', + 'clearOwnerlessFiles', + 'clearInstanceFiles', + 'severFollowRelations', + 'removeRelay', ].includes(log.type) }" >{{ i18n.ts._moderationLogTypes[log.type] }} @@ -100,6 +115,19 @@ SPDX-License-Identifier: AGPL-3.0-only : @{{ log.info.pageUserUsername }} : @{{ log.info.flashUserUsername }} : @{{ log.info.postUserUsername }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }} + : {{ log.info.count }} + : {{ log.info.fileName }} + : {{ log.info.host }} + : {{ log.info.host }} + : @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }} + : {{ log.info.inbox }} + : {{ log.info.inbox }} + + + + + + + + + + + + +
raw @@ -220,6 +289,7 @@ import { CodeDiff } from 'v-code-diff'; import JSON5 from 'json5'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; +import SkFetchNote from '@/components/SkFetchNote.vue'; const props = defineProps<{ log: Misskey.entities.ModerationLog; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 24a5294c91..e5d4bb6143 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2728,6 +2728,51 @@ type ModerationLog = { } | { type: '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) diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index b520c05d8e..0ce066182d 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -4,12 +4,8 @@ import type { Ad, Announcement, EmojiDetailed, - Flash, - GalleryPost, InviteCode, MetaDetailed, - Note, - Page, Role, ReversiGameDetailed, SystemWebhook, @@ -295,7 +291,6 @@ export type ModerationLogPayloads = { noteUserId: string; noteUserUsername: string; noteUserHost: string | null; - note: Note; }; createGlobalAnnouncement: { announcementId: string; @@ -478,19 +473,16 @@ export type ModerationLogPayloads = { pageId: string; pageUserId: string; pageUserUsername: string; - page: Page; }; deleteFlash: { flashId: string; flashUserId: string; flashUserUsername: string; - flash: Flash; }; deleteGalleryPost: { postId: string; postUserId: string; postUserUsername: string; - post: GalleryPost; }; acceptQuotesUser: { userId: string, @@ -510,4 +502,69 @@ export type ModerationLogPayloads = { id: 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; + clearOwnerlessFiles: { + count: number; + }; + updateCustomEmojis: { + ids: string[], + category?: string | null, + license?: string | null, + setAliases?: string[], + addAliases?: string[], + delAliases?: string[], + }; + importCustomEmojis: { + fileName: string, + }; + clearInstanceFiles: { + host: string; + count: number; + }; + severFollowRelations: { + host: string; + }; + createPromo: { + noteId: string, + noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; + }; + addRelay: { + inbox: string; + }; + removeRelay: { + inbox: string; + }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 7b5d56f01c..3b31a6e531 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -228,6 +228,51 @@ export type ModerationLog = { } | { type: '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 = { diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index ca454d2272..9a41a9d762 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -169,6 +169,9 @@ pinnedOnly: "Pinned" blockingYou: "Blocking you" warnExternalUrl: "Show warning when opening external URLs" flash: "Flash" +filesRemoved: "Files removed" +fileImported: "File imported" +cannotLoadNote: "Failed to load note" _flash: contentHidden: "Flash Content Hidden" poweredByRuffle: "Powered by Ruffle." @@ -320,6 +323,22 @@ _moderationLogTypes: acceptRemoteInstanceReports: "Accepted reports from remote instance" rejectQuotesUser: "Blocked/Stripped 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: 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."