merge: upstream

This commit is contained in:
Mar0xy 2023-09-26 02:26:30 +02:00
commit 8595a325ce
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
145 changed files with 3208 additions and 1285 deletions

View file

@ -0,0 +1,10 @@
export class UserBlacklistAnntena1689325027964 {
name = 'UserBlacklistAnntena1689325027964'
async up(queryRunner) {
await queryRunner.query(`ALTER TYPE "antenna_src_enum" ADD VALUE 'users_blacklist' AFTER 'list'`);
}
async down(queryRunner) {
}
}

View file

@ -0,0 +1,11 @@
export class ShortName1695440131671 {
name = 'ShortName1695440131671'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "shortName" character varying(64)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "shortName"`);
}
}

View file

@ -0,0 +1,21 @@
export class MutingNotificationTypes1695605508898 {
name = 'MutingNotificationTypes1695605508898'
async up(queryRunner) {
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test', 'pollVote', 'groupInvited')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
}
}

View file

@ -70,15 +70,15 @@
"@fastify/multipart": "7.7.3",
"@fastify/static": "6.11.2",
"@fastify/view": "8.2.0",
"@nestjs/common": "10.2.5",
"@nestjs/core": "10.2.5",
"@nestjs/testing": "10.2.5",
"@nestjs/common": "10.2.6",
"@nestjs/core": "10.2.6",
"@nestjs/testing": "10.2.6",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.1.1",
"@sinonjs/fake-timers": "11.1.0",
"@smithy/node-http-handler": "2.1.5",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.86",
"@swc/core": "1.3.87",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "6.0.1",
@ -87,7 +87,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.11.2",
"bullmq": "4.11.4",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -178,7 +178,7 @@
"@simplewebauthn/typescript-types": "8.0.0",
"@swc/jest": "0.2.29",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.2",
"@types/archiver": "5.3.3",
"@types/bcryptjs": "2.4.4",
"@types/body-parser": "1.19.3",
"@types/cbor": "6.0.0",
@ -193,9 +193,9 @@
"@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.1",
"@types/ms": "0.7.31",
"@types/node": "20.6.3",
"@types/node": "20.6.4",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.10",
"@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0",
@ -221,7 +221,7 @@
"@typescript-eslint/parser": "6.7.2",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.49.0",
"eslint": "8.50.0",
"eslint-plugin-import": "2.28.1",
"execa": "8.0.1",
"jest": "29.7.0",

View file

@ -7,11 +7,12 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead } from '@/models/_.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable()
export class AnnouncementService {
@ -22,8 +23,12 @@ export class AnnouncementService {
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) {
}
@ -58,7 +63,7 @@ export class AnnouncementService {
}
@bindThis
public async create(values: Partial<MiAnnouncement>): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
const announcement = await this.announcementsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
@ -79,10 +84,28 @@ export class AnnouncementService {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
announcement: packed,
});
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: values.userId });
this.moderationLogService.log(moderator, 'createUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
userId: values.userId,
userUsername: user.username,
userHost: user.host,
});
}
} else {
this.globalEventService.publishBroadcastStream('announcementCreated', {
announcement: packed,
});
if (moderator) {
this.moderationLogService.log(moderator, 'createGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}
}
return {
@ -91,6 +114,63 @@ export class AnnouncementService {
};
}
@bindThis
public async update(announcement: MiAnnouncement, values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<void> {
await this.announcementsRepository.update(announcement.id, {
updatedAt: new Date(),
title: values.title,
text: values.text,
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
imageUrl: values.imageUrl || null,
display: values.display,
icon: values.icon,
forExistingUsers: values.forExistingUsers,
needConfirmationToRead: values.needConfirmationToRead,
isActive: values.isActive,
});
const after = await this.announcementsRepository.findOneByOrFail({ id: announcement.id });
if (moderator) {
if (announcement.userId) {
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
this.moderationLogService.log(moderator, 'updateUserAnnouncement', {
announcementId: announcement.id,
before: announcement,
after: after,
userId: announcement.userId,
userUsername: user.username,
userHost: user.host,
});
} else {
this.moderationLogService.log(moderator, 'updateGlobalAnnouncement', {
announcementId: announcement.id,
before: announcement,
after: after,
});
}
}
}
@bindThis
public async delete(announcement: MiAnnouncement, moderator?: MiUser): Promise<void> {
await this.announcementsRepository.delete(announcement.id);
if (moderator) {
if (announcement.userId) {
this.moderationLogService.log(moderator, 'deleteUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
} else {
this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}
}
}
@bindThis
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
try {

View file

@ -119,6 +119,12 @@ export class AntennaService implements OnApplicationShutdown {
return this.utilityService.getFullApAccount(username, host).toLowerCase();
});
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} else if (antenna.src === 'users_blacklist') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
});
if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
}
const keywords = antenna.keywords

View file

@ -52,6 +52,7 @@ import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
import { WebhookService } from './WebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
@ -179,6 +180,7 @@ const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisti
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
@ -309,6 +311,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserListService,
UserMutingService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
WebhookService,
UtilityService,
@ -432,6 +435,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserListService,
$UserMutingService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
$WebhookService,
$UtilityService,
@ -556,6 +560,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserListService,
UserMutingService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
WebhookService,
UtilityService,
@ -678,6 +683,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserListService,
$UserMutingService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
$WebhookService,
$UtilityService,

View file

@ -12,12 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiEmoji } from '@/models/Emoji.js';
import type { EmojisRepository, MiRole } from '@/models/_.js';
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@ -36,6 +37,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
private utilityService: UtilityService,
private idService: IdService,
private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
@ -66,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: boolean;
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}): Promise<MiEmoji> {
}, moderator?: MiUser): Promise<MiEmoji> {
const emoji = await this.emojisRepository.insert({
id: this.idService.genId(),
updatedAt: new Date(),
@ -89,6 +91,13 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id),
});
if (moderator) {
this.moderationLogService.log(moderator, 'addCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
}
return emoji;
@ -104,7 +113,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive?: boolean;
localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}): Promise<void> {
}, moderator?: MiUser): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
@ -125,11 +134,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.localEmojisCache.refresh();
const updated = await this.emojiEntityService.packDetailed(emoji.id);
const packed = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
emojis: [packed],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
@ -137,7 +146,16 @@ export class CustomEmojiService implements OnApplicationShutdown {
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
emoji: packed,
});
}
if (moderator) {
const updated = await this.emojisRepository.findOneByOrFail({ id: id });
this.moderationLogService.log(moderator, 'updateCustomEmoji', {
emojiId: emoji.id,
before: emoji,
after: updated,
});
}
}
@ -231,7 +249,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
}
@bindThis
public async delete(id: MiEmoji['id']) {
public async delete(id: MiEmoji['id'], moderator?: MiUser) {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
await this.emojisRepository.delete(emoji.id);
@ -241,16 +259,30 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
if (moderator) {
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
}
@bindThis
public async deleteBulk(ids: MiEmoji['id'][]) {
public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
}
this.localEmojisCache.refresh();

View file

@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
type AddFileArgs = {
/** User who wish to add file */
@ -86,6 +87,9 @@ type UploadFromUrlArgs = {
@Injectable()
export class DriveService {
public static NoSuchFolderError = class extends Error {};
public static InvalidFileNameError = class extends Error {};
public static CannotUnmarkSensitiveError = class extends Error {};
private registerLogger: Logger;
private downloaderLogger: Logger;
private deleteLogger: Logger;
@ -119,6 +123,7 @@ export class DriveService {
private globalEventService: GlobalEventService,
private queueService: QueueService,
private roleService: RoleService,
private moderationLogService: ModerationLogService,
private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart,
@ -648,7 +653,63 @@ export class DriveService {
}
@bindThis
public async deleteFile(file: MiDriveFile, isExpired = false) {
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
throw new DriveService.InvalidFileNameError();
}
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) {
throw new DriveService.CannotUnmarkSensitiveError();
}
if (values.folderId != null) {
const folder = await this.driveFoldersRepository.findOneBy({
id: values.folderId,
userId: file.userId!,
});
if (folder == null) {
throw new DriveService.NoSuchFolderError();
}
}
await this.driveFilesRepository.update(file.id, values);
const fileObj = await this.driveFileEntityService.pack(file.id, { self: true });
// Publish fileUpdated event
if (file.userId) {
this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj);
}
if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) {
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) {
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
if (values.isSensitive) {
this.moderationLogService.log(updater, 'markSensitiveDriveFile', {
fileId: file.id,
fileUserId: file.userId,
fileUserUsername: user?.username ?? null,
fileUserHost: user?.host ?? null,
});
} else {
this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', {
fileId: file.id,
fileUserId: file.userId,
fileUserUsername: user?.username ?? null,
fileUserHost: user?.host ?? null,
});
}
}
}
return fileObj;
}
@bindThis
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!);
@ -671,11 +732,11 @@ export class DriveService {
}
}
this.deletePostProcess(file, isExpired);
this.deletePostProcess(file, isExpired, deleter);
}
@bindThis
public async deleteFileSync(file: MiDriveFile, isExpired = false) {
public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
if (file.storedInternal) {
this.internalStorageService.del(file.accessKey!);
@ -702,11 +763,11 @@ export class DriveService {
await Promise.all(promises);
}
this.deletePostProcess(file, isExpired);
this.deletePostProcess(file, isExpired, deleter);
}
@bindThis
private async deletePostProcess(file: MiDriveFile, isExpired = false) {
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, {
@ -733,6 +794,20 @@ export class DriveService {
this.instanceChart.updateDrive(file, false);
}
}
if (file.userId) {
this.globalEventService.publishDriveStream(file.userId, 'fileDeleted', file.id);
}
if (deleter && await this.roleService.isModerator(deleter) && (file.userId !== deleter.id)) {
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
this.moderationLogService.log(deleter, 'deleteDriveFile', {
fileId: file.id,
fileUserId: file.userId,
fileUserUsername: user?.username ?? null,
fileUserHost: user?.host ?? null,
});
}
}
@bindThis

View file

@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
@Injectable()
export class ModerationLogService {
@ -21,13 +22,13 @@ export class ModerationLogService {
}
@bindThis
public async insertModerationLog(moderator: { id: MiUser['id'] }, type: string, info?: Record<string, any>) {
public async log<T extends typeof moderationLogTypes[number]>(moderator: { id: MiUser['id'] }, type: T, info?: ModerationLogPayloads[T]) {
await this.moderationLogsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: moderator.id,
type: type,
info: info ?? {},
info: (info as any) ?? {},
});
}
}

View file

@ -23,6 +23,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable()
export class NoteDeleteService {
@ -48,6 +49,7 @@ export class NoteDeleteService {
private apDeliverManagerService: ApDeliverManagerService,
private metaService: MetaService,
private searchService: SearchService,
private moderationLogService: ModerationLogService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart,
@ -58,7 +60,7 @@ export class NoteDeleteService {
* @param user 稿
* @param note 稿
*/
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false) {
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note);
@ -131,6 +133,17 @@ export class NoteDeleteService {
id: note.id,
userId: user.id,
});
if (deleter && (note.userId !== deleter.id)) {
const user = await this.usersRepository.findOneByOrFail({ id: note.userId });
this.moderationLogService.log(deleter, 'deleteNote', {
noteId: note.id,
noteUserId: note.userId,
noteUserUsername: user.username,
noteUserHost: user.host,
note: note,
});
}
}
@bindThis

View file

@ -18,6 +18,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@ -98,6 +99,7 @@ export class RoleService implements OnApplicationShutdown {
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
private moderationLogService: ModerationLogService,
) {
//this.onMessage = this.onMessage.bind(this);
@ -374,9 +376,11 @@ export class RoleService implements OnApplicationShutdown {
}
@bindThis
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null): Promise<void> {
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise<void> {
const now = new Date();
const role = await this.rolesRepository.findOneByOrFail({ id: roleId });
const existing = await this.roleAssignmentsRepository.findOneBy({
roleId: roleId,
userId: userId,
@ -406,10 +410,22 @@ export class RoleService implements OnApplicationShutdown {
});
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
userUsername: user.username,
userHost: user.host,
expiresAt: expiresAt ? expiresAt.toISOString() : null,
});
}
}
@bindThis
public async unassign(userId: MiUser['id'], roleId: MiRole['id']): Promise<void> {
public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise<void> {
const now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
@ -430,6 +446,20 @@ export class RoleService implements OnApplicationShutdown {
});
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
if (moderator) {
const [user, role] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: userId }),
this.rolesRepository.findOneByOrFail({ id: roleId }),
]);
this.moderationLogService.log(moderator, 'unassignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
userUsername: user.username,
userHost: user.host,
});
}
}
@bindThis
@ -451,6 +481,75 @@ export class RoleService implements OnApplicationShutdown {
redisPipeline.exec();
}
@bindThis
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
const date = new Date();
const created = await this.rolesRepository.insert({
id: this.idService.genId(),
createdAt: date,
updatedAt: date,
lastUsedAt: date,
name: values.name,
description: values.description,
color: values.color,
iconUrl: values.iconUrl,
target: values.target,
condFormula: values.condFormula,
isPublic: values.isPublic,
isAdministrator: values.isAdministrator,
isModerator: values.isModerator,
isExplorable: values.isExplorable,
asBadge: values.asBadge,
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('roleCreated', created);
if (moderator) {
this.moderationLogService.log(moderator, 'createRole', {
roleId: created.id,
role: created,
});
}
return created;
}
@bindThis
public async update(role: MiRole, params: Partial<MiRole>, moderator?: MiUser): Promise<void> {
const date = new Date();
await this.rolesRepository.update(role.id, {
updatedAt: date,
...params,
});
const updated = await this.rolesRepository.findOneByOrFail({ id: role.id });
this.globalEventService.publishInternalEvent('roleUpdated', updated);
if (moderator) {
this.moderationLogService.log(moderator, 'updateRole', {
roleId: role.id,
before: role,
after: updated,
});
}
}
@bindThis
public async delete(role: MiRole, moderator?: MiUser): Promise<void> {
await this.rolesRepository.delete({ id: role.id });
this.globalEventService.publishInternalEvent('roleDeleted', role);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteRole', {
roleId: role.id,
role: role,
});
}
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { QueryFailedError } from 'typeorm';
import * as OTPAuth from 'otpauth';
import { DI } from '@/di-symbols.js';
import type { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type { MiLocalUser } from '@/models/User.js';
@Injectable()
export class UserAuthService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
) {
}
@bindThis
public async twoFactorAuthenticate(profile: MiUserProfile, token: string): Promise<void> {
if (profile.twoFactorBackupSecret?.includes(token)) {
await this.userProfilesRepository.update({ userId: profile.userId }, {
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token),
});
} else {
const delta = OTPAuth.TOTP.validate({
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
digits: 6,
token,
window: 5,
});
if (delta === null) {
throw new Error('authentication failed');
}
}
}
}

View file

@ -4,9 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/_.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
@ -32,6 +33,7 @@ export class Resolver {
private notesRepository: NotesRepository,
private pollsRepository: PollsRepository,
private noteReactionsRepository: NoteReactionsRepository,
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService,
private instanceActorService: InstanceActorService,
private metaService: MetaService,
@ -146,13 +148,24 @@ export class Resolver {
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
case 'follows':
// rest should be <followee id>
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI');
return Promise.all(
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })),
)
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)));
return this.followRequestsRepository.findOneBy({ id: parsed.id })
.then(async followRequest => {
if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID');
const [follower, followee] = await Promise.all([
this.usersRepository.findOneBy({
id: followRequest.followerId,
host: IsNull(),
}),
this.usersRepository.findOneBy({
id: followRequest.followeeId,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
throw new Error('resolveLocal: follower or followee does not exist');
}
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
});
default:
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
}
@ -177,6 +190,9 @@ export class ApResolverService {
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService,
private instanceActorService: InstanceActorService,
private metaService: MetaService,
@ -196,6 +212,7 @@ export class ApResolverService {
this.notesRepository,
this.pollsRepository,
this.noteReactionsRepository,
this.followRequestsRepository,
this.utilityService,
this.instanceActorService,
this.metaService,

View file

@ -41,8 +41,8 @@ export class MiAntenna {
})
public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list'] })
public src: 'home' | 'all' | 'users' | 'list';
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'users_blacklist'] })
public src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist';
@Column({
...id(),

View file

@ -20,6 +20,11 @@ export class MiMeta {
})
public name: string | null;
@Column('varchar', {
length: 64, nullable: true,
})
public shortName: string | null;
@Column('varchar', {
length: 1024, nullable: true,
})

View file

@ -47,7 +47,7 @@ export const packedAntennaSchema = {
src: {
type: 'string',
optional: false, nullable: false,
enum: ['home', 'all', 'users', 'list'],
enum: ['home', 'all', 'users', 'list', 'users_blacklist'],
},
userListId: {
type: 'string',

View file

@ -20,6 +20,7 @@ import type { MiLocalUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
@ -43,6 +44,7 @@ export class SigninApiService {
private idService: IdService,
private rateLimiterService: RateLimiterService,
private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
) {
}
@ -125,7 +127,7 @@ export class SigninApiService {
const same = await argon2.verify(profile.password!, password);
const fail = async (status?: number, failure?: { id: string }) => {
// Append signin history
// Append signin history
await this.signinsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
@ -155,27 +157,15 @@ export class SigninApiService {
});
}
if (profile.twoFactorBackupSecret?.includes(token)) {
await this.userProfilesRepository.update({ userId: profile.userId }, {
twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token),
});
return this.signinService.signin(request, reply, user);
}
const delta = OTPAuth.TOTP.validate({
secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
digits: 6,
token,
window: 1,
});
if (delta === null) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});
} else {
return this.signinService.signin(request, reply, user);
}
return this.signinService.signin(request, reply, user);
} else if (body.credential) {
if (!same && !profile.usePasswordLessLogin) {
return await fail(403, {
@ -204,6 +194,6 @@ export class SigninApiService {
reply.code(200);
return authRequest;
}
// never get here
// never get here
}
}

View file

@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
userId: ps.userId,
});
}, me);
return packed;
});

View file

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AnnouncementsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -37,13 +38,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
const announcement = await this.announcementsRepository.findOneBy({ id: ps.id });
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await this.announcementsRepository.delete(announcement.id);
await this.announcementService.delete(announcement, me);
});
}
}

View file

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AnnouncementsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -45,13 +46,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
const announcement = await this.announcementsRepository.findOneBy({ id: ps.id });
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await this.announcementsRepository.update(announcement.id, {
await this.announcementService.update(announcement, {
updatedAt: new Date(),
title: ps.title,
text: ps.text,
@ -62,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
isActive: ps.isActive,
});
}, me);
});
}
}

View file

@ -8,7 +8,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { ApiError } from '../../../error.js';
@ -61,7 +60,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
@ -77,11 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id,
});
}, me);
return this.emojiEntityService.packDetailed(emoji);
});

View file

@ -30,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.customEmojiService.deleteBulk(ps.ids);
await this.customEmojiService.deleteBulk(ps.ids, me);
});
}
}

View file

@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.customEmojiService.delete(ps.id);
await this.customEmojiService.delete(ps.id, me);
});
}
}

View file

@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive,
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
});
}, me);
});
}
}

View file

@ -9,6 +9,7 @@ import type { InstancesRepository } from '@/models/_.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -34,6 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) });
@ -42,9 +44,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found');
}
this.federatedInstanceService.update(instance.id, {
await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended,
});
if (instance.isSuspended !== ps.isSuspended) {
if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id,
host: instance.host,
});
} else {
this.moderationLogService.log(me, 'unsuspendRemoteInstance', {
id: instance.id,
host: instance.host,
});
}
}
});
}
}

View file

@ -321,6 +321,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
maintainerEmail: instance.maintainerEmail,
version: this.config.version,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,

View file

@ -30,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
this.queueService.destroy();
this.moderationLogService.insertModerationLog(me, 'clearQueue');
this.moderationLogService.log(me, 'clearQueue');
});
}
}

View file

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}
this.moderationLogService.insertModerationLog(me, 'promoteQueue');
this.moderationLogService.log(me, 'promoteQueue');
});
}
}

View file

@ -10,6 +10,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -47,8 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps) => {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
@ -70,6 +73,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
password: hash,
});
this.moderationLogService.log(me, 'resetPassword', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
return {
password: passwd,
};

View file

@ -10,6 +10,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
import { QueueService } from '@/core/QueueService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -41,6 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
@ -61,6 +63,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
assigneeId: me.id,
forwarded: ps.forward && report.targetUserHost != null,
});
this.moderationLogService.log(me, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost != null,
});
});
}
}

View file

@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return;
}
await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null);
await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null, me);
});
}
}

View file

@ -5,11 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
@ -58,37 +55,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,
private globalEventService: GlobalEventService,
private idService: IdService,
private roleEntityService: RoleEntityService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const date = new Date();
const created = await this.rolesRepository.insert({
id: this.idService.genId(),
createdAt: date,
updatedAt: date,
lastUsedAt: date,
name: ps.name,
description: ps.description,
color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target,
condFormula: ps.condFormula,
isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator,
isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('roleCreated', created);
const created = await this.roleService.create(ps, me);
return await this.roleEntityService.pack(created, me);
});

View file

@ -6,9 +6,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
@ -41,17 +41,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,
private globalEventService: GlobalEventService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps) => {
super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (role == null) {
throw new ApiError(meta.errors.noSuchRole);
}
await this.rolesRepository.delete({
id: ps.roleId,
});
this.globalEventService.publishInternalEvent('roleDeleted', role);
await this.roleService.delete(role, me);
});
}
}

View file

@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchUser);
}
await this.roleService.unassign(user.id, role.id);
await this.roleService.unassign(user.id, role.id, me);
});
}
}

View file

@ -9,6 +9,7 @@ import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
@ -70,17 +71,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,
private globalEventService: GlobalEventService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps) => {
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
if (!roleExist) {
super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (role == null) {
throw new ApiError(meta.errors.noSuchRole);
}
const date = new Date();
await this.rolesRepository.update(ps.roleId, {
updatedAt: date,
await this.roleService.update(role, {
name: ps.name,
description: ps.description,
color: ps.color,
@ -95,9 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
});
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
this.globalEventService.publishInternalEvent('roleUpdated', updated);
}, me);
});
}
}

View file

@ -62,6 +62,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
type: { type: 'string', nullable: true },
userId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: [],
} as const;
@ -78,6 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
if (ps.type != null) {
query.andWhere('report.type = :type', { type: ps.type });
}
if (ps.userId != null) {
query.andWhere('report.userId = :userId', { userId: ps.userId });
}
const reports = await query.limit(ps.limit).getMany();
return await this.moderationLogEntityService.packMany(reports);

View file

@ -60,8 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSuspended: true,
});
this.moderationLogService.insertModerationLog(me, 'suspend', {
targetId: user.id,
this.moderationLogService.log(me, 'suspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {

View file

@ -45,8 +45,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSuspended: false,
});
this.moderationLogService.insertModerationLog(me, 'unsuspend', {
targetId: user.id,
this.moderationLogService.log(me, 'unsuspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
this.userSuspendService.doPostUnsuspend(user);

View file

@ -44,6 +44,7 @@ export const paramDef = {
backgroundImageUrl: { type: 'string', nullable: true },
logoImageUrl: { type: 'string', nullable: true },
name: { type: 'string', nullable: true },
shortName: { type: 'string', nullable: true },
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
@ -188,6 +189,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.name = ps.name;
}
if (ps.shortName !== undefined) {
set.shortName = ps.shortName;
}
if (ps.description !== undefined) {
set.description = ps.description;
}
@ -436,8 +441,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.manifestJsonOverride = ps.manifestJsonOverride;
}
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
this.moderationLogService.insertModerationLog(me, 'updateMeta');
const after = await this.metaService.fetch(true);
this.moderationLogService.log(me, 'updateServerSettings', {
before,
after,
});
});
}
}

View file

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -32,6 +33,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -40,9 +43,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found');
}
const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update({ userId: user.id }, {
moderationNote: ps.text,
});
this.moderationLogService.log(me, 'updateUserNote', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
before: currentProfile.moderationNote,
after: ps.text,
});
});
}
}

View file

@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
.where('announcement.isActive = :isActive', { isActive: ps.isActive })
.andWhere('announcement.isActive = :isActive', { isActive: ps.isActive })
.andWhere(new Brackets(qb => {
if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id });
qb.orWhere('announcement.userId IS NULL');

View file

@ -47,7 +47,7 @@ export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] },
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: {
type: 'array', items: {

View file

@ -46,7 +46,7 @@ export const paramDef = {
properties: {
antennaId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] },
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: {
type: 'array', items: {

View file

@ -65,11 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
// Delete
await this.driveService.deleteFile(file);
// Publish fileDeleted event
this.globalEventService.publishDriveStream(me.id, 'fileDeleted', file.id);
await this.driveService.deleteFile(file, false, me);
});
}
}

View file

@ -4,12 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -77,16 +76,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.driveFoldersRepository)
private driveFoldersRepository: DriveFoldersRepository,
private driveFileEntityService: DriveFileEntityService,
private driveService: DriveService,
private roleService: RoleService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(me.id)).alwaysMarkNsfw;
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
@ -95,49 +89,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
if (ps.name) file.name = ps.name;
if (!this.driveFileEntityService.validateFileName(file.name)) {
throw new ApiError(meta.errors.invalidFileName);
}
let packedFile;
if (ps.comment !== undefined) file.comment = ps.comment;
if (ps.isSensitive !== undefined && ps.isSensitive !== file.isSensitive && alwaysMarkNsfw && !ps.isSensitive) {
throw new ApiError(meta.errors.restrictedByRole);
}
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
if (ps.folderId !== undefined) {
if (ps.folderId === null) {
file.folderId = null;
try {
packedFile = await this.driveService.updateFile(file, {
folderId: ps.folderId,
name: ps.name,
isSensitive: ps.isSensitive,
comment: ps.comment,
}, me);
} catch (e) {
if (e instanceof DriveService.InvalidFileNameError) {
throw new ApiError(meta.errors.invalidFileName);
} else if (e instanceof DriveService.NoSuchFolderError) {
throw new ApiError(meta.errors.noSuchFolder);
} else if (e instanceof DriveService.CannotUnmarkSensitiveError) {
throw new ApiError(meta.errors.restrictedByRole);
} else {
const folder = await this.driveFoldersRepository.findOneBy({
id: ps.folderId,
userId: me.id,
});
if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
file.folderId = folder.id;
throw e;
}
}
await this.driveFilesRepository.update(file.id, {
name: file.name,
comment: file.comment,
folderId: file.folderId,
isSensitive: file.isSensitive,
});
const fileObj = await this.driveFileEntityService.pack(file, { self: true });
// Publish fileUpdated event
this.globalEventService.publishDriveStream(me.id, 'fileUpdated', fileObj);
return fileObj;
return packedFile;
});
}
}

View file

@ -75,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
summary: ps.summary,
script: ps.script,
permissions: ps.permissions,
visibility: ps.visibility,
});
});
}

View file

@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
digits: 6,
token,
window: 1,
window: 5,
});
if (delta === null) {

View file

@ -13,6 +13,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -38,6 +39,7 @@ export const paramDef = {
type: 'object',
properties: {
password: { type: 'string' },
token: { type: 'string', nullable: true },
name: { type: 'string', minLength: 1, maxLength: 30 },
credential: { type: 'object' },
},
@ -55,16 +57,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userSecurityKeysRepository: UserSecurityKeysRepository,
private webAuthnService: WebAuthnService,
private userAuthService: UserAuthService,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await argon2.verify(profile.password ?? '', ps.password);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);;
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword);
}

View file

@ -11,6 +11,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -42,6 +43,7 @@ export const paramDef = {
type: 'object',
properties: {
password: { type: 'string' },
token: { type: 'string', nullable: true },
},
required: ['password'],
} as const;
@ -54,8 +56,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userProfilesRepository: UserProfilesRepository,
private webAuthnService: WebAuthnService,
private userAuthService: UserAuthService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOne({
where: {
userId: me.id,
@ -68,9 +72,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// Compare password
const same = await argon2.verify(profile.password ?? '', ps.password);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);;
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword);
}

View file

@ -13,6 +13,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -32,6 +33,7 @@ export const paramDef = {
type: 'object',
properties: {
password: { type: 'string' },
token: { type: 'string', nullable: true },
},
required: ['password'],
} as const;
@ -44,14 +46,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userAuthService: UserAuthService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await argon2.verify(profile.password ?? '', ps.password);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword);
}

View file

@ -12,6 +12,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -31,6 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
password: { type: 'string' },
token: { type: 'string', nullable: true },
credentialId: { type: 'string' },
},
required: ['password', 'credentialId'],
@ -46,15 +48,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private userAuthService: UserAuthService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await argon2.verify(profile.password ?? '', ps.password);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword);
}

View file

@ -12,6 +12,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -31,6 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
password: { type: 'string' },
token: { type: 'string', nullable: true },
},
required: ['password'],
} as const;
@ -42,15 +44,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private userAuthService: UserAuthService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await argon2.verify(profile.password ?? '', ps.password);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password ?? '', ps.password);
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword);
}

View file

@ -9,6 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -21,6 +22,7 @@ export const paramDef = {
properties: {
currentPassword: { type: 'string' },
newPassword: { type: 'string', minLength: 1 },
token: { type: 'string', nullable: true },
},
required: ['currentPassword', 'newPassword'],
} as const;
@ -30,14 +32,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userAuthService: UserAuthService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await argon2.verify(profile.password!, ps.currentPassword);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password!, ps.currentPassword);
if (!passwordMatched) {
throw new Error('incorrect password');
}

View file

@ -10,6 +10,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
export const meta = {
requireCredential: true,
@ -21,6 +22,7 @@ export const paramDef = {
type: 'object',
properties: {
password: { type: 'string' },
token: { type: 'string', nullable: true },
},
required: ['password'],
} as const;
@ -34,19 +36,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userAuthService: UserAuthService,
private deleteAccountService: DeleteAccountService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
if (userDetailed.isDeleted) {
return;
}
// Compare password
const same = await argon2.verify(profile.password!, ps.password);
if (!same) {
const passwordMatched = await argon2.verify(profile.password!, ps.password);
if (!passwordMatched) {
throw new Error('incorrect password');
}

View file

@ -15,6 +15,7 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -47,6 +48,7 @@ export const paramDef = {
properties: {
password: { type: 'string' },
email: { type: 'string', nullable: true },
token: { type: 'string', nullable: true },
},
required: ['password'],
} as const;
@ -62,15 +64,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private emailService: EmailService,
private userAuthService: UserAuthService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password
const same = await argon2.verify(profile.password!, ps.password);
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
if (!same) {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const passwordMatched = await argon2.verify(profile.password!, ps.password);;
if (!passwordMatched) {
throw new ApiError(meta.errors.incorrectPassword);
}

View file

@ -40,6 +40,10 @@ export const meta = {
type: 'string',
optional: false, nullable: false,
},
shortName: {
type: 'string',
optional: false, nullable: true,
},
uri: {
type: 'string',
optional: false, nullable: false,
@ -288,6 +292,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
version: this.config.version,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,

View file

@ -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);
});
}
}

View file

@ -4,12 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets, type FindOptionsWhere } from 'typeorm';
import type { NoteReactionsRepository } from '@/models/_.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
import { DI } from '@/di-symbols.js';
import type { FindOptionsWhere } from 'typeorm';
import { QueryService } from '@/core/QueryService.js';
export const meta = {
tags: ['notes', 'reactions'],
@ -44,7 +45,6 @@ export const paramDef = {
noteId: { type: 'string', format: 'misskey:id' },
type: { type: 'string', nullable: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
@ -58,29 +58,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteReactionsRepository: NoteReactionsRepository,
private noteReactionEntityService: NoteReactionEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = {
noteId: ps.noteId,
} as FindOptionsWhere<MiNoteReaction>;
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId)
.andWhere('reaction.noteId = :noteId', { noteId: ps.noteId })
.leftJoinAndSelect('reaction.user', 'user')
.leftJoinAndSelect('reaction.note', 'note');
if (ps.type) {
// ローカルリアクションはホスト名が . とされているが
// DB 上ではそうではないので、必要に応じて変換
const suffix = '@.:';
const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type;
query.reaction = type;
query.andWhere('reaction.reaction = :type', { type });
}
const reactions = await this.noteReactionsRepository.find({
where: query,
take: ps.limit,
skip: ps.offset,
order: {
id: -1,
},
relations: ['user', 'note'],
});
const reactions = await query.limit(ps.limit).getMany();
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
});

View file

@ -114,10 +114,10 @@ export class ClientServerService {
let manifest = {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'short_name': instance.name || 'Misskey',
'short_name': instance.shortName || instance.name || this.config.host,
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'name': instance.name || 'Misskey',
'name': instance.name || this.config.host,
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',

View file

@ -35,7 +35,7 @@ html
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.32.0')
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.35.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists

View file

@ -26,3 +26,176 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
export const ffVisibility = ['public', 'followers', 'private'] as const;
export const moderationLogTypes = [
'updateServerSettings',
'suspend',
'unsuspend',
'updateUserNote',
'addCustomEmoji',
'updateCustomEmoji',
'deleteCustomEmoji',
'assignRole',
'unassignRole',
'createRole',
'updateRole',
'deleteRole',
'clearQueue',
'promoteQueue',
'deleteDriveFile',
'deleteNote',
'createGlobalAnnouncement',
'createUserAnnouncement',
'updateGlobalAnnouncement',
'updateUserAnnouncement',
'deleteGlobalAnnouncement',
'deleteUserAnnouncement',
'resetPassword',
'suspendRemoteInstance',
'unsuspendRemoteInstance',
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
] as const;
export type ModerationLogPayloads = {
updateServerSettings: {
before: any | null;
after: any | null;
};
suspend: {
userId: string;
userUsername: string;
userHost: string | null;
};
unsuspend: {
userId: string;
userUsername: string;
userHost: string | null;
};
updateUserNote: {
userId: string;
userUsername: string;
userHost: string | null;
before: string | null;
after: string | null;
};
addCustomEmoji: {
emojiId: string;
emoji: any;
};
updateCustomEmoji: {
emojiId: string;
before: any;
after: any;
};
deleteCustomEmoji: {
emojiId: string;
emoji: any;
};
assignRole: {
userId: string;
userUsername: string;
userHost: string | null;
roleId: string;
roleName: string;
expiresAt: string | null;
};
unassignRole: {
userId: string;
userUsername: string;
userHost: string | null;
roleId: string;
roleName: string;
};
createRole: {
roleId: string;
role: any;
};
updateRole: {
roleId: string;
before: any;
after: any;
};
deleteRole: {
roleId: string;
role: any;
};
clearQueue: Record<string, never>;
promoteQueue: Record<string, never>;
deleteDriveFile: {
fileId: string;
fileUserId: string | null;
fileUserUsername: string | null;
fileUserHost: string | null;
};
deleteNote: {
noteId: string;
noteUserId: string;
noteUserUsername: string;
noteUserHost: string | null;
note: any;
};
createGlobalAnnouncement: {
announcementId: string;
announcement: any;
};
createUserAnnouncement: {
announcementId: string;
announcement: any;
userId: string;
userUsername: string;
userHost: string | null;
};
updateGlobalAnnouncement: {
announcementId: string;
before: any;
after: any;
};
updateUserAnnouncement: {
announcementId: string;
before: any;
after: any;
userId: string;
userUsername: string;
userHost: string | null;
};
deleteGlobalAnnouncement: {
announcementId: string;
announcement: any;
};
deleteUserAnnouncement: {
announcementId: string;
announcement: any;
};
resetPassword: {
userId: string;
userUsername: string;
userHost: string | null;
};
suspendRemoteInstance: {
id: string;
host: string;
};
unsuspendRemoteInstance: {
id: string;
host: string;
};
markSensitiveDriveFile: {
fileId: string;
fileUserId: string | null;
fileUserUsername: string | null;
fileUserHost: string | null;
};
unmarkSensitiveDriveFile: {
fileId: string;
fileUserId: string | null;
fileUserUsername: string | null;
fileUserHost: string | null;
};
resolveAbuseReport: {
reportId: string;
report: any;
forwarded: boolean;
};
};

View file

@ -60,10 +60,12 @@ describe('2要素認証', () => {
};
const keyDoneParam = (param: {
token: string,
keyName: string,
credentialId: Buffer,
creationOptions: PublicKeyCredentialCreationOptionsJSON,
}): {
token: string,
password: string,
name: string,
credential: RegistrationResponseJSON,
@ -94,6 +96,7 @@ describe('2要素認証', () => {
return {
password,
token: param.token,
name: param.keyName,
credential: <RegistrationResponseJSON>{
id: param.credentialId.toString('base64url'),
@ -218,6 +221,12 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでログインできる。', async () => {
@ -233,6 +242,7 @@ describe('2要素認証', () => {
const registerKeyResponse = await api('/i/2fa/register-key', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
assert.notEqual(registerKeyResponse.body.rp, undefined);
@ -241,6 +251,7 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
@ -271,6 +282,12 @@ describe('2要素認証', () => {
}));
assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
@ -285,6 +302,7 @@ describe('2要素認証', () => {
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
@ -292,6 +310,7 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
@ -326,6 +345,12 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
@ -340,6 +365,7 @@ describe('2要素認証', () => {
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
@ -347,6 +373,7 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
@ -367,6 +394,12 @@ describe('2要素認証', () => {
assert.strictEqual(securityKeys.length, 1);
assert.strictEqual(securityKeys[0].name, renamedKey);
assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
@ -381,6 +414,7 @@ describe('2要素認証', () => {
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
@ -388,6 +422,7 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
credentialId,
creationOptions: registerKeyResponse.body,
@ -400,6 +435,7 @@ describe('2要素認証', () => {
assert.strictEqual(iResponse.status, 200);
for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('/i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
credentialId: key.id,
}, alice);
@ -418,6 +454,12 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
@ -438,6 +480,7 @@ describe('2要素認証', () => {
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('/i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
@ -447,5 +490,11 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
});

View file

@ -15,7 +15,7 @@ import type { LoggerService } from '@/core/LoggerService.js';
import type { MetaService } from '@/core/MetaService.js';
import type { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
type MockResponse = {
type: string;
@ -33,6 +33,7 @@ export class MockResolver extends Resolver {
{} as NotesRepository,
{} as PollsRepository,
{} as NoteReactionsRepository,
{} as FollowRequestsRepository,
{} as UtilityService,
{} as InstanceActorService,
{} as MetaService,

View file

@ -16,6 +16,7 @@ import { genAidx } from '@/misc/id/aidx.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
@ -29,6 +30,7 @@ describe('AnnouncementService', () => {
let announcementsRepository: AnnouncementsRepository;
let announcementReadsRepository: AnnouncementReadsRepository;
let globalEventService: jest.Mocked<GlobalEventService>;
let moderationLogService: jest.Mocked<ModerationLogService>;
function createUser(data: Partial<MiUser> = {}) {
const un = secureRndstr(16);
@ -71,8 +73,11 @@ describe('AnnouncementService', () => {
publishMainStream: jest.fn(),
publishBroadcastStream: jest.fn(),
};
}
if (typeof token === 'function') {
} else if (token === ModerationLogService) {
return {
log: jest.fn(),
};
} else if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
@ -87,6 +92,7 @@ describe('AnnouncementService', () => {
announcementsRepository = app.get<AnnouncementsRepository>(DI.announcementsRepository);
announcementReadsRepository = app.get<AnnouncementReadsRepository>(DI.announcementReadsRepository);
globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
});
afterEach(async () => {
@ -155,10 +161,11 @@ describe('AnnouncementService', () => {
describe('create', () => {
test('通常', async () => {
const me = await createUser();
const result = await announcementService.create({
title: 'Title',
text: 'Text',
});
}, me);
expect(result.raw.title).toBe('Title');
expect(result.packed.title).toBe('Title');
@ -166,15 +173,17 @@ describe('AnnouncementService', () => {
expect(globalEventService.publishBroadcastStream).toHaveBeenCalled();
expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated');
expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed);
expect(moderationLogService.log).toHaveBeenCalled();
});
test('ユーザー指定', async () => {
const me = await createUser();
const user = await createUser();
const result = await announcementService.create({
title: 'Title',
text: 'Text',
userId: user.id,
});
}, me);
expect(result.raw.title).toBe('Title');
expect(result.packed.title).toBe('Title');
@ -184,6 +193,7 @@ describe('AnnouncementService', () => {
expect(globalEventService.publishMainStream.mock.lastCall![0]).toBe(user.id);
expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated');
expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed);
expect(moderationLogService.log).toHaveBeenCalled();
});
});

View file

@ -23,7 +23,7 @@
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.4",
"@syuilo/aiscript": "0.16.0",
"@tabler/icons-webfont": "2.32.0",
"@tabler/icons-webfont": "2.35.0",
"@vitejs/plugin-vue": "4.3.4",
"@vue-macros/reactivity-transform": "0.3.23",
"@vue/compiler-sfc": "3.3.4",
@ -70,6 +70,7 @@
"twemoji-parser": "14.0.0",
"typescript": "5.2.2",
"uuid": "9.0.1",
"v-code-diff": "^1.7.1",
"vanilla-tilt": "1.8.1",
"vite": "4.4.9",
"vue": "3.3.4",
@ -77,30 +78,30 @@
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.4.3",
"@storybook/addon-essentials": "7.4.3",
"@storybook/addon-interactions": "7.4.3",
"@storybook/addon-links": "7.4.3",
"@storybook/addon-storysource": "7.4.3",
"@storybook/addons": "7.4.3",
"@storybook/blocks": "7.4.3",
"@storybook/core-events": "7.4.3",
"@storybook/addon-actions": "7.4.4",
"@storybook/addon-essentials": "7.4.4",
"@storybook/addon-interactions": "7.4.4",
"@storybook/addon-links": "7.4.4",
"@storybook/addon-storysource": "7.4.4",
"@storybook/addons": "7.4.4",
"@storybook/blocks": "7.4.4",
"@storybook/core-events": "7.4.4",
"@storybook/jest": "0.2.2",
"@storybook/manager-api": "7.4.3",
"@storybook/preview-api": "7.4.3",
"@storybook/react": "7.4.3",
"@storybook/react-vite": "7.4.3",
"@storybook/manager-api": "7.4.4",
"@storybook/preview-api": "7.4.4",
"@storybook/react": "7.4.4",
"@storybook/react-vite": "7.4.4",
"@storybook/testing-library": "0.2.1",
"@storybook/theming": "7.4.3",
"@storybook/types": "7.4.3",
"@storybook/vue3": "7.4.3",
"@storybook/vue3-vite": "7.4.3",
"@storybook/theming": "7.4.4",
"@storybook/types": "7.4.4",
"@storybook/vue3": "7.4.4",
"@storybook/vue3-vite": "7.4.4",
"@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1",
"@types/estree": "1.0.1",
"@types/estree": "1.0.2",
"@types/matter-js": "0.19.0",
"@types/micromatch": "4.0.2",
"@types/node": "20.6.3",
"@types/node": "20.6.4",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.0",
"@types/throttle-debounce": "5.0.0",
@ -110,12 +111,12 @@
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"@vitest/coverage-v8": "0.34.4",
"@vitest/coverage-v8": "0.34.5",
"@vue/runtime-core": "3.3.4",
"acorn": "8.10.0",
"cross-env": "7.0.3",
"cypress": "13.2.0",
"eslint": "8.49.0",
"eslint": "8.50.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-vue": "9.17.0",
"fast-glob": "3.3.1",
@ -127,14 +128,14 @@
"prettier": "3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.0",
"storybook": "7.4.2",
"start-server-and-test": "2.0.1",
"storybook": "7.4.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.4",
"vitest": "0.34.5",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.11"
"vue-tsc": "1.8.13"
}
}

View file

@ -155,6 +155,10 @@ onMounted(() => {
}
});
});
defineExpose({
focus,
});
</script>
<style lang="scss" module>

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
<component
:is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? {
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:title="image.comment || image.name"
:width="image.properties.width"
:height="image.properties.height"
:style="hide ? 'filter: brightness(0.5);' : null"
:style="hide ? 'filter: brightness(0.7);' : null"
/>
</component>
<template v-if="hide">
@ -125,6 +125,22 @@ function showMenu(ev: MouseEvent) {
position: relative;
}
.sensitive {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
}
}
.hiddenText {
position: absolute;
left: 0;

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="hide" :class="$style.hidden" @click="hide = false">
<div v-if="hide" :class="[$style.hidden, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]" @click="hide = false">
<!-- 注意dataSaverMode が有効になっている際にはhide false になるまでサムネイルや動画を読み込まないようにすること -->
<div :class="$style.sensitive">
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
@ -12,11 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
<div v-else :class="$style.visible">
<div :class="$style.indicators">
<div v-if="video.comment" :class="$style.indicator">ALT</div>
<div v-if="!video.comment" :class="$style.indicator" title="Video lacks descriptive text"><i class="ti ti-pencil-off"></i></div>
</div>
<div v-else :class="[$style.visible, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]">
<video
:class="$style.video"
:poster="video.thumbnailUrl"
@ -53,6 +49,22 @@ const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enab
position: relative;
}
.sensitiveContainer {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 0 4px var(--warn);
}
}
.hide {
display: block;
position: absolute;

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from '@/types/menu';
import { MenuItem } from '@/types/menu.js';
const props = defineProps<{
items: MenuItem[];

View file

@ -36,14 +36,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</button>
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
<span :class="$style.switchText">{{ item.text }}</span>
</button>
<div v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
</div>
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
<span style="pointer-events: none;">{{ item.text }}</span>
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
</button>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>

View file

@ -946,9 +946,6 @@ function readPromo() {
height: 32px;
margin: 2px;
padding: 0 6px;
border: dashed 1px var(--divider);
border-radius: 4px;
background: transparent;
opacity: .8;
}
</style>

View file

@ -144,11 +144,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
<MkPagination :pagination="renotesPagination">
<MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
<template #default="{ items }">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
<MkUserCardMini :user="item.user" :withChart="false"/>
</MkA>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
<MkUserCardMini :user="item.user" :withChart="false"/>
</MkA>
</div>
</template>
</MkPagination>
</div>
@ -159,11 +161,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
</button>
</div>
<MkPagination :pagination="reactionsPagination">
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
<template #default="{ items }">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
<MkUserCardMini :user="item.user" :withChart="false"/>
</MkA>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
<MkUserCardMini :user="item.user" :withChart="false"/>
</MkA>
</div>
</template>
</MkPagination>
</div>

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPagination ref="pagingComponent" :pagination="pagination">
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
<template #empty>
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
@ -42,6 +42,7 @@ import { infoImageUrl } from '@/instance.js';
const props = defineProps<{
pagination: Paging;
noGap?: boolean;
disableAutoLoad?: boolean;
}>();
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();

View file

@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tail">
<header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: {{ notification.note.user.name ?? notification.note.user.username }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>

View file

@ -0,0 +1,70 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="370"
:height="400"
@close="onClose"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.authentication }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div style="padding: 0 0 16px 0; text-align: center;">
<img src="/fluent-emoji/1f510.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;">
<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
</div>
<div class="_gaps">
<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true">
<template #prefix><i class="ti ti-password"></i></template>
</MkInput>
<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false">
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
<template #prefix><i class="ti ti-123"></i></template>
</MkInput>
<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
const emit = defineEmits<{
(ev: 'done', v: { password: string; token: string | null; }): void;
(ev: 'closed'): void;
(ev: 'cancelled'): void;
}>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
const passwordInput = $shallowRef<InstanceType<typeof MkInput>>();
const password = $ref('');
const token = $ref(null);
function onClose() {
emit('cancelled');
if (dialog) dialog.close();
}
function done(res) {
emit('done', { password, token });
if (dialog) dialog.close();
}
onMounted(() => {
if (passwordInput) passwordInput.focus();
});
</script>

View file

@ -32,7 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div>
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
<div class="_gaps_s">
<MkSwitch v-for="kind in (initialPermissions || Misskey.permissions)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
</div>
</div>
</MkSpacer>
</MkModalWindow>

View file

@ -17,6 +17,7 @@ import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
import MkPageWindow from '@/components/MkPageWindow.vue';
import MkToast from '@/components/MkToast.vue';
import MkDialog from '@/components/MkDialog.vue';
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
@ -333,6 +334,18 @@ export function inputDate(props: {
});
}
export function authenticateDialog(): Promise<{ canceled: true; result: undefined; } | {
canceled: false; result: { password: string; token: string | null; };
}> {
return new Promise((resolve, reject) => {
popup(MkPasswordDialog, {}, {
done: result => {
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
},
}, 'closed');
});
}
export function select<C = any>(props: {
title?: string | null;
text?: string | null;

View file

@ -145,6 +145,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: currentPage?.route.name === 'abuses',
}, {
icon: 'ti ti-list-search',
text: i18n.ts.moderationLogs,
to: '/admin/modlog',
active: currentPage?.route.name === 'modlog',
}],
}, {
title: i18n.ts.settings,

View file

@ -0,0 +1,116 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder>
<template #label>
<b>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
<span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'createUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'updateUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
</template>
<template #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/>
</template>
<template #suffix>
<MkTime :time="log.createdAt"/>
</template>
<div :class="$style.root">
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div>
<div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div>
</div>
<template v-if="log.type === 'updateServerSettings'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'updateUserNote'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'suspend'">
<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 === 'unsuspend'">
<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 === 'updateRole'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</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>
<template v-else-if="log.type === 'updateCustomEmoji'">
<div>{{ i18n.ts.emoji }}: {{ log.info.emojiId }}</div>
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<details>
<summary>raw</summary>
<pre>{{ JSON5.stringify(log, null, '\t') }}</pre>
</details>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { CodeDiff } from 'v-code-diff';
import JSON5 from 'json5';
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;
}
.diff {
background: #fff;
color: #000;
border-radius: 6px;
overflow: clip;
}
</style>

View 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">{{ i18n.ts._moderationLogTypes[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>

View file

@ -14,6 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.instanceName }}</template>
</MkInput>
<MkInput v-model="shortName">
<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})</template>
<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
</MkInput>
<MkTextarea v-model="description">
<template #label>{{ i18n.ts.instanceDescription }}</template>
</MkTextarea>
@ -118,6 +123,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
let name: string | null = $ref(null);
let shortName: string | null = $ref(null);
let description: string | null = $ref(null);
let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null);
@ -133,6 +139,7 @@ let deeplIsPro: boolean = $ref(false);
async function init(): Promise<void> {
const meta = await os.api('admin/meta');
name = meta.name;
shortName = meta.shortName;
description = meta.description;
maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail;
@ -149,6 +156,7 @@ async function init(): Promise<void> {
function save(): void {
os.apiWithDialog('admin/update-meta', {
name,
shortName: shortName === '' ? null : shortName,
description,
maintainerName,
maintainerEmail,

View file

@ -16,12 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
</MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId">
<template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect>
<MkTextarea v-else-if="src === 'users'" v-model="users">
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<template #label>{{ i18n.ts.users }}</template>
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
</MkTextarea>

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note">
<div v-if="showNext" class="_margin">
<MkNotes class="" :pagination="nextPagination" :noGap="true"/>
<MkNotes class="" :pagination="nextPagination" :noGap="true" :disableAutoLoad="true"/>
</div>
<div class="_margin">

View file

@ -94,16 +94,12 @@ withDefaults(defineProps<{
const usePasswordLessLogin = $computed(() => $i?.usePasswordLessLogin ?? false);
async function registerTOTP(): Promise<void> {
const password = await os.inputText({
title: i18n.ts._2fa.registerTOTP,
text: i18n.ts._2fa.passwordToTOTP,
type: 'password',
autocomplete: 'current-password',
});
if (password.canceled) return;
const auth = await os.authenticateDialog();
if (auth.canceled) return;
const twoFactorData = await os.apiWithDialog('i/2fa/register', {
password: password.result,
password: auth.result.password,
token: auth.result.token,
});
os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), {
@ -111,20 +107,17 @@ async function registerTOTP(): Promise<void> {
}, {}, 'closed');
}
function unregisterTOTP(): void {
os.inputText({
title: i18n.ts.password,
type: 'password',
autocomplete: 'current-password',
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.apiWithDialog('i/2fa/unregister', {
password: password,
}).catch(error => {
os.alert({
type: 'error',
text: error,
});
async function unregisterTOTP(): Promise<void> {
const auth = await os.authenticateDialog();
if (auth.canceled) return;
os.apiWithDialog('i/2fa/unregister', {
password: auth.result.password,
token: auth.result.token,
}).catch(error => {
os.alert({
type: 'error',
text: error,
});
});
}
@ -150,15 +143,12 @@ async function unregisterKey(key) {
});
if (confirm.canceled) return;
const password = await os.inputText({
title: i18n.ts.password,
type: 'password',
autocomplete: 'current-password',
});
if (password.canceled) return;
const auth = await os.authenticateDialog();
if (auth.canceled) return;
await os.apiWithDialog('i/2fa/remove-key', {
password: password.result,
password: auth.result.password,
token: auth.result.token,
credentialId: key.id,
});
os.success();
@ -181,16 +171,13 @@ async function renameKey(key) {
}
async function addSecurityKey() {
const password = await os.inputText({
title: i18n.ts.password,
type: 'password',
autocomplete: 'current-password',
});
if (password.canceled) return;
const auth = await os.authenticateDialog();
if (auth.canceled) return;
const registrationOptions = parseCreationOptionsFromJSON({
publicKey: await os.apiWithDialog('i/2fa/register-key', {
password: password.result,
password: auth.result.password,
token: auth.result.token,
}),
});
@ -211,8 +198,12 @@ async function addSecurityKey() {
);
if (!credential) return;
const auth2 = await os.authenticateDialog();
if (auth2.canceled) return;
await os.apiWithDialog('i/2fa/key-done', {
password: password.result,
password: auth.result.password,
token: auth.result.token,
name: name.result,
credential: credential.toJSON(),
});

View file

@ -67,18 +67,16 @@ const onChangeReceiveAnnouncementEmail = (v) => {
});
};
const saveEmailAddress = () => {
os.inputText({
title: i18n.ts.password,
type: 'password',
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.apiWithDialog('i/update-email', {
password: password,
email: emailAddress.value,
});
async function saveEmailAddress() {
const auth = await os.authenticateDialog();
if (auth.canceled) return;
os.apiWithDialog('i/update-email', {
password: auth.result.password,
token: auth.result.token,
email: emailAddress.value,
});
};
}
const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention'));
const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply'));

View file

@ -115,6 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="useBlurEffect">{{ i18n.ts.useBlurEffect }}</MkSwitch>
<MkSwitch v-model="useBlurEffectForModal">{{ i18n.ts.useBlurEffectForModal }}</MkSwitch>
<MkSwitch v-model="disableShowingAnimatedImages">{{ i18n.ts.disableShowingAnimatedImages }}</MkSwitch>
<MkSwitch v-model="highlightSensitiveMedia">{{ i18n.ts.highlightSensitiveMedia }}</MkSwitch>
<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
@ -234,6 +235,7 @@ const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
@ -283,6 +285,7 @@ watch([
overridedDeviceKind,
mediaListWithOneImageAppearance,
reactionsDisplaySize,
highlightSensitiveMedia,
keepScreenOn,
], async () => {
await reloadAsk();

View file

@ -113,14 +113,12 @@ async function deleteAccount() {
if (canceled) return;
}
const { canceled, result: password } = await os.inputText({
title: i18n.ts.password,
type: 'password',
});
if (canceled) return;
const auth = await os.authenticateDialog();
if (auth.canceled) return;
await os.apiWithDialog('i/delete-account', {
password: password,
password: auth.result.password,
token: auth.result.token,
});
await os.alert({

View file

@ -10,28 +10,49 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts.manage }}</template>
<div class="_gaps_s">
<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_s" style="padding: 20px;">
<span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_m" style="padding: 20px;">
<div class="_gaps_s">
<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
</div>
<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
<MkKeyValue>
<template #key>{{ i18n.ts.author }}</template>
<template #value>{{ plugin.author }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ plugin.description }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.permission }}</template>
<template #value>{{ plugin.permission }}</template>
</MkKeyValue>
<div class="_gaps_s">
<MkKeyValue>
<template #key>{{ i18n.ts.author }}</template>
<template #value>{{ plugin.author }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ plugin.description }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.permission }}</template>
<template #value>
<ul style="margin-top: 0; margin-bottom: 0;">
<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
</ul>
</template>
</MkKeyValue>
</div>
<div class="_buttons">
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
</div>
<MkFolder>
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<div class="_gaps_s">
<div class="_buttons">
<MkButton inline @click="copy(plugin)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
</div>
<MkCode :code="plugin.src ?? ''"/>
</div>
</MkFolder>
</div>
</div>
</FormSection>
@ -44,8 +65,11 @@ import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
import MkCode from '@/components/MkCode.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { ColdDeviceStorage } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
@ -61,6 +85,11 @@ function uninstall(plugin) {
});
}
function copy(plugin) {
copyToClipboard(plugin.src ?? '');
os.success();
}
// TODO: storeactionAiScriptAPI
async function config(plugin) {
const config = plugin.config;

View file

@ -55,13 +55,6 @@ const pagination = {
};
async function change() {
const { canceled: canceled1, result: currentPassword } = await os.inputText({
title: i18n.ts.currentPassword,
type: 'password',
autocomplete: 'current-password',
});
if (canceled1) return;
const { canceled: canceled2, result: newPassword } = await os.inputText({
title: i18n.ts.newPassword,
type: 'password',
@ -84,21 +77,23 @@ async function change() {
return;
}
const auth = await os.authenticateDialog();
if (auth.canceled) return;
os.apiWithDialog('i/change-password', {
currentPassword,
currentPassword: auth.result.password,
token: auth.result.token,
newPassword,
});
}
function regenerateToken() {
os.inputText({
title: i18n.ts.password,
type: 'password',
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/regenerate-token', {
password: password,
});
async function regenerateToken() {
const auth = await os.authenticateDialog();
if (auth.canceled) return;
os.api('i/regenerate-token', {
password: auth.result.password,
token: auth.result.token,
});
}

View file

@ -395,6 +395,10 @@ export const routes = [{
path: '/abuses',
name: 'abuses',
component: page(() => import('./pages/admin/abuses.vue')),
}, {
path: '/modlog',
name: 'modlog',
component: page(() => import('./pages/admin/modlog.vue')),
}, {
path: '/settings',
name: 'settings',

View file

@ -34,12 +34,15 @@ export function createAiScriptEnv(opts) {
return confirm.canceled ? values.FALSE : values.TRUE;
}),
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
utils.assertString(ep);
if (ep.value.includes('://')) throw new Error('invalid endpoint');
if (token) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
}
return os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)).then(res => {
const actualToken: string|null = token?.value ?? opts.token ?? null;
return os.api(ep.value, utils.valToJs(param), actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));

View file

@ -383,8 +383,8 @@ export function getNoteMenu(props: {
.filter(x => x !== undefined);
} else {
menu = [{
icon: 'ti ti-external-link',
text: i18n.ts.detailed,
icon: 'ti ti-info-circle',
text: i18n.ts.details,
action: openDetail,
}, {
icon: 'ti ti-copy',

View file

@ -186,6 +186,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 'respect' as 'respect' | 'force' | 'ignore',
},
highlightSensitiveMedia: {
where: 'device',
default: false,
},
animation: {
where: 'device',
default: !window.matchMedia('(prefers-reduced-motion)').matches,
@ -378,6 +382,9 @@ export type Plugin = {
src: string | null;
version: string;
ast: any[];
author?: string;
description?: string;
permissions?: string[];
};
interface Watcher {

View file

@ -2278,7 +2278,8 @@ declare namespace entities {
Invite,
InviteLimit,
UserSorting,
OriginType
OriginType,
ModerationLog
}
}
export { entities }
@ -2404,6 +2405,7 @@ type LiteInstanceMetadata = {
maintainerEmail: string | null;
version: string;
name: string | null;
shortName: string | null;
uri: string;
description: string | null;
langs: string[];
@ -2515,6 +2517,98 @@ type MessagingMessage = {
groupId: UserGroup['id'] | null;
};
// @public (undocumented)
type ModerationLog = {
id: ID;
createdAt: DateString;
userId: User['id'];
user: UserDetailed | null;
} & ({
type: 'updateServerSettings';
info: ModerationLogPayloads['updateServerSettings'];
} | {
type: 'suspend';
info: ModerationLogPayloads['suspend'];
} | {
type: 'unsuspend';
info: ModerationLogPayloads['unsuspend'];
} | {
type: 'updateUserNote';
info: ModerationLogPayloads['updateUserNote'];
} | {
type: 'addCustomEmoji';
info: ModerationLogPayloads['addCustomEmoji'];
} | {
type: 'updateCustomEmoji';
info: ModerationLogPayloads['updateCustomEmoji'];
} | {
type: 'deleteCustomEmoji';
info: ModerationLogPayloads['deleteCustomEmoji'];
} | {
type: 'assignRole';
info: ModerationLogPayloads['assignRole'];
} | {
type: 'unassignRole';
info: ModerationLogPayloads['unassignRole'];
} | {
type: 'createRole';
info: ModerationLogPayloads['createRole'];
} | {
type: 'updateRole';
info: ModerationLogPayloads['updateRole'];
} | {
type: 'deleteRole';
info: ModerationLogPayloads['deleteRole'];
} | {
type: 'clearQueue';
info: ModerationLogPayloads['clearQueue'];
} | {
type: 'promoteQueue';
info: ModerationLogPayloads['promoteQueue'];
} | {
type: 'deleteDriveFile';
info: ModerationLogPayloads['deleteDriveFile'];
} | {
type: 'deleteNote';
info: ModerationLogPayloads['deleteNote'];
} | {
type: 'createGlobalAnnouncement';
info: ModerationLogPayloads['createGlobalAnnouncement'];
} | {
type: 'createUserAnnouncement';
info: ModerationLogPayloads['createUserAnnouncement'];
} | {
type: 'updateGlobalAnnouncement';
info: ModerationLogPayloads['updateGlobalAnnouncement'];
} | {
type: 'updateUserAnnouncement';
info: ModerationLogPayloads['updateUserAnnouncement'];
} | {
type: 'deleteGlobalAnnouncement';
info: ModerationLogPayloads['deleteGlobalAnnouncement'];
} | {
type: 'deleteUserAnnouncement';
info: ModerationLogPayloads['deleteUserAnnouncement'];
} | {
type: 'resetPassword';
info: ModerationLogPayloads['resetPassword'];
} | {
type: 'suspendRemoteInstance';
info: ModerationLogPayloads['suspendRemoteInstance'];
} | {
type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
type: 'markSensitiveDriveFile';
info: ModerationLogPayloads['markSensitiveDriveFile'];
} | {
type: 'unmarkSensitiveDriveFile';
info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport"];
// @public (undocumented)
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
@ -2860,6 +2954,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:579:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -23,10 +23,10 @@
"@microsoft/api-extractor": "7.37.0",
"@swc/jest": "0.2.29",
"@types/jest": "29.5.5",
"@types/node": "20.6.3",
"@types/node": "20.6.4",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"eslint": "8.49.0",
"eslint": "8.50.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
@ -39,7 +39,7 @@
],
"dependencies": {
"@swc/cli": "0.1.62",
"@swc/core": "1.3.86",
"@swc/core": "1.3.87",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}

View file

@ -44,3 +44,176 @@ export const permissions = [
'read:flash-likes',
'write:flash-likes',
];
export const moderationLogTypes = [
'updateServerSettings',
'suspend',
'unsuspend',
'updateUserNote',
'addCustomEmoji',
'updateCustomEmoji',
'deleteCustomEmoji',
'assignRole',
'unassignRole',
'createRole',
'updateRole',
'deleteRole',
'clearQueue',
'promoteQueue',
'deleteDriveFile',
'deleteNote',
'createGlobalAnnouncement',
'createUserAnnouncement',
'updateGlobalAnnouncement',
'updateUserAnnouncement',
'deleteGlobalAnnouncement',
'deleteUserAnnouncement',
'resetPassword',
'suspendRemoteInstance',
'unsuspendRemoteInstance',
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
] as const;
export type ModerationLogPayloads = {
updateServerSettings: {
before: any | null;
after: any | null;
};
suspend: {
userId: string;
userUsername: string;
userHost: string | null;
};
unsuspend: {
userId: string;
userUsername: string;
userHost: string | null;
};
updateUserNote: {
userId: string;
userUsername: string;
userHost: string | null;
before: string | null;
after: string | null;
};
addCustomEmoji: {
emojiId: string;
emoji: any;
};
updateCustomEmoji: {
emojiId: string;
before: any;
after: any;
};
deleteCustomEmoji: {
emojiId: string;
emoji: any;
};
assignRole: {
userId: string;
userUsername: string;
userHost: string | null;
roleId: string;
roleName: string;
expiresAt: string | null;
};
unassignRole: {
userId: string;
userUsername: string;
userHost: string | null;
roleId: string;
roleName: string;
};
createRole: {
roleId: string;
role: any;
};
updateRole: {
roleId: string;
before: any;
after: any;
};
deleteRole: {
roleId: string;
role: any;
};
clearQueue: Record<string, never>;
promoteQueue: Record<string, never>;
deleteDriveFile: {
fileId: string;
fileUserId: string | null;
fileUserUsername: string | null;
fileUserHost: string | null;
};
deleteNote: {
noteId: string;
noteUserId: string;
noteUserUsername: string;
noteUserHost: string | null;
note: any;
};
createGlobalAnnouncement: {
announcementId: string;
announcement: any;
};
createUserAnnouncement: {
announcementId: string;
announcement: any;
userId: string;
userUsername: string;
userHost: string | null;
};
updateGlobalAnnouncement: {
announcementId: string;
before: any;
after: any;
};
updateUserAnnouncement: {
announcementId: string;
before: any;
after: any;
userId: string;
userUsername: string;
userHost: string | null;
};
deleteGlobalAnnouncement: {
announcementId: string;
announcement: any;
};
deleteUserAnnouncement: {
announcementId: string;
announcement: any;
};
resetPassword: {
userId: string;
userUsername: string;
userHost: string | null;
};
suspendRemoteInstance: {
id: string;
host: string;
};
unsuspendRemoteInstance: {
id: string;
host: string;
};
markSensitiveDriveFile: {
fileId: string;
fileUserId: string | null;
fileUserUsername: string | null;
fileUserHost: string | null;
};
unmarkSensitiveDriveFile: {
fileId: string;
fileUserId: string | null;
fileUserUsername: string | null;
fileUserHost: string | null;
};
resolveAbuseReport: {
reportId: string;
report: any;
forwarded: boolean;
};
};

View file

@ -1,3 +1,5 @@
import { ModerationLogPayloads } from './consts.js';
export type ID = string;
export type DateString = string;
@ -308,6 +310,7 @@ export type LiteInstanceMetadata = {
maintainerEmail: string | null;
version: string;
name: string | null;
shortName: string | null;
uri: string;
description: string | null;
langs: string[];
@ -575,3 +578,91 @@ export type UserSorting =
| '+updatedAt'
| '-updatedAt';
export type OriginType = 'combined' | 'local' | 'remote';
export type ModerationLog = {
id: ID;
createdAt: DateString;
userId: User['id'];
user: UserDetailed | null;
} & ({
type: 'updateServerSettings';
info: ModerationLogPayloads['updateServerSettings'];
} | {
type: 'suspend';
info: ModerationLogPayloads['suspend'];
} | {
type: 'unsuspend';
info: ModerationLogPayloads['unsuspend'];
} | {
type: 'updateUserNote';
info: ModerationLogPayloads['updateUserNote'];
} | {
type: 'addCustomEmoji';
info: ModerationLogPayloads['addCustomEmoji'];
} | {
type: 'updateCustomEmoji';
info: ModerationLogPayloads['updateCustomEmoji'];
} | {
type: 'deleteCustomEmoji';
info: ModerationLogPayloads['deleteCustomEmoji'];
} | {
type: 'assignRole';
info: ModerationLogPayloads['assignRole'];
} | {
type: 'unassignRole';
info: ModerationLogPayloads['unassignRole'];
} | {
type: 'createRole';
info: ModerationLogPayloads['createRole'];
} | {
type: 'updateRole';
info: ModerationLogPayloads['updateRole'];
} | {
type: 'deleteRole';
info: ModerationLogPayloads['deleteRole'];
} | {
type: 'clearQueue';
info: ModerationLogPayloads['clearQueue'];
} | {
type: 'promoteQueue';
info: ModerationLogPayloads['promoteQueue'];
} | {
type: 'deleteDriveFile';
info: ModerationLogPayloads['deleteDriveFile'];
} | {
type: 'deleteNote';
info: ModerationLogPayloads['deleteNote'];
} | {
type: 'createGlobalAnnouncement';
info: ModerationLogPayloads['createGlobalAnnouncement'];
} | {
type: 'createUserAnnouncement';
info: ModerationLogPayloads['createUserAnnouncement'];
} | {
type: 'updateGlobalAnnouncement';
info: ModerationLogPayloads['updateGlobalAnnouncement'];
} | {
type: 'updateUserAnnouncement';
info: ModerationLogPayloads['updateUserAnnouncement'];
} | {
type: 'deleteGlobalAnnouncement';
info: ModerationLogPayloads['deleteGlobalAnnouncement'];
} | {
type: 'deleteUserAnnouncement';
info: ModerationLogPayloads['deleteUserAnnouncement'];
} | {
type: 'resetPassword';
info: ModerationLogPayloads['resetPassword'];
} | {
type: 'suspendRemoteInstance';
info: ModerationLogPayloads['suspendRemoteInstance'];
} | {
type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
type: 'markSensitiveDriveFile';
info: ModerationLogPayloads['markSensitiveDriveFile'];
} | {
type: 'unmarkSensitiveDriveFile';
info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
});

View file

@ -17,6 +17,7 @@ export const notificationTypes = consts.notificationTypes;
export const noteVisibilities = consts.noteVisibilities;
export const mutedNoteReasons = consts.mutedNoteReasons;
export const ffVisibility = consts.ffVisibility;
export const moderationLogTypes = consts.moderationLogTypes;
// api extractor not supported yet
//export * as api from './api.js';

View file

@ -16,7 +16,7 @@
"devDependencies": {
"@typescript-eslint/parser": "6.7.2",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint": "8.49.0",
"eslint": "8.50.0",
"eslint-plugin-import": "2.28.1",
"typescript": "5.2.2"
},