diff --git a/CHANGELOG.md b/CHANGELOG.md index 5695b5e392..99b3fb7bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,6 @@ - Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 -- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client - Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 diff --git a/locales/index.d.ts b/locales/index.d.ts index a437b95689..71176daf8d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -279,6 +279,7 @@ export interface Locale { "imageUrl": string; "remove": string; "removed": string; + "requestApprovalAreYouSure": string; "removeAreYouSure": string; "deleteAreYouSure": string; "undraftAreYouSure": string; @@ -869,9 +870,9 @@ export interface Locale { "high": string; "middle": string; "low": string; + "list": string; "GamingSpeedChange": string; "GamingSpeedChangeInfo": string; - "list": string; "emailNotConfiguredWarning": string; "ratio": string; "showVisibilityColor": string; @@ -1077,6 +1078,9 @@ export interface Locale { "license": string; "draft": string; "undrafted": string; + "requestPending": string; + "approval": string; + "requestingEmojis": string; "unfavoriteConfirm": string; "myClips": string; "drivecleaner": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f6858bf8be..faabbd4984 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -276,6 +276,7 @@ announcements: "お知らせ" imageUrl: "画像URL" remove: "削除" removed: "削除しました" +requestApprovalAreYouSure: "「{x}」のリクエストを承認しますか?" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" undraftAreYouSure: "「{x}」をドラフト解除しますか?" @@ -866,9 +867,9 @@ priority: "優先度" high: "高" middle: "中" low: "低" +list: "一覧" GamingSpeedChange: "ゲーミングの光るスピードの調整" GamingSpeedChangeInfo: "左にすれば早くなる、右にすれば遅くなる。それだけ。" -list: "一覧" emailNotConfiguredWarning: "メールアドレスの設定がされていません。" ratio: "比率" showVisibilityColor: "ノートの公開範囲を色付けする" @@ -1072,6 +1073,9 @@ hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" +requestPending: "申請中" +approval: "承認" +requestingEmojis: "リクエストされている絵文字" draft: "ドラフト" undrafted: "ドラフト解除" unfavoriteConfirm: "お気に入り解除しますか?" diff --git a/packages/backend/migration/1698131657286-EmojiRequest.js b/packages/backend/migration/1698131657286-EmojiRequest.js new file mode 100644 index 0000000000..7db57c9cd0 --- /dev/null +++ b/packages/backend/migration/1698131657286-EmojiRequest.js @@ -0,0 +1,13 @@ +export class EmojiRequest1698131657286 { + name = 'EmojiRequest1698131657286' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "emoji_request" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "name" character varying(128) NOT NULL, "category" character varying(128), "originalUrl" character varying(512) NOT NULL, "publicUrl" character varying(512) NOT NULL DEFAULT '', "type" character varying(64), "aliases" character varying(128) array NOT NULL DEFAULT '{}', "license" character varying(1024), "fileId" character varying(1024) NOT NULL, "localOnly" boolean NOT NULL DEFAULT false, "isSensitive" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c74521e048dc744f0c7eb65f4a" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ea1d771e867e9843300f09d02c" ON "emoji_request" ("name") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_ea1d771e867e9843300f09d02c"`); + await queryRunner.query(`DROP TABLE "emoji_request"`); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index bc6d24b951..da2f73dbc9 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -90,6 +90,7 @@ import { ClipEntityService } from './entities/ClipEntityService.js'; import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; import { EmojiEntityService } from './entities/EmojiEntityService.js'; +import { EmojiRequestsEntityService } from './entities/EmojiRequestsEntityService.js'; import { FollowingEntityService } from './entities/FollowingEntityService.js'; import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js'; import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js'; @@ -225,6 +226,7 @@ const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService }; const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useExisting: EmojiEntityService }; +const $EmojiRequestsEntityService: Provider = { provide: 'EmojiRequestsEntityService', useExisting: EmojiRequestsEntityService }; const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useExisting: FollowingEntityService }; const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useExisting: FollowRequestEntityService }; const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useExisting: GalleryLikeEntityService }; @@ -360,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting DriveFileEntityService, DriveFolderEntityService, EmojiEntityService, + EmojiRequestsEntityService, FollowingEntityService, FollowRequestEntityService, GalleryLikeEntityService, @@ -490,6 +493,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $DriveFileEntityService, $DriveFolderEntityService, $EmojiEntityService, + $EmojiRequestsEntityService, $FollowingEntityService, $FollowRequestEntityService, $GalleryLikeEntityService, @@ -620,6 +624,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting DriveFileEntityService, DriveFolderEntityService, EmojiEntityService, + EmojiRequestsEntityService, FollowingEntityService, FollowRequestEntityService, GalleryLikeEntityService, @@ -749,6 +754,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $DriveFileEntityService, $DriveFolderEntityService, $EmojiEntityService, + $EmojiRequestsEntityService, $FollowingEntityService, $FollowRequestEntityService, $GalleryLikeEntityService, diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 80acc4042b..b9a1181a69 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -12,12 +12,14 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js'; +import type { EmojisRepository, EmojiRequestsRepository, MiRole, MiUser } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; + const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() @@ -32,6 +34,9 @@ export class CustomEmojiService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + private utilityService: UtilityService, private idService: IdService, private emojiEntityService: EmojiEntityService, @@ -54,6 +59,41 @@ export class CustomEmojiService implements OnApplicationShutdown { }); } + @bindThis + public async request(data: { + driveFile: MiDriveFile; + name: string; + category: string | null; + aliases: string[]; + license: string | null; + isSensitive: boolean; + localOnly: boolean; + }, me?: MiUser): Promise { + const emoji = await this.emojiRequestsRepository.insert({ + id: this.idService.gen(), + updatedAt: new Date(), + name: data.name, + category: data.category, + aliases: data.aliases, + originalUrl: data.driveFile.url, + publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, + type: data.driveFile.webpublicType ?? data.driveFile.type, + license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + fileId: data.driveFile.id, + }).then(x => this.emojiRequestsRepository.findOneByOrFail(x.identifiers[0])); + + if (me) { + this.moderationLogService.log(me, 'addCustomEmoji', { + emojiId: emoji.id, + emoji: emoji, + }); + } + + return emoji; + } + @bindThis public async add(data: { driveFile: MiDriveFile; @@ -64,7 +104,6 @@ export class CustomEmojiService implements OnApplicationShutdown { license: string | null; isSensitive: boolean; localOnly: boolean; - draft: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.insert({ @@ -81,7 +120,6 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, - draft: data.draft, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { @@ -111,7 +149,6 @@ export class CustomEmojiService implements OnApplicationShutdown { license?: string | null; isSensitive?: boolean; localOnly?: boolean; - draft: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); @@ -126,7 +163,6 @@ export class CustomEmojiService implements OnApplicationShutdown { license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, - draft: data.draft, originalUrl: data.driveFile != null ? data.driveFile.url : undefined, publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, @@ -161,6 +197,36 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + @bindThis + public async updateRequest(id: MiEmojiRequest['id'], data: { + driveFile?: MiDriveFile; + name?: string; + category?: string | null; + aliases?: string[]; + license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + }, moderator?: MiUser): Promise { + const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id }); + const sameNameEmoji = await this.emojiRequestsRepository.findOneBy({ name: data.name }); + if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); + + await this.emojiRequestsRepository.update(emoji.id, { + updatedAt: new Date(), + name: data.name, + category: data.category, + aliases: data.aliases, + license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + originalUrl: data.driveFile != null ? data.driveFile.url : undefined, + publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, + type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, + }); + + this.localEmojisCache.refresh(); + } + @bindThis public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ @@ -232,36 +298,7 @@ export class CustomEmojiService implements OnApplicationShutdown { emojis: await this.emojiEntityService.packDetailedMany(ids), }); } - @bindThis - public async setLocalOnlyBulk(ids: MiEmoji['id'][], localOnly: boolean | false) { - await this.emojisRepository.update({ - id: In(ids), - }, { - updatedAt: new Date(), - localOnly: localOnly, - }); - this.localEmojisCache.refresh(); - - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packDetailedMany(ids), - }); - } - @bindThis - public async setisSensitiveBulk(ids: MiEmoji['id'][], isSensitive: boolean | false) { - await this.emojisRepository.update({ - id: In(ids), - }, { - updatedAt: new Date(), - isSensitive: isSensitive, - }); - - this.localEmojisCache.refresh(); - - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packDetailedMany(ids), - }); - } @bindThis public async setLicenseBulk(ids: MiEmoji['id'][], license: string | null) { await this.emojisRepository.update({ @@ -298,6 +335,13 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + @bindThis + public async deleteRequest(id: MiEmojiRequest['id']) { + const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id }); + + await this.emojiRequestsRepository.delete(emoji.id); + } + @bindThis public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) { const emojis = await this.emojisRepository.findBy({ @@ -419,11 +463,21 @@ export class CustomEmojiService implements OnApplicationShutdown { return this.emojisRepository.exist({ where: { name, host: IsNull() } }); } + @bindThis + public checkRequestDuplicate(name: string): Promise { + return this.emojiRequestsRepository.exist({ where: { name } }); + } + @bindThis public getEmojiById(id: string): Promise { return this.emojisRepository.findOneBy({ id }); } + @bindThis + public getEmojiRequestById(id: string): Promise { + return this.emojiRequestsRepository.findOneBy({ id }); + } + @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7a7ee1a13a..64cb12d4f5 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -72,6 +72,7 @@ export const DEFAULT_POLICIES: RolePolicies = { inviteLimitCycle: 60 * 24 * 7, inviteExpirationTime: 0, canManageCustomEmojis: false, + canRequestCustomEmojis: false, canManageAvatarDecorations: false, canRequestCustomEmojis: false, canSearchNotes: false, @@ -336,6 +337,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), + canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)), canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)), canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 14be000367..9ff986592e 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -133,7 +133,10 @@ export class DriveFileEntityService { } return url; } - + @bindThis + public async getFromUrl(url: string): Promise { + return this.driveFilesRepository.findOneBy({ url: url }); + } @bindThis public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise { const id = typeof user === 'object' ? user.id : user; diff --git a/packages/backend/src/core/entities/EmojiRequestsEntityService.ts b/packages/backend/src/core/entities/EmojiRequestsEntityService.ts new file mode 100644 index 0000000000..46308e654c --- /dev/null +++ b/packages/backend/src/core/entities/EmojiRequestsEntityService.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { EmojiRequestsRepository } from '@/models/_.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { bindThis } from '@/decorators.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; + +@Injectable() +export class EmojiRequestsEntityService { + constructor( + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + ) { + } + + @bindThis + public async packSimple( + src: MiEmojiRequest['id'] | MiEmojiRequest, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src }); + + return { + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url: emoji.publicUrl, + isSensitive: emoji.isSensitive ? true : undefined, + }; + } + + @bindThis + public packSimpleMany( + emojis: any[], + ) { + return Promise.all(emojis.map(x => this.packSimple(x))); + } + + @bindThis + public async packDetailed( + src: MiEmojiRequest['id'] | MiEmojiRequest, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src }); + + return { + id: emoji.id, + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + url: emoji.publicUrl, + license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + fileId: emoji.fileId, + }; + } + + @bindThis + public packDetailedMany( + emojis: any[], + ) { + return Promise.all(emojis.map(x => this.packDetailed(x))); + } +} + diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index afa919dfa9..328ea0ba11 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -41,6 +41,7 @@ export const DI = { followRequestsRepository: Symbol('followRequestsRepository'), instancesRepository: Symbol('instancesRepository'), emojisRepository: Symbol('emojisRepository'), + emojiRequestsRepository: Symbol('emojiRequestsRepository'), driveFilesRepository: Symbol('driveFilesRepository'), driveFoldersRepository: Symbol('driveFoldersRepository'), metasRepository: Symbol('metasRepository'), diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 176978d35f..a0f9dd7879 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -33,7 +33,7 @@ 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 { packedEmojiDetailedSchema, packedEmojiRequestSimpleSchema, packedEmojiSimpleSchema, packedEmojiRequestDetailedSchema } 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'; @@ -73,7 +73,9 @@ export const refs = { FederationInstance: packedFederationInstanceSchema, GalleryPost: packedGalleryPostSchema, EmojiSimple: packedEmojiSimpleSchema, + EmojiRequestSimple: packedEmojiRequestSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, + EmojiRequestDetailed: packedEmojiRequestDetailedSchema, Flash: packedFlashSchema, Signin: packedSigninSchema, RoleLite: packedRoleLiteSchema, diff --git a/packages/backend/src/models/EmojiRequest.ts b/packages/backend/src/models/EmojiRequest.ts new file mode 100644 index 0000000000..53a5563f7e --- /dev/null +++ b/packages/backend/src/models/EmojiRequest.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('emoji_request') +@Index(['name'], { unique: true }) +export class MiEmojiRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true, + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 128, + }) + public name: string; + + @Column('varchar', { + length: 128, nullable: true, + }) + public category: string | null; + + @Column('varchar', { + length: 512, + }) + public originalUrl: string; + + @Column('varchar', { + length: 512, + default: '', + }) + public publicUrl: string; + + // publicUrlの方のtypeが入る + @Column('varchar', { + length: 64, nullable: true, + }) + public type: string | null; + + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public aliases: string[]; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public license: string | null; + + @Column('varchar', { + length: 1024, nullable: false, + }) + public fileId: string; + + @Column('boolean', { + default: false, + }) + public localOnly: boolean; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 1d3d0c873f..15fed93e70 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -25,7 +25,7 @@ import { MiDriveFile, MiDriveFolder, MiEmoji, - MiFlash, + MiEmojiRequest,MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, @@ -244,6 +244,12 @@ const $emojisRepository: Provider = { inject: [DI.db], }; +const $emojiRequestsRepository: Provider = { + provide: DI.emojiRequestsRepository, + useFactory: (db: DataSource) => db.getRepository(MiEmojiRequest), + inject: [DI.db], +}; + const $driveFilesRepository: Provider = { provide: DI.driveFilesRepository, useFactory: (db: DataSource) => db.getRepository(MiDriveFile), @@ -504,6 +510,7 @@ const $userMemosRepository: Provider = { $followRequestsRepository, $instancesRepository, $emojisRepository, + $emojiRequestsRepository, $driveFilesRepository, $driveFoldersRepository, $metasRepository, @@ -572,6 +579,7 @@ const $userMemosRepository: Provider = { $followRequestsRepository, $instancesRepository, $emojisRepository, + $emojiRequestsRepository, $driveFilesRepository, $driveFoldersRepository, $metasRepository, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 10f5982ce5..c3689d37b6 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -21,6 +21,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { MiDriveFolder } from '@/models/DriveFolder.js'; import { MiEmoji } from '@/models/Emoji.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; import { MiFollowing } from '@/models/Following.js'; import { MiFollowRequest } from '@/models/FollowRequest.js'; import { MiGalleryLike } from '@/models/GalleryLike.js'; @@ -90,6 +91,7 @@ export { MiDriveFile, MiDriveFolder, MiEmoji, + MiEmojiRequest, MiFollowing, MiFollowRequest, MiGalleryLike, @@ -158,6 +160,7 @@ export type ClipFavoritesRepository = Repository; export type DriveFilesRepository = Repository; export type DriveFoldersRepository = Repository; export type EmojisRepository = Repository; +export type EmojiRequestsRepository = Repository; export type FollowingsRepository = Repository; export type FollowRequestsRepository = Repository; export type GalleryLikesRepository = Repository; diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 90054cbc50..501eeb2ad4 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -46,6 +46,36 @@ export const packedEmojiSimpleSchema = { }, }, } as const; +export const packedEmojiRequestSimpleSchema = { + type: 'object', + properties: { + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + isSensitive: { + type: 'boolean', + optional: true, nullable: false, + }, + }, +} as const; export const packedEmojiDetailedSchema = { type: 'object', @@ -108,3 +138,51 @@ export const packedEmojiDetailedSchema = { }, }, } as const; + +export const packedEmojiRequestDetailedSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + license: { + type: 'string', + optional: false, nullable: true, + }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + localOnly: { + type: 'boolean', + optional: false, nullable: false, + }, + fileId: { + type: 'string', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 47af990231..379fd63381 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -29,6 +29,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { MiDriveFolder } from '@/models/DriveFolder.js'; import { MiEmoji } from '@/models/Emoji.js'; +import { MiEmojiRequest } from '@/models/EmojiRequest.js'; import { MiFollowing } from '@/models/Following.js'; import { MiFollowRequest } from '@/models/FollowRequest.js'; import { MiGalleryLike } from '@/models/GalleryLike.js'; @@ -164,6 +165,7 @@ export const entities = [ MiPoll, MiPollVote, MiEmoji, + MiEmojiRequest, MiHashtag, MiSwSubscription, MiAbuseUserReport, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index e8d90d65e0..76649ce0fb 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -34,18 +34,20 @@ import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-al import * as ep___admin_emoji_setlocalOnlyBulk from './endpoints/admin/emoji/set-localonly-bulk.js'; import * as ep___admin_emoji_setisSensitiveBulk from './endpoints/admin/emoji/set-issensitive-bulk.js'; import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; -import * as ep___admin_emoji_addDraft from './endpoints/admin/emoji/add-draft.js'; +import * as ep___admin_emoji_addRequest from './endpoints/admin/emoji/add-request.js'; import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_listRequest from './endpoints/admin/emoji/list-request.js'; import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; 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_updateRequest from './endpoints/admin/emoji/update-request.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'; @@ -255,6 +257,7 @@ import * as ep___invite_list from './endpoints/invite/list.js'; import * as ep___invite_limit from './endpoints/invite/limit.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emojiRequests from './endpoints/emoji-requests.js'; import * as ep___emoji from './endpoints/emoji.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; @@ -402,18 +405,20 @@ const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-ali const $admin_emoji_setlocalOnlyBulk: Provider = { provide: 'ep:admin/emoji/set-localonly-bulk', useClass: ep___admin_emoji_setlocalOnlyBulk.default }; const $admin_emoji_setisSensitiveBulk: Provider = { provide: 'ep:admin/emoji/set-issensitive-bulk', useClass: ep___admin_emoji_setisSensitiveBulk.default }; const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default }; -const $admin_emoji_addDraft: Provider = { provide: 'ep:admin/emoji/add-draft', useClass: ep___admin_emoji_addDraft.default }; +const $admin_emoji_addRequest: Provider = { provide: 'ep:admin/emoji/add-request', useClass: ep___admin_emoji_addRequest.default }; const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default }; const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default }; const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default }; const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default }; const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default }; const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default }; +const $admin_emoji_listRequest: Provider = { provide: 'ep:admin/emoji/list-request', useClass: ep___admin_emoji_listRequest.default }; const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default }; const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default }; 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_updateRequest: Provider = { provide: 'ep:admin/emoji/update-request', useClass: ep___admin_emoji_updateRequest.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 }; @@ -624,6 +629,7 @@ const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invit const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; +const $emoji_requests: Provider = { provide: 'ep:emoji-requests', useClass: ep___emojiRequests.default }; const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; @@ -774,18 +780,20 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_emoji_setlocalOnlyBulk, $admin_emoji_setisSensitiveBulk, $admin_emoji_add, - $admin_emoji_addDraft, + $admin_emoji_addRequest, $admin_emoji_copy, $admin_emoji_deleteBulk, $admin_emoji_delete, $admin_emoji_importZip, $admin_emoji_listRemote, $admin_emoji_list, + $admin_emoji_listRequest, $admin_emoji_removeAliasesBulk, $admin_emoji_setAliasesBulk, $admin_emoji_setCategoryBulk, $admin_emoji_setLicenseBulk, $admin_emoji_update, + $admin_emoji_updateRequest, $admin_federation_deleteAllFiles, $admin_federation_refreshRemoteInstanceMetadata, $admin_federation_removeAllFollowing, @@ -996,6 +1004,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $invite_limit, $meta, $emojis, + $emoji_requests, $emoji, $miauth_genToken, $mute_create, @@ -1138,13 +1147,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_drive_showFile, $admin_emoji_addAliasesBulk, $admin_emoji_add, - $admin_emoji_addDraft, + $admin_emoji_addRequest, $admin_emoji_copy, $admin_emoji_deleteBulk, $admin_emoji_delete, $admin_emoji_importZip, $admin_emoji_listRemote, $admin_emoji_list, + $admin_emoji_listRequest, $admin_emoji_removeAliasesBulk, $admin_emoji_setAliasesBulk, $admin_emoji_setCategoryBulk, @@ -1152,6 +1162,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_emoji_setlocalOnlyBulk, $admin_emoji_setisSensitiveBulk, $admin_emoji_update, + $admin_emoji_updateRequest, $admin_federation_deleteAllFiles, $i_userstats, $admin_federation_refreshRemoteInstanceMetadata, @@ -1362,6 +1373,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $invite_limit, $meta, $emojis, + $emoji_requests, $emoji, $miauth_genToken, $mute_create, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index ef52c41934..f38d1fcdbb 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -33,18 +33,20 @@ import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; -import * as ep___admin_emoji_addDraft from './endpoints/admin/emoji/add-draft.js'; +import * as ep___admin_emoji_addRequest from './endpoints/admin/emoji/add-request.js'; import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; +import * as ep___admin_emoji_listRequest from './endpoints/admin/emoji/list-request.js'; import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; 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_updateRequest from './endpoints/admin/emoji/update-request.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'; @@ -255,6 +257,7 @@ import * as ep___invite_list from './endpoints/invite/list.js'; import * as ep___invite_limit from './endpoints/invite/limit.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emojiRequests from './endpoints/emoji-requests.js'; import * as ep___emoji from './endpoints/emoji.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; @@ -397,13 +400,14 @@ const eps = [ ['admin/drive/show-file', ep___admin_drive_showFile], ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk], ['admin/emoji/add', ep___admin_emoji_add], - ['admin/emoji/add-draft', ep___admin_emoji_addDraft], + ['admin/emoji/add-request', ep___admin_emoji_addRequest], ['admin/emoji/copy', ep___admin_emoji_copy], ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk], ['admin/emoji/delete', ep___admin_emoji_delete], ['admin/emoji/import-zip', ep___admin_emoji_importZip], ['admin/emoji/list-remote', ep___admin_emoji_listRemote], ['admin/emoji/list', ep___admin_emoji_list], + ['admin/emoji/list-request', ep___admin_emoji_listRequest], ['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk], ['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk], ['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk], @@ -411,6 +415,7 @@ const eps = [ ['admin/emoji/set-issensitive-bulk', ep___admin_emoji_setisSensitiveBulk], ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk], ['admin/emoji/update', ep___admin_emoji_update], + ['admin/emoji/update-request', ep___admin_emoji_updateRequest], ['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], @@ -621,6 +626,7 @@ const eps = [ ['invite/limit', ep___invite_limit], ['meta', ep___meta], ['emojis', ep___emojis], + ['emoji-requests', ep___emojiRequests], ['emoji', ep___emoji], ['miauth/gen-token', ep___miauth_genToken], ['mute/create', ep___mute_create], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-request.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-request.ts new file mode 100644 index 0000000000..609297f667 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-request.ts @@ -0,0 +1,91 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canRequestCustomEmojis', + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', + }, + duplicateName: { + message: 'Duplicate name.', + code: 'DUPLICATE_NAME', + id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean', nullable: true }, + localOnly: { type: 'boolean', nullable: true }, + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['name', 'fileId'], +} as const; + +// TODO: ロジックをサービスに切り出す + +@Injectable() +// eslint-disable-next-line import/no-default-export +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private customEmojiService: CustomEmojiService, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + const isRequestDuplicate = await this.customEmojiService.checkRequestDuplicate(ps.name); + + if (isDuplicate || isRequestDuplicate) throw new ApiError(meta.errors.duplicateName); + const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + + const emoji = await this.customEmojiService.request({ + driveFile, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + }); + + await this.moderationLogService.log(me, 'addCustomEmoji', { + emojiId: emoji.id, + emoji: emoji, + }); + + return { + id: emoji.id, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index e15af7717b..1ad7e2f9e0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -38,7 +38,14 @@ export default class extends Endpoint { // eslint- private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.customEmojiService.delete(ps.id, me); + const emoji = await this.customEmojiService.getEmojiById(ps.id); + const RequestEmoji = await this.customEmojiService.getEmojiRequestById(ps.id); + if (emoji != null) { + await this.customEmojiService.delete(ps.id, me); + } + if (RequestEmoji != null) { + await this.customEmojiService.deleteRequest(ps.id); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-request.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-request.ts new file mode 100644 index 0000000000..f5faca972f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-request.ts @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { EmojiRequestsRepository } from '@/models/_.js'; +import type { MiEmojiRequest } from '@/models/EmojiRequest.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { EmojiRequestsEntityService } from '@/core/entities/EmojiRequestsEntityService.js'; +//import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + + 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, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + query: { type: 'string', nullable: true, default: null }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + + private emojiRequestsEntityService: EmojiRequestsEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const q = this.queryService.makePaginationQuery(this.emojiRequestsRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId); + + let emojis: MiEmojiRequest[]; + + 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) { + emojis = emojis.filter(emoji => + queryarry.includes(`:${emoji.name}:`), + ); + } else { + emojis = emojis.filter(emoji => + emoji.name.includes(ps.query!) || + emoji.aliases.some(a => a.includes(ps.query!)) || + emoji.category?.includes(ps.query!)); + } + emojis.splice(ps.limit + 1); + } else { + emojis = await q.limit(ps.limit).getMany(); + } + + return this.emojiRequestsEntityService.packDetailedMany(emojis); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update-request.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update-request.ts new file mode 100644 index 0000000000..4be16bb780 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update-request.ts @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + + errors: { + noSuchEmoji: { + message: 'No such emoji.', + code: 'NO_SUCH_EMOJI', + id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', + }, + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d', + }, + sameNameEmojiExists: { + message: 'Emoji that have same name already exists.', + code: 'SAME_NAME_EMOJI_EXISTS', + id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + fileId: { type: 'string', format: 'misskey:id' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, + Request: { type: 'boolean' }, + }, + required: ['id', 'name', 'aliases'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private customEmojiService: CustomEmojiService, + private driveFileEntityService: DriveFileEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + let driveFile; + const isRequest = !!ps.Request; + if (ps.fileId) { + driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + } + + const emoji = await this.customEmojiService.getEmojiRequestById(ps.id); + if (emoji != null) { + if (ps.name !== emoji.name) { + const isDuplicate = await this.customEmojiService.checkRequestDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); + } + } else { + throw new ApiError(meta.errors.noSuchEmoji); + } + if (!isRequest) { + const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl); + if (file === null) throw new ApiError(meta.errors.noSuchFile); + await this.customEmojiService.add({ + driveFile: file, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + host: null, + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + }, me); + await this.customEmojiService.deleteRequest(ps.id); + } else { + await this.customEmojiService.updateRequest(ps.id, { + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + }, me); + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 104b163224..d2d0bbc9b1 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import type { DriveFilesRepository , EmojisRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -62,7 +63,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, - draft: { type: 'boolean' }, + Request: { type: 'boolean' }, }, required: ['id', 'name', 'draft', 'aliases'], } as const; @@ -73,10 +74,11 @@ export default class extends Endpoint { // eslint- @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, private customEmojiService: CustomEmojiService, + private driveFileEntityService: DriveFileEntityService, ) { super(meta, paramDef, async (ps, me) => { let driveFile; - + const isRequest = !!ps.Request; if (ps.fileId) { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); @@ -91,7 +93,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchEmoji); } - await this.customEmojiService.update(ps.id, { + if (!isRequest) {await this.customEmojiService.update(ps.id, { driveFile, name: ps.name, category: ps.category ?? null, @@ -101,7 +103,20 @@ export default class extends Endpoint { // eslint- localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, draft: ps.draft, - }, me); + }, me);} else { + const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl); + if (file === null) throw new ApiError(meta.errors.noSuchFile); + await this.customEmojiService.request({ + driveFile: file, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + }, me); + await this.customEmojiService.delete(ps.id); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/emoji-requests.ts b/packages/backend/src/server/api/endpoints/emoji-requests.ts new file mode 100644 index 0000000000..63ca88e93e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/emoji-requests.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { EmojiRequestsRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojiRequestsEntityService } from '@/core/entities/EmojiRequestsEntityService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + emojis: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'EmojiRequestSimple', + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.emojiRequestsRepository) + private emojiRequestsRepository: EmojiRequestsRepository, + + private emojiRequestsEntityService: EmojiRequestsEntityService, + ) { + super(meta, paramDef, async () => { + const emojis = await this.emojiRequestsRepository.find({ + order: { + category: 'ASC', + name: 'ASC', + }, + }); + + return { + emojis: await this.emojiRequestsEntityService.packSimpleMany(emojis), + }; + }); + } +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 03e755bcf9..6b724530a2 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -57,6 +57,7 @@ export const moderationLogTypes = [ 'unsuspend', 'updateUserNote', 'addCustomEmoji', + 'requestCustomEmoji', 'updateCustomEmoji', 'deleteCustomEmoji', 'assignRole', @@ -117,6 +118,10 @@ export type ModerationLogPayloads = { emojiId: string; emoji: any; }; + requestCustomEmoji: { + emojiId: string; + emoji: any; + }; updateCustomEmoji: { emojiId: string; before: any; diff --git a/packages/frontend/src/components/MkCustomEmojiEditLocal.vue b/packages/frontend/src/components/MkCustomEmojiEditLocal.vue index a02702e6bc..8d9484b8e7 100644 --- a/packages/frontend/src/components/MkCustomEmojiEditLocal.vue +++ b/packages/frontend/src/components/MkCustomEmojiEditLocal.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 07347eda29..5cfcb3b93a 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -212,7 +212,6 @@ async function init(): Promise { const item = res[i]; if (i === 3) item._shouldInsertAd_ = true; } - if (res.length === 0 || props.pagination.noPaging) { concatItems(res); more.value = false; @@ -221,7 +220,6 @@ async function init(): Promise { concatItems(res); more.value = true; } - offset.value = res.length; error.value = false; fetching.value = false; diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index e22c4423cb..150aab340c 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -5,138 +5,112 @@ SPDX-License-Identifier: AGPL-3.0-only