diff --git a/locales/index.d.ts b/locales/index.d.ts index 42f382dcb6ab..56b6045feaa5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5096,6 +5096,10 @@ export interface Locale extends ILocale { * 新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。 */ "prohibitSkippingInitialTutorialDescription": string; + /** + * エクスポートしたいカテゴリを選択 + */ + "selectCategoryYouWantToExport": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 66bf161ac188..bacd14fb57b1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1270,6 +1270,7 @@ createdAntennas: "作成したアンテナ" clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする" prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。" +selectCategoryYouWantToExport: "エクスポートしたいカテゴリを選択" _delivery: status: "配信状態" diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 37f242f63cfc..a0161f830238 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -183,9 +183,10 @@ export class QueueService { } @bindThis - public createExportCustomEmojisJob(user: ThinUser) { + public createExportCustomEmojisJob(user: ThinUser, opts: { categories?: (string | null)[]; } = {}) { return this.dbQueue.add('exportCustomEmojis', { user: { id: user.id }, + categories: opts.categories, }, { removeOnComplete: true, removeOnFail: true, diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index e4eb4791bd7f..8a87233fd63d 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -75,7 +75,7 @@ export class ExportCustomEmojisProcessorService { await writeMeta(`{"metaVersion":2,"host":"${this.config.host}","exportedAt":"${new Date().toString()}","emojis":[`); - const customEmojis = await this.emojisRepository.find({ + let customEmojis = await this.emojisRepository.find({ where: { host: IsNull(), }, @@ -84,6 +84,20 @@ export class ExportCustomEmojisProcessorService { }, }); + // job.data.categoriesに指定がある場合はそのカテゴリのみをエクスポート + const categories = job.data.categories as (string | null)[] | null; + this.logger.info(`Exporting categories: ${categories}`); + if (categories != null) { + customEmojis = customEmojis.filter(emoji => { + if (emoji.category == null || emoji.category === 'null') { + // カテゴリがnull、つまりカテゴリなしのもの + return categories.includes(null) || categories.includes('null'); + } else { + return categories.includes(emoji.category); + } + }); + } + for (const emoji of customEmojis) { if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) { this.logger.error(`invalid emoji name: ${emoji.name}`); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index de888bda092f..c4cd7b8f94a9 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -41,7 +41,7 @@ export type DbJobData = DbJobMap[T]; export type DbJobMap = { deleteDriveFiles: DbJobDataWithUser; - exportCustomEmojis: DbJobDataWithUser; + exportCustomEmojis: DbExportCustomEmojisJobData; exportAntennas: DBExportAntennasData; exportNotes: DbJobDataWithUser; exportFavorites: DbJobDataWithUser; @@ -71,6 +71,11 @@ export type DbJobDataWithUser = { user: ThinUser; } +export type DbExportCustomEmojisJobData = { + user: ThinUser; + categories?: string[] +} + export type DbExportFollowingData = { user: ThinUser; excludeMuting: boolean; diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index 5ff099524df7..bd41862463a8 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -19,7 +19,17 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + categories: { + type: 'array', + items: { + type: 'string', + nullable: true, + }, + description: 'If null is provided instead of array, all emojis will be exported. If null is provided as an element of the array, emojis without category will be exported.', + nullable: true, + }, + }, required: [], } as const; @@ -29,7 +39,7 @@ export default class extends Endpoint { // eslint- private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { - this.queueService.createExportCustomEmojisJob(me); + this.queueService.createExportCustomEmojisJob(me, { categories: ps.categories ?? [] }); }); } } diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index eea3f681301b..4ac9e1b6c298 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -84,6 +84,8 @@ import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import type { EnumItem } from '@/scripts/form.js'; +import { customEmojiCategories } from '@/custom-emojis.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const emojisPaginationComponent = shallowRef>(); @@ -180,7 +182,41 @@ const menu = (ev: MouseEvent) => { icon: 'ti ti-download', text: i18n.ts.export, action: async () => { + const emojiCategoryItems = customEmojiCategories.value.map(category => { + if (category == null) { + return { value: '_NULL_', label: i18n.ts.other }; + } else { + return { value: category, label: category }; + } + }) satisfies EnumItem[]; + + // TODO: 複数選択可にする + const { canceled, result } = await os.form(i18n.ts.selectCategoryYouWantToExport, { + category: { + type: 'enum', + label: i18n.ts.category, + enum: [ + { value: '_ALL_', label: i18n.ts.all }, + ...emojiCategoryItems, + ], + default: '_ALL_', + }, + }); + + if (canceled) return; + + let categories: (string | null)[] | null = null; + + if (result.category === '_ALL_') { + categories = null; + } else if (result.category === '_NULL_') { + categories = [null]; + } else { + categories = [result.category]; + } + misskeyApi('export-custom-emojis', { + categories, }) .then(() => { os.alert({ diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 242a504c3b5f..79f2bf8107ba 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -5,7 +5,7 @@ import * as Misskey from 'misskey-js'; -type EnumItem = string | { +export type EnumItem = string | { label: string; value: string; }; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index ca1f4a32539c..b95448a5defa 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1439,6 +1439,7 @@ declare namespace entities { EndpointRequest, EndpointResponse, EndpointsResponse, + ExportCustomEmojisRequest, FederationFollowersRequest, FederationFollowersResponse, FederationFollowingRequest, @@ -1828,6 +1829,9 @@ export { entities } // @public (undocumented) type Error_2 = components['schemas']['Error']; +// @public (undocumented) +type ExportCustomEmojisRequest = operations['export-custom-emojis']['requestBody']['content']['application/json']; + // @public (undocumented) type FederationFollowersRequest = operations['federation___followers']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index f4778316a610..0084a07be354 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -248,6 +248,7 @@ import type { EndpointRequest, EndpointResponse, EndpointsResponse, + ExportCustomEmojisRequest, FederationFollowersRequest, FederationFollowersResponse, FederationFollowingRequest, @@ -741,7 +742,7 @@ export type Endpoints = { 'email-address/available': { req: EmailAddressAvailableRequest; res: EmailAddressAvailableResponse }; 'endpoint': { req: EndpointRequest; res: EndpointResponse }; 'endpoints': { req: EmptyRequest; res: EndpointsResponse }; - 'export-custom-emojis': { req: EmptyRequest; res: EmptyResponse }; + 'export-custom-emojis': { req: ExportCustomEmojisRequest; res: EmptyResponse }; 'federation/followers': { req: FederationFollowersRequest; res: FederationFollowersResponse }; 'federation/following': { req: FederationFollowingRequest; res: FederationFollowingResponse }; 'federation/instances': { req: FederationInstancesRequest; res: FederationInstancesResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index ee9eaed42250..0c12b3e80d50 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -251,6 +251,7 @@ export type EmailAddressAvailableResponse = operations['email-address___availabl export type EndpointRequest = operations['endpoint']['requestBody']['content']['application/json']; export type EndpointResponse = operations['endpoint']['responses']['200']['content']['application/json']; export type EndpointsResponse = operations['endpoints']['responses']['200']['content']['application/json']; +export type ExportCustomEmojisRequest = operations['export-custom-emojis']['requestBody']['content']['application/json']; export type FederationFollowersRequest = operations['federation___followers']['requestBody']['content']['application/json']; export type FederationFollowersResponse = operations['federation___followers']['responses']['200']['content']['application/json']; export type FederationFollowingRequest = operations['federation___following']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index b79dc8b8c60e..dd0f52dfdeb9 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -14905,6 +14905,14 @@ export type operations = { * **Credential required**: *Yes* */ 'export-custom-emojis': { + requestBody: { + content: { + 'application/json': { + /** @description If null is provided instead of array, all emojis will be exported. If null is provided as an element of the array, emojis without category will be exported. */ + categories?: ((string | null)[]) | null; + }; + }; + }; responses: { /** @description OK (without any results) */ 204: {