add list v2 endpoint

This commit is contained in:
samunohito 2024-02-05 16:20:35 +09:00
parent d5db737469
commit f8529a01b9
17 changed files with 731 additions and 179 deletions

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setImmediate } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull } from 'typeorm';
import * as Redis from 'ioredis';
@ -21,6 +22,53 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
export const fetchEmojisHostTypes = [
'local',
'remote',
'all',
] as const;
export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number];
export const fetchEmojisSortKeys = [
'id',
'updatedAt',
'name',
'host',
'uri',
'publicUrl',
'type',
'aliases',
'category',
'license',
'isSensitive',
'localOnly',
] as const;
export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number];
export type FetchEmojisParams = {
query?: {
updatedAtFrom?: string;
updatedAtTo?: string;
name?: string;
host?: string;
uri?: string;
publicUrl?: string;
type?: string;
aliases?: string;
category?: string;
license?: string;
isSensitive?: boolean;
localOnly?: boolean;
hostType?: FetchEmojisHostTypes;
},
sinceId?: string;
untilId?: string;
limit?: number;
page?: number;
sort?: {
key : FetchEmojisSortKeys;
order : 'ASC' | 'DESC';
}[]
}
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
private cache: MemoryKVCache<MiEmoji | null>;
@ -99,64 +147,6 @@ 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<MiEmoji[]> {
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;
@ -214,103 +204,6 @@ 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<void> {
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<string>();
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({
@ -545,6 +438,265 @@ export class CustomEmojiService implements OnApplicationShutdown {
return this.emojisRepository.findOneBy({ id });
}
@bindThis
public async fetchEmojis(params?: FetchEmojisParams) {
const builder = this.emojisRepository.createQueryBuilder('emoji');
if (params?.query) {
const q = params.query;
if (q.updatedAtFrom) {
// noIndexScan
builder.andWhere('emoji.updatedAt >= :updateAtFrom', { updateAtFrom: q.updatedAtFrom });
}
if (q.updatedAtTo) {
// noIndexScan
builder.andWhere('emoji.updatedAt <= :updateAtTo', { updateAtTo: q.updatedAtTo });
}
if (q.name) {
builder.andWhere('emoji.name LIKE :name', { name: `%${q.name}%` });
}
if (q.hostType === 'local') {
builder.andWhere('emoji.host LIKE :host', { host: `%${q.host}%` });
} else {
if (q.host) {
// noIndexScan
builder.andWhere('emoji.host LIKE :host', { host: `%${q.host}%` });
} else {
builder.andWhere('emoji.host IS NOT NULL');
}
}
if (q.uri) {
// noIndexScan
builder.andWhere('emoji.uri LIKE :uri', { url: `%${q.uri}%` });
}
if (q.publicUrl) {
// noIndexScan
builder.andWhere('emoji.publicUrl LIKE :publicUrl', { publicUrl: `%${q.publicUrl}%` });
}
if (q.type) {
// noIndexScan
builder.andWhere('emoji.type LIKE :type', { type: `%${q.type}%` });
}
if (q.aliases) {
// noIndexScan
builder.andWhere('emoji.aliases ANY(:aliases)', { aliases: q.aliases });
}
if (q.category) {
// noIndexScan
builder.andWhere('emoji.category LIKE :category', { category: `%${q.category}%` });
}
if (q.license) {
// noIndexScan
builder.andWhere('emoji.license LIKE :license', { license: `%${q.license}%` });
}
if (q.isSensitive != null) {
// noIndexScan
builder.andWhere('emoji.isSensitive = :isSensitive', { isSensitive: q.isSensitive });
}
if (q.localOnly != null) {
// noIndexScan
builder.andWhere('emoji.localOnly = :localOnly', { localOnly: q.localOnly });
}
}
if (params?.sinceId) {
builder.andWhere('emoji.id > :sinceId', { sinceId: params.sinceId });
}
if (params?.untilId) {
builder.andWhere('emoji.id < :untilId', { untilId: params.untilId });
}
if (params?.sort) {
for (const sort of params.sort) {
builder.addOrderBy(`emoji.${sort.key}`, sort.order);
}
} else {
builder.addOrderBy('emoji.id', 'DESC');
}
const limit = params?.limit ?? 10;
if (params?.page) {
builder.skip((params.page - 1) * limit);
}
builder.take(limit);
const [emojis, count] = await builder.getManyAndCount();
return {
emojis,
count: (count > limit ? emojis.length : count),
allCount: count,
allPages: Math.ceil(count / limit),
};
}
@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<MiEmoji[]> {
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')
.where({ id: In(x.identifiers) })
.getMany(),
);
// 以降は絵文字登録による副作用なのでリクエストから切り離して実行
// noinspection ES6MissingAwait
setImmediate(async () => {
const localEmojis = emojis.filter(it => it.host == null);
if (localEmojis.length > 0) {
await this.localEmojisCache.refresh();
const packedEmojis = await this.emojiEntityService.packDetailedMany(localEmojis);
for (const emoji of packedEmojis) {
this.globalEventService.publishBroadcastStream('emojiAdded', { emoji });
}
if (moderator) {
for (const emoji of localEmojis) {
await this.moderationLogService.log(moderator, 'addCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
}
}
});
return emojis;
}
@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<void> {
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<string>();
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,
});
}
// 以降は絵文字更新による副作用なのでリクエストから切り離して実行
// noinspection ES6MissingAwait
setImmediate(async () => {
await 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()) {
await this.moderationLogService.log(moderator, 'updateCustomEmoji', {
emojiId: emoji.id,
before: emoji,
after: updatedEmojis.get(emoji.id),
});
}
}
});
}
@bindThis
public dispose(): void {
this.cache.dispose();

View file

@ -70,5 +70,35 @@ export class EmojiEntityService {
): Promise<Packed<'EmojiDetailed'>[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
@bindThis
public async packDetailedAdmin(
src: MiEmoji['id'] | MiEmoji,
): Promise<Packed<'EmojiDetailedAdmin'>> {
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
return {
id: emoji.id,
updatedAt: emoji.updatedAt?.toISOString() ?? null,
name: emoji.name,
host: emoji.host,
uri: emoji.uri,
type: emoji.type,
aliases: emoji.aliases,
category: emoji.category,
publicUrl: emoji.publicUrl,
license: emoji.license,
localOnly: emoji.localOnly,
isSensitive: emoji.isSensitive,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
};
}
@bindThis
public packDetailedAdminMany(
emojis: any[],
): Promise<Packed<'EmojiDetailedAdmin'>[]> {
return Promise.all(emojis.map(x => this.packDetailedAdmin(x)));
}
}

View file

@ -33,7 +33,11 @@ import { packedClipSchema } from '@/models/json-schema/clip.js';
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
import {
packedEmojiDetailedAdminSchema,
packedEmojiDetailedSchema,
packedEmojiSimpleSchema,
} from '@/models/json-schema/emoji.js';
import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
@ -75,6 +79,7 @@ export const refs = {
GalleryPost: packedGalleryPostSchema,
EmojiSimple: packedEmojiSimpleSchema,
EmojiDetailed: packedEmojiDetailedSchema,
EmojiDetailedAdmin: packedEmojiDetailedAdminSchema,
Flash: packedFlashSchema,
Signin: packedSigninSchema,
RoleLite: packedRoleLiteSchema,

View file

@ -100,3 +100,74 @@ export const packedEmojiDetailedSchema = {
},
},
} as const;
export const packedEmojiDetailedAdminSchema = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
optional: false, nullable: false,
},
updatedAt: {
type: 'string',
format: 'date-time',
optional: false, nullable: true,
},
name: {
type: 'string',
optional: false, nullable: false,
},
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
publicUrl: {
type: 'string',
optional: false, nullable: false,
},
uri: {
type: 'string',
optional: false, nullable: true,
},
type: {
type: 'string',
optional: false, nullable: true,
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
format: 'id',
optional: false, nullable: false,
},
},
category: {
type: 'string',
optional: false, nullable: true,
},
license: {
type: 'string',
optional: false, nullable: true,
},
localOnly: {
type: 'boolean',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisEmojiAsReaction: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
} as const;

