feat: 指定のロールはリアクションに使えないカスタム絵文字 (MisskeyIO#136)

This commit is contained in:
まっちゃとーにゅ 2023-08-07 23:16:33 +09:00 committed by GitHub
parent 42a90f56e1
commit 3b73874196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 102 additions and 10 deletions

View file

@ -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"

1
locales/index.d.ts vendored
View file

@ -1068,6 +1068,7 @@ export interface Locale {
"rolesThatCanBeUsedThisEmojiAsReaction": string;
"rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription": string;
"rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn": string;
"rolesThatCanNotBeUsedThisEmojiAsReaction": string;
"cancelReactionConfirm": string;
"changeReactionConfirm": string;
"later": string;

View file

@ -1065,6 +1065,7 @@ update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールの指定が一つもない場合、誰でもリアクションとして使えます。"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールである必要があります。"
rolesThatCanNotBeUsedThisEmojiAsReaction: "リアクションとして使えないロール"
cancelReactionConfirm: "リアクションを取り消しますか?"
changeReactionConfirm: "リアクションを変更しますか?"
later: "あとで"

View file

@ -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"`);
}
}

View file

@ -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();

View file

@ -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}:`;
// センシティブ

View file

@ -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,
};
}

View file

@ -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[];
}

View file

@ -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: 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,

View file

@ -109,6 +109,7 @@ export class ImportCustomEmojisProcessorService {
isSensitive: emojiInfo.isSensitive,
localOnly: emojiInfo.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
roleIdsThatCanNotBeUsedThisEmojiAsReaction: [],
});
}

View file

@ -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', {

View file

@ -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,
});
});
}

View file

@ -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() {

View file

@ -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) {

View file

@ -263,6 +263,9 @@ type CustomEmoji = {
url: string;
category: string;
aliases: string[];
isSensitive?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[];
};
// @public (undocumented)

View file

@ -281,6 +281,9 @@ export type CustomEmoji = {
url: string;
category: string;
aliases: string[];
isSensitive?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[];
};
export type LiteInstanceMetadata = {