From aacee3c97083b98b33b11e24c026d3b9d96dae34 Mon Sep 17 00:00:00 2001 From: samunohito <46447427+samunohito@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:02:50 +0900 Subject: [PATCH] wip --- .../backend/src/core/CustomEmojiService.ts | 160 +++++++- .../src/core/entities/EmojiEntityService.ts | 2 +- .../server/api/endpoints/admin/emoji/add.ts | 20 +- .../server/api/endpoints/admin/emoji/list.ts | 45 +-- .../src/server/api/endpoints/emojis.ts | 23 +- .../src/components/grid/MkDataCell.vue | 7 +- .../src/components/grid/MkDataRow.vue | 9 +- .../frontend/src/components/grid/MkGrid.vue | 360 +++++++++++++++--- .../src/components/grid/MkHeaderRow.vue | 1 - .../src/components/grid/MkNumberCell.vue | 3 - .../frontend/src/components/grid/types.ts | 8 +- .../pages/admin/custom-emojis-grid.impl.ts | 45 ++- .../pages/admin/custom-emojis-grid.list.vue | 89 +++++ .../admin/custom-emojis-grid.register.vue | 218 +++++++++++ .../src/pages/admin/custom-emojis-grid.vue | 88 ++--- packages/misskey-js/etc/misskey-js.api.md | 24 -- packages/misskey-js/src/api.types.ts | 22 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 2 +- packages/misskey-js/src/autogen/endpoint.ts | 5 +- packages/misskey-js/src/autogen/entities.ts | 3 +- packages/misskey-js/src/autogen/models.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 22 +- 22 files changed, 878 insertions(+), 280 deletions(-) create mode 100644 packages/frontend/src/pages/admin/custom-emojis-grid.list.vue create mode 100644 packages/frontend/src/pages/admin/custom-emojis-grid.register.vue diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 9a8267b466..7deac4d963 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -16,7 +16,6 @@ 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 '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -30,10 +29,8 @@ export class CustomEmojiService implements OnApplicationShutdown { constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - private utilityService: UtilityService, private idService: IdService, private emojiEntityService: EmojiEntityService, @@ -102,6 +99,64 @@ export class CustomEmojiService implements OnApplicationShutdown { return emoji; } + @bindThis + public async addBulk( + params: { + driveFile: MiDriveFile; + name: string; + category: string | null; + aliases: string[]; + host: string | null; + license: string | null; + isSensitive: boolean; + localOnly: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; + }[], + moderator?: MiUser, + ): Promise { + const emojis = await this.emojisRepository + .insert( + params.map(it => ({ + id: this.idService.gen(), + updatedAt: new Date(), + name: it.name, + category: it.category, + host: it.host, + aliases: it.aliases, + originalUrl: it.driveFile.url, + publicUrl: it.driveFile.webpublicUrl ?? it.driveFile.url, + type: it.driveFile.webpublicType ?? it.driveFile.type, + license: it.license, + isSensitive: it.isSensitive, + localOnly: it.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction, + })), + ) + .then(x => this.emojisRepository.createQueryBuilder('emoji').whereInIds(x.identifiers).getMany()); + + const localEmojis = emojis.filter(it => it.host == null); + if (localEmojis.length > 0) { + this.localEmojisCache.refresh(); + + this.emojiEntityService.packDetailedMany(localEmojis).then(it => { + for (const emoji of it) { + this.globalEventService.publishBroadcastStream('emojiAdded', { emoji }); + } + }); + + if (moderator) { + for (const emoji of localEmojis) { + this.moderationLogService.log(moderator, 'addCustomEmoji', { + emojiId: emoji.id, + emoji: emoji, + }); + } + } + } + + return emojis; + } + @bindThis public async update(id: MiEmoji['id'], data: { driveFile?: MiDriveFile; @@ -159,6 +214,103 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + @bindThis + public async updateBulk( + params: { + id: MiEmoji['id']; + driveFile?: MiDriveFile; + name?: string; + category?: string | null; + aliases?: string[]; + license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; + }[], + moderator?: MiUser, + ): Promise { + const ids = params.map(it => it.id); + + // IDに対応するものと、新しく設定しようとしている名前と同じ名前を持つレコードをそれぞれ取得する + const [storedEmojis, sameNameEmojis] = await Promise.all([ + this.emojisRepository.createQueryBuilder('emoji') + .whereInIds(ids) + .getMany() + .then(emojis => new Map(emojis.map(it => [it.id, it]))), + this.emojisRepository.createQueryBuilder('emoji') + .where('emoji.name IN (:...names) AND emoji.host IS NULL', { names: params.map(it => it.name) }) + .getMany(), + ]); + + // 新しく設定しようとしている名前と同じ名前を持つ別レコードがある場合、重複とみなしてエラーとする + const alreadyExists = Array.of(); + for (const sameNameEmoji of sameNameEmojis) { + const emoji = storedEmojis.get(sameNameEmoji.id); + if (emoji != null && emoji.id !== sameNameEmoji.id) { + alreadyExists.push(sameNameEmoji.name); + } + } + if (alreadyExists.length > 0) { + throw new Error(`name already exists: ${alreadyExists.join(', ')}`); + } + + for (const emoji of params) { + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + name: emoji.name, + category: emoji.category, + aliases: emoji.aliases, + license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + originalUrl: emoji.driveFile != null ? emoji.driveFile.url : undefined, + publicUrl: emoji.driveFile != null ? (emoji.driveFile.webpublicUrl ?? emoji.driveFile.url) : undefined, + type: emoji.driveFile != null ? (emoji.driveFile.webpublicType ?? emoji.driveFile.type) : undefined, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, + }); + } + + this.localEmojisCache.refresh(); + + // 名前が変わっていないものはそのまま更新としてイベント発信 + const updateEmojis = params.filter(it => storedEmojis.get(it.id)?.name === it.name); + if (updateEmojis.length > 0) { + const packedList = await this.emojiEntityService.packDetailedMany(updateEmojis); + this.globalEventService.publishBroadcastStream('emojiUpdated', { + emojis: packedList, + }); + } + + // 名前が変わったものは削除・追加としてイベント発信 + const nameChangeEmojis = params.filter(it => storedEmojis.get(it.id)?.name !== it.name); + if (nameChangeEmojis.length > 0) { + const packedList = await this.emojiEntityService.packDetailedMany(nameChangeEmojis); + this.globalEventService.publishBroadcastStream('emojiDeleted', { + emojis: packedList, + }); + + for (const packed of packedList) { + this.globalEventService.publishBroadcastStream('emojiAdded', { + emoji: packed, + }); + } + } + + if (moderator) { + const updatedEmojis = await this.emojisRepository.createQueryBuilder('emoji') + .whereInIds(storedEmojis.keys()) + .getMany() + .then(it => new Map(it.map(it => [it.id, it]))); + for (const emoji of storedEmojis.values()) { + this.moderationLogService.log(moderator, 'updateCustomEmoji', { + emojiId: emoji.id, + before: emoji, + after: updatedEmojis.get(emoji.id), + }); + } + } + } + @bindThis public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ @@ -293,7 +445,7 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { - // クエリに使うホスト + // クエリに使うホスト let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) : this.utilityService.isSelfHost(src) ? null // 自ホスト指定 diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 5b97cfad5e..663cef38ca 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -67,7 +67,7 @@ export class EmojiEntityService { @bindThis public packDetailedMany( emojis: any[], - ) { + ): Promise[]> { return Promise.all(emojis.map(x => this.packDetailed(x))); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 4a9418d051..2b6cc17752 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -44,15 +44,21 @@ export const paramDef = { nullable: true, description: 'Use `null` to reset the category.', }, - aliases: { type: 'array', items: { - type: 'string', - } }, + aliases: { + type: 'array', + items: { + type: 'string', + }, + }, license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { - type: 'string', - } }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', + items: { + type: 'string', + }, + }, }, required: ['name', 'fileId'], } as const; @@ -64,9 +70,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private customEmojiService: CustomEmojiService, - private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 59e87253f6..b2bf6131d0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -21,42 +21,9 @@ export const meta = { res: { type: 'array', - optional: false, nullable: false, items: { type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - aliases: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - category: { - type: 'string', - optional: false, nullable: true, - }, - host: { - type: 'string', - optional: false, nullable: true, - description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.', - }, - url: { - type: 'string', - optional: false, nullable: false, - }, - }, + ref: 'EmojiDetailed', }, }, } as const; @@ -88,15 +55,11 @@ export default class extends Endpoint { // eslint- let emojis: MiEmoji[]; if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); - //const emojis = await q.limit(ps.limit).getMany(); - emojis = await q.getMany(); - const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); - - if (queryarry) { + const queries = ps.query.match(/:([a-z0-9_]*):/g); + if (queries) { emojis = emojis.filter(emoji => - queryarry.includes(`:${emoji.name}:`), + queries.includes(`:${emoji.name}:`), ); } else { emojis = emojis.filter(emoji => diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 8b8b10a237..2adf0a21b3 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -25,18 +25,9 @@ export const meta = { type: 'array', optional: false, nullable: false, items: { - anyOf: [ - { - type: 'object', - optional: false, nullable: false, - ref: 'EmojiSimple', - }, - { - type: 'object', - optional: false, nullable: false, - ref: 'EmojiDetailed', - }, - ], + type: 'object', + optional: false, nullable: false, + ref: 'EmojiSimple', }, }, }, @@ -46,10 +37,6 @@ export const meta = { export const paramDef = { type: 'object', properties: { - detail: { - type: 'boolean', - nullable: true, - }, }, required: [], } as const; @@ -74,9 +61,7 @@ export default class extends Endpoint { // eslint- }); return { - emojis: ps.detail - ? await this.emojiEntityService.packDetailedMany(emojis) - : await this.emojiEntityService.packSimpleMany(emojis), + emojis: await this.emojiEntityService.packSimpleMany(emojis), }; }); } diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index 12acecbe45..05a79dc725 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -21,7 +21,7 @@ {{ cell.value }}
- +
@@ -49,18 +49,17 @@ + + diff --git a/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue b/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue new file mode 100644 index 0000000000..5a71a0fcf1 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/custom-emojis-grid.vue b/packages/frontend/src/pages/admin/custom-emojis-grid.vue index 0de336331c..ebd00c2e51 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-grid.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-grid.vue @@ -2,38 +2,17 @@
- - - - + + + + -
- - - -
- -
- -
- -
- - - - - - - - - ... - - - - +
+ +
@@ -41,49 +20,31 @@