View file

@ -43,6 +43,7 @@ import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-al
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_emoji_v2_list from './endpoints/admin/emoji/v2/list.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
@ -414,6 +415,7 @@ const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-ali
const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
const $admin_emoji_v2_list: Provider = { provide: 'ep:admin/emoji/v2/list', useClass: ep___admin_emoji_v2_list.default };
const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
@ -789,6 +791,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_emoji_setCategoryBulk,
$admin_emoji_setLicenseBulk,
$admin_emoji_update,
$admin_emoji_v2_list,
$admin_federation_deleteAllFiles,
$admin_federation_refreshRemoteInstanceMetadata,
$admin_federation_removeAllFollowing,
@ -1158,6 +1161,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_emoji_setCategoryBulk,
$admin_emoji_setLicenseBulk,
$admin_emoji_update,
$admin_emoji_v2_list,
$admin_federation_deleteAllFiles,
$admin_federation_refreshRemoteInstanceMetadata,
$admin_federation_removeAllFollowing,

View file

@ -44,6 +44,7 @@ import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-al
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_emoji_v2_list from './endpoints/admin/emoji/v2/list.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
@ -413,6 +414,7 @@ const eps = [
['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
['admin/emoji/update', ep___admin_emoji_update],
['admin/emoji/v2/list', ep___admin_emoji_v2_list],
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],

View file

@ -0,0 +1,143 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { CustomEmojiService, FetchEmojisParams } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
type: 'object',
properties: {
emojis: {
type: 'array',
items: {
type: 'object',
ref: 'EmojiDetailedAdmin',
},
},
count: { type: 'integer' },
allCount: { type: 'integer' },
allPages: { type: 'integer' },
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
query: {
type: 'object',
nullable: true,
properties: {
updatedAtFrom: { type: 'string' },
updatedAtTo: { type: 'string' },
name: { type: 'string' },
host: { type: 'string' },
uri: { type: 'string' },
publicUrl: { type: 'string' },
type: { type: 'string' },
aliases: { type: 'string' },
category: { type: 'string' },
license: { type: 'string' },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
hostType: { type: 'string', enum: ['local', 'remote', 'all'], default: 'all' },
},
},
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
page: { type: 'integer' },
sort: {
type: 'array',
items: {
type: 'object',
properties: {
key: {
type: 'string',
enum: [
'id',
'updatedAt',
'name',
'host',
'uri',
'publicUrl',
'type',
'aliases',
'category',
'license',
'isSensitive',
'localOnly',
],
default: 'id',
},
order: {
type: 'string',
enum: ['ASC', 'DESC'],
default: 'DESC',
},
},
required: ['key', 'order'],
},
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const params: FetchEmojisParams = {};
if (ps.query) {
params.query = {
updatedAtFrom: ps.query.updatedAtFrom,
updatedAtTo: ps.query.updatedAtTo,
name: ps.query.name,
host: ps.query.host,
uri: ps.query.uri,
publicUrl: ps.query.publicUrl,
type: ps.query.type,
aliases: ps.query.aliases,
category: ps.query.category,
license: ps.query.license,
isSensitive: ps.query.isSensitive,
localOnly: ps.query.localOnly,
hostType: ps.query.hostType,
};
}
params.sinceId = ps.sinceId;
params.untilId = ps.untilId;
params.limit = ps.limit;
params.page = ps.page;
params.sort = ps.sort?.map(it => ({
key: it.key,
order: it.order,
}));
const result = await this.customEmojiService.fetchEmojis(params);
return {
emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis),
count: result.count,
allCount: result.allCount,
allPages: result.allPages,
};
});
}
}

