diff --git a/locales/en-US.yml b/locales/en-US.yml index 4ae409477..234ab73a6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1065,6 +1065,7 @@ update: "Update" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "If no roles are specified, anyone can use this emoji as reaction." rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "These roles must be public." +rolesThatCanNotBeUsedThisEmojiAsReaction: "Roles that can not use this emoji as reaction" cancelReactionConfirm: "Really delete your reaction?" changeReactionConfirm: "Really change your reaction?" later: "Later" diff --git a/locales/index.d.ts b/locales/index.d.ts index 7fc21a1cc..e97eec6c6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1068,6 +1068,7 @@ export interface Locale { "rolesThatCanBeUsedThisEmojiAsReaction": string; "rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription": string; "rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn": string; + "rolesThatCanNotBeUsedThisEmojiAsReaction": string; "cancelReactionConfirm": string; "changeReactionConfirm": string; "later": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d5c50b59d..a4d877954 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1065,6 +1065,7 @@ update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールの指定が一つもない場合、誰でもリアクションとして使えます。" rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールである必要があります。" +rolesThatCanNotBeUsedThisEmojiAsReaction: "リアクションとして使えないロール" cancelReactionConfirm: "リアクションを取り消しますか?" changeReactionConfirm: "リアクションを変更しますか?" later: "あとで" diff --git a/packages/backend/migration/1691317808362-customemoji-restricted-roles.js b/packages/backend/migration/1691317808362-customemoji-restricted-roles.js new file mode 100644 index 000000000..84e88229c --- /dev/null +++ b/packages/backend/migration/1691317808362-customemoji-restricted-roles.js @@ -0,0 +1,11 @@ +export class CustomemojiRestrictedRoles1691317808362 { + name = 'CustomemojiRestrictedRoles1691317808362' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "roleIdsThatCanNotBeUsedThisEmojiAsReaction" character varying(128) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "roleIdsThatCanNotBeUsedThisEmojiAsReaction"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 661d956bd..5a8721d66 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -68,6 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: boolean; localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][]; + roleIdsThatCanNotBeUsedThisEmojiAsReaction: Role['id'][]; }): Promise { const emoji = await this.emojisRepository.insert({ id: this.idService.genId(), @@ -83,6 +84,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: data.roleIdsThatCanNotBeUsedThisEmojiAsReaction, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { @@ -106,6 +108,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive?: boolean; localOnly?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][]; + roleIdsThatCanNotBeUsedThisEmojiAsReaction?: Role['id'][]; }): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -123,6 +126,7 @@ export class CustomEmojiService implements OnApplicationShutdown { publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: data.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? undefined, }); this.localEmojisCache.refresh(); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 4b01b6af7..be0581a6f 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -122,7 +122,10 @@ export class ReactionService { }); if (emoji) { - if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) { + if ( + (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) && + (emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.length === 0 || !(await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.includes(r.id))) + ) { reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; // センシティブ diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 4a18cd1b3..74740c70d 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -28,6 +28,7 @@ export class EmojiEntityService { url: emoji.publicUrl || emoji.originalUrl, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction : undefined, }; } @@ -56,6 +57,7 @@ export class EmojiEntityService { isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction, }; } diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts index 8fd3e65f5..98c1732fd 100644 --- a/packages/backend/src/models/entities/Emoji.ts +++ b/packages/backend/src/models/entities/Emoji.ts @@ -76,4 +76,9 @@ export class Emoji { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public roleIdsThatCanNotBeUsedThisEmojiAsReaction: string[]; } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 63f56e77c..85cd5900a 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -35,6 +35,15 @@ export const packedEmojiSimpleSchema = { format: 'id', }, }, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, }, } as const; @@ -86,7 +95,16 @@ export const packedEmojiDetailedSchema = { }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', - optional: false, nullable: false, + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: { + type: 'array', + optional: true, nullable: false, items: { type: 'string', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 37b929cb0..651858178 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -109,6 +109,7 @@ export class ImportCustomEmojisProcessorService { isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: [], + roleIdsThatCanNotBeUsedThisEmojiAsReaction: [], }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 200ede0b0..06d430911 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -40,6 +40,11 @@ export const paramDef = { localOnly: { type: 'boolean' }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', + format: 'misskey:id', + } }, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + format: 'misskey:id', } }, }, required: ['name', 'fileId'], @@ -73,6 +78,7 @@ export default class extends Endpoint { isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], + roleIdsThatCanNotBeUsedThisEmojiAsReaction: ps.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? [], }); this.moderationLogService.insertModerationLog(me, 'addEmoji', { 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 edc1af5a5..92ef171a6 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -49,6 +49,11 @@ export const paramDef = { localOnly: { type: 'boolean' }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', + format: 'misskey:id', + } }, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + format: 'misskey:id', } }, }, required: ['id', 'name', 'aliases'], @@ -80,6 +85,7 @@ export default class extends Endpoint { isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: ps.roleIdsThatCanNotBeUsedThisEmojiAsReaction, }); }); } diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index be7e38a34..75904c48d 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -284,7 +284,8 @@ watch(q, () => { }); function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean { - return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))); + return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)))) && + ((emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.length === 0) || ($i && !$i.roles.some(r => emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.includes(r.id)))); } function focus() { diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 58f865e03..dd8c87123 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -44,11 +44,28 @@
- {{ i18n.ts.add }} + {{ i18n.ts.add }}
- + + +
+ + {{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }} + {{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }} +
+ + + + + +
+ {{ i18n.ts.add }} + +
+ +
@@ -97,12 +114,18 @@ let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false); let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); +let roleIdsThatCanNotBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction : []); +let rolesThatCanNotBeUsedThisEmojiAsReaction = $ref([]); let file = $ref(); watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); }, { immediate: true }); +watch($$(roleIdsThatCanNotBeUsedThisEmojiAsReaction), async () => { + rolesThatCanNotBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanNotBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); +}, { immediate: true }); + const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); const emit = defineEmits<{ @@ -118,20 +141,22 @@ async function changeImage(ev) { } } -async function addRole() { +async function addRole(type: boolean) { const roles = await os.api('admin/roles/list'); - const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id); + const currentRoleIds = type ? rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id) : rolesThatCanNotBeUsedThisEmojiAsReaction.map(x => x.id); const { canceled, result: role } = await os.select({ items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })), }); if (canceled) return; - rolesThatCanBeUsedThisEmojiAsReaction.push(role); + if (type) rolesThatCanBeUsedThisEmojiAsReaction.push(role); + else rolesThatCanNotBeUsedThisEmojiAsReaction.push(role); } -async function removeRole(role, ev) { - rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id); +async function removeRole(type: boolean, role, ev) { + if (type) rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id); + else rolesThatCanNotBeUsedThisEmojiAsReaction = rolesThatCanNotBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id); } async function done() { @@ -143,6 +168,7 @@ async function done() { isSensitive, localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id), + roleIdsThatCanNotBeUsedThisEmojiAsReaction: rolesThatCanNotBeUsedThisEmojiAsReaction.map(x => x.id), }; if (file) { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 62152384f..e62cd7451 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -263,6 +263,9 @@ type CustomEmoji = { url: string; category: string; aliases: string[]; + isSensitive?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; }; // @public (undocumented) diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 9cf004528..a338fa9ad 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -281,6 +281,9 @@ export type CustomEmoji = { url: string; category: string; aliases: string[]; + isSensitive?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; }; export type LiteInstanceMetadata = {