diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 193bb4831d..b4cca8cc1d 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import { In, IsNull } from 'typeorm'; +import { Brackets, In, IsNull, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; @@ -40,6 +40,7 @@ export const fetchEmojisSortKeys = [ 'license', 'isSensitive', 'localOnly', + 'roleIdsThatCanBeUsedThisEmojiAsReaction', ] as const; export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number]; export type FetchEmojisParams = { @@ -57,14 +58,15 @@ export type FetchEmojisParams = { isSensitive?: boolean; localOnly?: boolean; hostType?: FetchEmojisHostTypes; + roleIds?: string[]; }, sinceId?: string; untilId?: string; limit?: number; page?: number; sort?: { - key : FetchEmojisSortKeys; - direction : 'ASC' | 'DESC'; + key: FetchEmojisSortKeys; + direction: 'ASC' | 'DESC'; }[] } @@ -76,10 +78,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, @@ -441,6 +441,19 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public async fetchEmojis(params?: FetchEmojisParams) { + function multipleWordsToQuery( + query: string, + builder: SelectQueryBuilder, + action: (qb: WhereExpressionBuilder, word: string) => void, + ) { + const words = query.split(/\s/); + builder.andWhere(new Brackets((qb => { + for (const word of words) { + action(qb, word); + } + }))); + } + const builder = this.emojisRepository.createQueryBuilder('emoji'); if (params?.query) { const q = params.query; @@ -453,41 +466,64 @@ export class CustomEmojiService implements OnApplicationShutdown { builder.andWhere('emoji.updatedAt <= :updateAtTo', { updateAtTo: q.updatedAtTo }); } if (q.name) { - builder.andWhere('emoji.name LIKE :name', { name: `%${q.name}%` }); + multipleWordsToQuery(q.name, builder, (qb, word) => { + qb.orWhere('emoji.name LIKE :name', { name: `%${word}%` }); + }); } - if (q.hostType === 'local') { - builder.andWhere('emoji.host IS NULL'); - } else { - if (q.host) { - // noIndexScan - builder.andWhere('emoji.host LIKE :host', { host: `%${q.host}%` }); - } else { - builder.andWhere('emoji.host IS NOT NULL'); + + switch (true) { + case q.hostType === 'local': { + builder.andWhere('emoji.host IS NULL'); + break; + } + case q.hostType === 'remote': { + if (q.host) { + // noIndexScan + multipleWordsToQuery(q.host, builder, (qb, word) => { + qb.orWhere('emoji.host LIKE :host', { host: `%${word}%` }); + }); + } else { + builder.andWhere('emoji.host IS NOT NULL'); + } + break; } } + if (q.uri) { // noIndexScan - builder.andWhere('emoji.uri LIKE :uri', { url: `%${q.uri}%` }); + multipleWordsToQuery(q.uri, builder, (qb, word) => { + qb.orWhere('emoji.uri LIKE :uri', { uri: `%${word}%` }); + }); } if (q.publicUrl) { // noIndexScan - builder.andWhere('emoji.publicUrl LIKE :publicUrl', { publicUrl: `%${q.publicUrl}%` }); + multipleWordsToQuery(q.publicUrl, builder, (qb, word) => { + qb.orWhere('emoji.publicUrl LIKE :publicUrl', { publicUrl: `%${word}%` }); + }); } if (q.type) { // noIndexScan - builder.andWhere('emoji.type LIKE :type', { type: `%${q.type}%` }); + multipleWordsToQuery(q.type, builder, (qb, word) => { + qb.orWhere('emoji.type LIKE :type', { type: `%${word}%` }); + }); } if (q.aliases) { // noIndexScan - builder.andWhere('emoji.aliases ANY(:aliases)', { aliases: q.aliases }); + multipleWordsToQuery(q.aliases, builder, (qb, word) => { + qb.orWhere('emoji.aliases LIKE :aliases', { aliases: `%${word}%` }); + }); } if (q.category) { // noIndexScan - builder.andWhere('emoji.category LIKE :category', { category: `%${q.category}%` }); + multipleWordsToQuery(q.category, builder, (qb, word) => { + qb.orWhere('emoji.category LIKE :category', { category: `%${word}%` }); + }); } if (q.license) { // noIndexScan - builder.andWhere('emoji.license LIKE :license', { license: `%${q.license}%` }); + multipleWordsToQuery(q.license, builder, (qb, word) => { + qb.orWhere('emoji.license LIKE :license', { license: `%${word}%` }); + }); } if (q.isSensitive != null) { // noIndexScan diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 58c9c5bb3c..6515089fe1 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -4,10 +4,10 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository } from '@/models/_.js'; +import type { EmojisRepository, MiRole, RolesRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; import type { MiEmoji } from '@/models/Emoji.js'; import { bindThis } from '@/decorators.js'; @@ -16,6 +16,8 @@ export class EmojiEntityService { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, ) { } @@ -75,9 +77,28 @@ export class EmojiEntityService { @bindThis public async packDetailedAdmin( src: MiEmoji['id'] | MiEmoji, + hint?: { + roles?: Map + }, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + const roles = Array.of(); + if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0) { + if (hint?.roles) { + const hintRoles = hint.roles; + roles.push( + ...emoji.roleIdsThatCanBeUsedThisEmojiAsReaction + .filter(x => hintRoles.has(x)) + .map(x => hintRoles.get(x)!), + ); + } else { + roles.push( + ...await this.rolesRepository.findBy({ id: In(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) }), + ); + } + } + return { id: emoji.id, updatedAt: emoji.updatedAt?.toISOString() ?? null, @@ -92,15 +113,39 @@ export class EmojiEntityService { license: emoji.license, localOnly: emoji.localOnly, isSensitive: emoji.isSensitive, - roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + roleIdsThatCanBeUsedThisEmojiAsReaction: roles.map(it => ({ id: it.id, name: it.name })), }; } @bindThis - public packDetailedAdminMany( - emojis: any[], + public async packDetailedAdminMany( + emojis: MiEmoji['id'][] | MiEmoji[], + hint?: { + roles?: Map + }, ): Promise[]> { - return Promise.all(emojis.map(x => this.packDetailedAdmin(x))); + // IDのみの要素をピックアップし、DBからレコードを取り出して他の値を補完する + const emojiEntities = emojis.filter(x => typeof x === 'object') as MiEmoji[]; + const emojiIdOnlyList = emojis.filter(x => typeof x === 'string') as string[]; + if (emojiIdOnlyList.length > 0) { + emojiEntities.push(...await this.emojisRepository.findBy({ id: In(emojiIdOnlyList) })); + } + + // 特定ロール専用の絵文字である場合、そのロール情報をあらかじめまとめて取得しておく(pack側で都度取得も出来るが負荷が高いので) + let hintRoles: Map; + if (hint?.roles) { + hintRoles = hint.roles; + } else { + const roles = Array.of(); + const roleIds = [...new Set(emojiEntities.flatMap(x => x.roleIdsThatCanBeUsedThisEmojiAsReaction))]; + if (roleIds.length > 0) { + roles.push(...await this.rolesRepository.findBy({ id: In(roleIds) })); + } + + hintRoles = new Map(roles.map(x => [x.id, x])); + } + + return Promise.all(emojis.map(x => this.packDetailedAdmin(x, { roles: hintRoles }))); } } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index bc08b93401..3cd263fa37 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -170,11 +170,19 @@ export const packedEmojiDetailedAdminSchema = { }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', - optional: false, nullable: false, items: { - type: 'string', - optional: false, nullable: false, - format: 'id', + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/v2/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/v2/list.ts index c2139c37de..a65f9bca3a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/v2/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/v2/list.ts @@ -53,6 +53,10 @@ export const paramDef = { isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, hostType: { type: 'string', enum: ['local', 'remote', 'all'], default: 'all' }, + roleIds: { + type: 'array', + items: { type: 'string', format: 'misskey:id' }, + }, }, }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -79,6 +83,7 @@ export const paramDef = { 'license', 'isSensitive', 'localOnly', + 'roleIdsThatCanBeUsedThisEmojiAsReaction', ], default: 'id', }, @@ -119,6 +124,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.query.isSensitive, localOnly: ps.query.localOnly, hostType: ps.query.hostType, + roleIds: ps.query.roleIds, }; } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 4698f98121..61b68c689d 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4462,7 +4462,11 @@ export type components = { license: string | null; localOnly: boolean; isSensitive: boolean; - roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + roleIdsThatCanBeUsedThisEmojiAsReaction: { + /** Format: misskey:id */ + id: string; + name: string; + }[]; }; Flash: { /** @@ -6963,6 +6967,7 @@ export type operations = { * @enum {string} */ hostType?: 'local' | 'remote' | 'all'; + roleIds?: string[]; }) | null; /** Format: misskey:id */ sinceId?: string; @@ -6976,7 +6981,7 @@ export type operations = { * @default id * @enum {string} */ - key: 'id' | 'updatedAt' | 'name' | 'host' | 'uri' | 'publicUrl' | 'type' | 'aliases' | 'category' | 'license' | 'isSensitive' | 'localOnly'; + key: 'id' | 'updatedAt' | 'name' | 'host' | 'uri' | 'publicUrl' | 'type' | 'aliases' | 'category' | 'license' | 'isSensitive' | 'localOnly' | 'roleIdsThatCanBeUsedThisEmojiAsReaction'; /** * @default DESC * @enum {string}