View file

@ -22,12 +22,12 @@ export type GridItem = {
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
}
export function fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
export function fromEmojiDetailedAdmin(it: Misskey.entities.EmojiDetailedAdmin): GridItem {
return {
checked: false,
id: it.id,
fileId: undefined,
url: it.url,
url: it.publicUrl,
name: it.name,
host: it.host ?? '',
category: it.category ?? '',

View file

@ -37,7 +37,7 @@
import { computed, ref, toRefs, watch } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import { fromEmojiDetailedAdmin, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
@ -81,7 +81,7 @@ const emit = defineEmits<{
}>();
const props = defineProps<{
customEmojis: Misskey.entities.EmojiDetailed[];
customEmojis: Misskey.entities.EmojiDetailedAdmin[];
}>();
const { customEmojis } = toRefs(props);
@ -287,7 +287,7 @@ function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState)
}
function refreshGridItems() {
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
gridItems.value = customEmojis.value.map(it => fromEmojiDetailedAdmin(it));
originGridItems.value = JSON.parse(JSON.stringify(gridItems.value));
}

View file

@ -29,22 +29,14 @@ import XRegisterComponent from '@/pages/admin/custom-emojis-grid.local.register.
type PageMode = 'list' | 'register';
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const modeTab = ref<PageMode>('list');
const query = ref<string>();
async function refreshCustomEmojis(query?: string, sinceId?: string, untilId?: string) {
const emojis = await misskeyApi('admin/emoji/list', {
const emojis = await misskeyApi('admin/emoji/v2/list', {
limit: 100,
query: query?.length ? query : undefined,
sinceId,
untilId,
});
if (sinceId) {
// IDsinceId
emojis.reverse();
}
}).then(it => it.emojis);
customEmojis.value = emojis;
}

View file

@ -43,7 +43,7 @@ import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/column.js';
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import { fromEmojiDetailedAdmin, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import {
GridCellContextMenuEvent,
GridCellValueChangeEvent,
@ -187,7 +187,7 @@ async function refreshCustomEmojis(query?: string, host?: string, sinceId?: stri
customEmojis.value = emojis;
console.log(customEmojis.value);
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
gridItems.value = customEmojis.value.map(it => fromEmojiDetailedAdmin(it));
}
onMounted(async () => {

View file

@ -169,6 +169,12 @@ type AdminEmojiSetLicenseBulkRequest = operations['admin/emoji/set-license-bulk'
// @public (undocumented)
type AdminEmojiUpdateRequest = operations['admin/emoji/update']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminEmojiV2ListRequest = operations['admin/emoji/v2/list']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminEmojiV2ListResponse = operations['admin/emoji/v2/list']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminFederationDeleteAllFilesRequest = operations['admin/federation/delete-all-files']['requestBody']['content']['application/json'];
@ -996,6 +1002,9 @@ type EmojiDeleted = {
// @public (undocumented)
type EmojiDetailed = components['schemas']['EmojiDetailed'];
// @public (undocumented)
type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin'];
// @public (undocumented)
type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
@ -1133,6 +1142,8 @@ declare namespace entities {
AdminEmojiSetCategoryBulkRequest,
AdminEmojiSetLicenseBulkRequest,
AdminEmojiUpdateRequest,
AdminEmojiV2ListRequest,
AdminEmojiV2ListResponse,
AdminFederationDeleteAllFilesRequest,
AdminFederationRefreshRemoteInstanceMetadataRequest,
AdminFederationRemoveAllFollowingRequest,
@ -1668,6 +1679,7 @@ declare namespace entities {
GalleryPost,
EmojiSimple,
EmojiDetailed,
EmojiDetailedAdmin,
Flash,
Signin,
RoleLite,

View file

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
* generatedAt: 2024-02-04T07:16:03.625Z
* generatedAt: 2024-02-05T06:03:40.656Z
*/
import type { SwitchCaseResponseType } from '../api.js';
@ -416,6 +416,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
request<E extends 'admin/emoji/v2/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View file

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
* generatedAt: 2024-02-04T07:16:03.623Z
* generatedAt: 2024-02-05T06:03:40.654Z
*/
import type {
@ -54,6 +54,8 @@ import type {
AdminEmojiSetCategoryBulkRequest,
AdminEmojiSetLicenseBulkRequest,
AdminEmojiUpdateRequest,
AdminEmojiV2ListRequest,
AdminEmojiV2ListResponse,
AdminFederationDeleteAllFilesRequest,
AdminFederationRefreshRemoteInstanceMetadataRequest,
AdminFederationRemoveAllFollowingRequest,
@ -596,6 +598,7 @@ export type Endpoints = {
'admin/emoji/set-category-bulk': { req: AdminEmojiSetCategoryBulkRequest; res: EmptyResponse };
'admin/emoji/set-license-bulk': { req: AdminEmojiSetLicenseBulkRequest; res: EmptyResponse };
'admin/emoji/update': { req: AdminEmojiUpdateRequest; res: EmptyResponse };
'admin/emoji/v2/list': { req: AdminEmojiV2ListRequest; res: AdminEmojiV2ListResponse };
'admin/federation/delete-all-files': { req: AdminFederationDeleteAllFilesRequest; res: EmptyResponse };
'admin/federation/refresh-remote-instance-metadata': { req: AdminFederationRefreshRemoteInstanceMetadataRequest; res: EmptyResponse };
'admin/federation/remove-all-following': { req: AdminFederationRemoveAllFollowingRequest; res: EmptyResponse };

View file

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
* generatedAt: 2024-02-04T07:16:03.621Z
* generatedAt: 2024-02-05T06:03:40.652Z
*/
import { operations } from './types.js';
@ -56,6 +56,8 @@ export type AdminEmojiSetAliasesBulkRequest = operations['admin/emoji/set-aliase
export type AdminEmojiSetCategoryBulkRequest = operations['admin/emoji/set-category-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiSetLicenseBulkRequest = operations['admin/emoji/set-license-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiUpdateRequest = operations['admin/emoji/update']['requestBody']['content']['application/json'];
export type AdminEmojiV2ListRequest = operations['admin/emoji/v2/list']['requestBody']['content']['application/json'];
export type AdminEmojiV2ListResponse = operations['admin/emoji/v2/list']['responses']['200']['content']['application/json'];
export type AdminFederationDeleteAllFilesRequest = operations['admin/federation/delete-all-files']['requestBody']['content']['application/json'];
export type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin/federation/refresh-remote-instance-metadata']['requestBody']['content']['application/json'];
export type AdminFederationRemoveAllFollowingRequest = operations['admin/federation/remove-all-following']['requestBody']['content']['application/json'];

View file

@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
* generatedAt: 2024-02-04T07:16:03.620Z
* generatedAt: 2024-02-05T06:03:40.651Z
*/
import { components } from './types.js';
@ -37,6 +37,7 @@ export type FederationInstance = components['schemas']['FederationInstance'];
export type GalleryPost = components['schemas']['GalleryPost'];
export type EmojiSimple = components['schemas']['EmojiSimple'];
export type EmojiDetailed = components['schemas']['EmojiDetailed'];
export type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin'];
export type Flash = components['schemas']['Flash'];
export type Signin = components['schemas']['Signin'];
export type RoleLite = components['schemas']['RoleLite'];

View file

@ -3,7 +3,7 @@
/*
* version: 2024.2.0-beta.7
* generatedAt: 2024-02-04T07:16:03.539Z
* generatedAt: 2024-02-05T06:03:40.574Z
*/
/**
@ -351,6 +351,15 @@ export type paths = {
*/
post: operations['admin/emoji/update'];
};
'/admin/emoji/v2/list': {
/**
* admin/emoji/v2/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
post: operations['admin/emoji/v2/list'];
};
'/admin/federation/delete-all-files': {
/**
* admin/federation/delete-all-files
@ -4281,6 +4290,24 @@ export type components = {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
};
EmojiDetailedAdmin: {
/** Format: id */
id: string;
/** Format: date-time */
updatedAt: string | null;
name: string;
/** @description The local host is represented with `null`. */
host: string | null;
publicUrl: string;
uri: string | null;
type: string | null;
aliases: string[];
category: string | null;
license: string | null;
localOnly: boolean;
isSensitive: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
};
Flash: {
/**
* Format: id
@ -6839,6 +6866,103 @@ export type operations = {
};
};
};
/**
* admin/emoji/v2/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
*/
'admin/emoji/v2/list': {
requestBody: {
content: {
'application/json': {
query?: ({
/** Format: date-time */
updatedAtFrom?: string;
/** Format: date-time */
updatedAtTo?: string;
name?: string;
host?: string;
uri?: string;
publicUrl?: string;
type?: string;
aliases?: string;
category?: string;
license?: string;
isSensitive?: boolean;
localOnly?: boolean;
/**
* @default all
* @enum {string}
*/
hostType?: 'local' | 'remote' | 'all';
}) | null;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
/** @default 10 */
limit?: number;
page?: number;
sort?: ({
/**
* @default id
* @enum {string}
*/
key: 'id' | 'updatedAt' | 'name' | 'host' | 'uri' | 'publicUrl' | 'type' | 'aliases' | 'category' | 'license' | 'isSensitive' | 'localOnly';
/**
* @default DESC
* @enum {string}
*/
order: 'ASC' | 'DESC';
})[];
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
emojis: components['schemas']['EmojiDetailedAdmin'][];
count: number;
allCount: number;
allPages: number;
};
};
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/federation/delete-all-files
* @description No description provided.