add role name

This commit is contained in:
samunohito 2024-02-15 13:20:22 +09:00
parent 089682c08d
commit 4bbf0457fa
5 changed files with 132 additions and 32 deletions

View File

@ -4,7 +4,7 @@
*/ */
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; 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 * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -40,6 +40,7 @@ export const fetchEmojisSortKeys = [
'license', 'license',
'isSensitive', 'isSensitive',
'localOnly', 'localOnly',
'roleIdsThatCanBeUsedThisEmojiAsReaction',
] as const; ] as const;
export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number]; export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number];
export type FetchEmojisParams = { export type FetchEmojisParams = {
@ -57,6 +58,7 @@ export type FetchEmojisParams = {
isSensitive?: boolean; isSensitive?: boolean;
localOnly?: boolean; localOnly?: boolean;
hostType?: FetchEmojisHostTypes; hostType?: FetchEmojisHostTypes;
roleIds?: string[];
}, },
sinceId?: string; sinceId?: string;
untilId?: string; untilId?: string;
@ -76,10 +78,8 @@ export class CustomEmojiService implements OnApplicationShutdown {
constructor( constructor(
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
@ -441,6 +441,19 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetchEmojis(params?: FetchEmojisParams) { public async fetchEmojis(params?: FetchEmojisParams) {
function multipleWordsToQuery(
query: string,
builder: SelectQueryBuilder<MiEmoji>,
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'); const builder = this.emojisRepository.createQueryBuilder('emoji');
if (params?.query) { if (params?.query) {
const q = params.query; const q = params.query;
@ -453,41 +466,64 @@ export class CustomEmojiService implements OnApplicationShutdown {
builder.andWhere('emoji.updatedAt <= :updateAtTo', { updateAtTo: q.updatedAtTo }); builder.andWhere('emoji.updatedAt <= :updateAtTo', { updateAtTo: q.updatedAtTo });
} }
if (q.name) { 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') {
switch (true) {
case q.hostType === 'local': {
builder.andWhere('emoji.host IS NULL'); builder.andWhere('emoji.host IS NULL');
} else { break;
}
case q.hostType === 'remote': {
if (q.host) { if (q.host) {
// noIndexScan // noIndexScan
builder.andWhere('emoji.host LIKE :host', { host: `%${q.host}%` }); multipleWordsToQuery(q.host, builder, (qb, word) => {
qb.orWhere('emoji.host LIKE :host', { host: `%${word}%` });
});
} else { } else {
builder.andWhere('emoji.host IS NOT NULL'); builder.andWhere('emoji.host IS NOT NULL');
} }
break;
} }
}
if (q.uri) { if (q.uri) {
// noIndexScan // 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) { if (q.publicUrl) {
// noIndexScan // 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) { if (q.type) {
// noIndexScan // 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) { if (q.aliases) {
// noIndexScan // 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) { if (q.category) {
// noIndexScan // 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) { if (q.license) {
// noIndexScan // 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) { if (q.isSensitive != null) {
// noIndexScan // noIndexScan

View File

@ -4,10 +4,10 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; 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 { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -16,6 +16,8 @@ export class EmojiEntityService {
constructor( constructor(
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,
) { ) {
} }
@ -75,9 +77,28 @@ export class EmojiEntityService {
@bindThis @bindThis
public async packDetailedAdmin( public async packDetailedAdmin(
src: MiEmoji['id'] | MiEmoji, src: MiEmoji['id'] | MiEmoji,
hint?: {
roles?: Map<MiRole['id'], MiRole>
},
): Promise<Packed<'EmojiDetailedAdmin'>> { ): Promise<Packed<'EmojiDetailedAdmin'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
const roles = Array.of<MiRole>();
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 { return {
id: emoji.id, id: emoji.id,
updatedAt: emoji.updatedAt?.toISOString() ?? null, updatedAt: emoji.updatedAt?.toISOString() ?? null,
@ -92,15 +113,39 @@ export class EmojiEntityService {
license: emoji.license, license: emoji.license,
localOnly: emoji.localOnly, localOnly: emoji.localOnly,
isSensitive: emoji.isSensitive, isSensitive: emoji.isSensitive,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, roleIdsThatCanBeUsedThisEmojiAsReaction: roles.map(it => ({ id: it.id, name: it.name })),
}; };
} }
@bindThis @bindThis
public packDetailedAdminMany( public async packDetailedAdminMany(
emojis: any[], emojis: MiEmoji['id'][] | MiEmoji[],
hint?: {
roles?: Map<MiRole['id'], MiRole>
},
): Promise<Packed<'EmojiDetailedAdmin'>[]> { ): Promise<Packed<'EmojiDetailedAdmin'>[]> {
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<MiRole['id'], MiRole>;
if (hint?.roles) {
hintRoles = hint.roles;
} else {
const roles = Array.of<MiRole>();
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 })));
} }
} }

View File

@ -170,11 +170,19 @@ export const packedEmojiDetailedAdminSchema = {
}, },
roleIdsThatCanBeUsedThisEmojiAsReaction: { roleIdsThatCanBeUsedThisEmojiAsReaction: {
type: 'array', type: 'array',
optional: false, nullable: false,
items: { items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
optional: false, nullable: false,
},
name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
format: 'id', },
},
}, },
}, },
}, },

View File

@ -53,6 +53,10 @@ export const paramDef = {
isSensitive: { type: 'boolean' }, isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' }, localOnly: { type: 'boolean' },
hostType: { type: 'string', enum: ['local', 'remote', 'all'], default: 'all' }, hostType: { type: 'string', enum: ['local', 'remote', 'all'], default: 'all' },
roleIds: {
type: 'array',
items: { type: 'string', format: 'misskey:id' },
},
}, },
}, },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
@ -79,6 +83,7 @@ export const paramDef = {
'license', 'license',
'isSensitive', 'isSensitive',
'localOnly', 'localOnly',
'roleIdsThatCanBeUsedThisEmojiAsReaction',
], ],
default: 'id', default: 'id',
}, },
@ -119,6 +124,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.query.isSensitive, isSensitive: ps.query.isSensitive,
localOnly: ps.query.localOnly, localOnly: ps.query.localOnly,
hostType: ps.query.hostType, hostType: ps.query.hostType,
roleIds: ps.query.roleIds,
}; };
} }

View File

@ -4462,7 +4462,11 @@ export type components = {
license: string | null; license: string | null;
localOnly: boolean; localOnly: boolean;
isSensitive: boolean; isSensitive: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; roleIdsThatCanBeUsedThisEmojiAsReaction: {
/** Format: misskey:id */
id: string;
name: string;
}[];
}; };
Flash: { Flash: {
/** /**
@ -6963,6 +6967,7 @@ export type operations = {
* @enum {string} * @enum {string}
*/ */
hostType?: 'local' | 'remote' | 'all'; hostType?: 'local' | 'remote' | 'all';
roleIds?: string[];
}) | null; }) | null;
/** Format: misskey:id */ /** Format: misskey:id */
sinceId?: string; sinceId?: string;
@ -6976,7 +6981,7 @@ export type operations = {
* @default id * @default id
* @enum {string} * @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 * @default DESC
* @enum {string} * @enum {string}