feat: 指定のロールはリアクションに使えないカスタム絵文字 (MisskeyIO#136)
This commit is contained in:
		
							parent
							
								
									42a90f56e1
								
							
						
					
					
						commit
						3b73874196
					
				|  | @ -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" | ||||
|  |  | |||
|  | @ -1068,6 +1068,7 @@ export interface Locale { | |||
|     "rolesThatCanBeUsedThisEmojiAsReaction": string; | ||||
|     "rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription": string; | ||||
|     "rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn": string; | ||||
|     "rolesThatCanNotBeUsedThisEmojiAsReaction": string; | ||||
|     "cancelReactionConfirm": string; | ||||
|     "changeReactionConfirm": string; | ||||
|     "later": string; | ||||
|  |  | |||
|  | @ -1065,6 +1065,7 @@ update: "更新" | |||
| rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" | ||||
| rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールの指定が一つもない場合、誰でもリアクションとして使えます。" | ||||
| rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールである必要があります。" | ||||
| rolesThatCanNotBeUsedThisEmojiAsReaction: "リアクションとして使えないロール" | ||||
| cancelReactionConfirm: "リアクションを取り消しますか?" | ||||
| changeReactionConfirm: "リアクションを変更しますか?" | ||||
| later: "あとで" | ||||
|  |  | |||
|  | @ -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"`); | ||||
|     } | ||||
| } | ||||
|  | @ -68,6 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 		isSensitive: boolean; | ||||
| 		localOnly: boolean; | ||||
| 		roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][]; | ||||
| 		roleIdsThatCanNotBeUsedThisEmojiAsReaction: Role['id'][]; | ||||
| 	}): Promise<Emoji> { | ||||
| 		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<void> { | ||||
| 		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(); | ||||
|  |  | |||
|  | @ -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}:`; | ||||
| 
 | ||||
| 						// センシティブ
 | ||||
|  |  | |||
|  | @ -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, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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[]; | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -109,6 +109,7 @@ export class ImportCustomEmojisProcessorService { | |||
| 					isSensitive: emojiInfo.isSensitive, | ||||
| 					localOnly: emojiInfo.localOnly, | ||||
| 					roleIdsThatCanBeUsedThisEmojiAsReaction: [], | ||||
| 					roleIdsThatCanNotBeUsedThisEmojiAsReaction: [], | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<typeof meta, typeof paramDef> { | |||
| 				isSensitive: ps.isSensitive ?? false, | ||||
| 				localOnly: ps.localOnly ?? false, | ||||
| 				roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], | ||||
| 				roleIdsThatCanNotBeUsedThisEmojiAsReaction: ps.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? [], | ||||
| 			}); | ||||
| 
 | ||||
| 			this.moderationLogService.insertModerationLog(me, 'addEmoji', { | ||||
|  |  | |||
|  | @ -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<typeof meta, typeof paramDef> { | |||
| 				isSensitive: ps.isSensitive, | ||||
| 				localOnly: ps.localOnly, | ||||
| 				roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, | ||||
| 				roleIdsThatCanNotBeUsedThisEmojiAsReaction: ps.roleIdsThatCanNotBeUsedThisEmojiAsReaction, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -44,11 +44,28 @@ | |||
| 					<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template> | ||||
| 
 | ||||
| 					<div class="_gaps"> | ||||
| 						<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 						<MkButton rounded @click="addRole(true)"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 
 | ||||
| 						<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem"> | ||||
| 							<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> | ||||
| 							<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button> | ||||
| 							<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(true, role, $event)"><i class="ti ti-x"></i></button> | ||||
| 							<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> | ||||
| 						</div> | ||||
| 
 | ||||
| 						<MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo> | ||||
| 						<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo> | ||||
| 					</div> | ||||
| 				</MkFolder> | ||||
| 				<MkFolder> | ||||
| 					<template #label>{{ i18n.ts.rolesThatCanNotBeUsedThisEmojiAsReaction }}</template> | ||||
| 					<template #suffix>{{ rolesThatCanNotBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.none : rolesThatCanNotBeUsedThisEmojiAsReaction.length }}</template> | ||||
| 
 | ||||
| 					<div class="_gaps"> | ||||
| 						<MkButton rounded @click="addRole(false)"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 
 | ||||
| 						<div v-for="role in rolesThatCanNotBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem"> | ||||
| 							<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> | ||||
| 							<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(false, role, $event)"><i class="ti ti-x"></i></button> | ||||
| 							<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> | ||||
| 						</div> | ||||
| 
 | ||||
|  | @ -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<misskey.entities.DriveFile>(); | ||||
| 
 | ||||
| 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) { | ||||
|  |  | |||
|  | @ -263,6 +263,9 @@ type CustomEmoji = { | |||
|     url: string; | ||||
|     category: string; | ||||
|     aliases: string[]; | ||||
|     isSensitive?: boolean; | ||||
|     roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; | ||||
|     roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; | ||||
| }; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
|  |  | |||
|  | @ -281,6 +281,9 @@ export type CustomEmoji = { | |||
| 	url: string; | ||||
| 	category: string; | ||||
| 	aliases: string[]; | ||||
| 	isSensitive?: boolean; | ||||
| 	roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; | ||||
| 	roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; | ||||
| }; | ||||
| 
 | ||||
| export type LiteInstanceMetadata = { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue