merge: upstream
This commit is contained in:
commit
8595a325ce
145 changed files with 3208 additions and 1285 deletions
|
@ -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) {
|
||||
}
|
||||
}
|
11
packages/backend/migration/1695440131671-short-name.js
Normal file
11
packages/backend/migration/1695440131671-short-name.js
Normal 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"`);
|
||||
}
|
||||
}
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
45
packages/backend/src/core/UserAuthService.ts
Normal file
45
packages/backend/src/core/UserAuthService.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
isSensitive: ps.isSensitive,
|
||||
localOnly: ps.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
});
|
||||
}, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,6 +155,10 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
focus,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>>();
|
||||
|
|
|
@ -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>
|
||||
|
|
70
packages/frontend/src/components/MkPasswordDialog.vue
Normal file
70
packages/frontend/src/components/MkPasswordDialog.vue
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
116
packages/frontend/src/pages/admin/modlog.ModLog.vue
Normal file
116
packages/frontend/src/pages/admin/modlog.ModLog.vue
Normal 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>
|
67
packages/frontend/src/pages/admin/modlog.vue
Normal file
67
packages/frontend/src/pages/admin/modlog.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div>
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkSelect v-model="type" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.type }}</template>
|
||||
<option :value="null">{{ i18n.ts.all }}</option>
|
||||
<option v-for="t in Misskey.moderationLogTypes" :key="t" :value="t">{{ 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>
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
|
||||
async function config(plugin) {
|
||||
const config = plugin.config;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'];
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue