Enhance(frontend): フロント側でもリアクション権限のチェックをするように (#13134)

* フロント側でもリアクション権限のチェックをするように

* update CHANGELOG.md

* lint fixes

* remove unrelated diffs

* deny -> reject
denyは「(信用しないことを理由に)拒否する」という意味らしい

* allow -> accept

* EmojiSimpleにlocalOnlyを含めるように

* リアクション権限のない絵文字は打てないように(ダイアログを出すのではなく)

* regenerate type definitions

* lint fix

* remove unused locales

* remove unnecessary async
This commit is contained in:
1Step621 2024-02-06 16:45:21 +09:00 committed by GitHub
parent edb39a089d
commit 74245df382
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 53 additions and 16 deletions

View file

@ -50,6 +50,10 @@
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように - Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように - Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
- Enhance: コードのシンタックスハイライトにテーマを適用できるように - Enhance: コードのシンタックスハイライトにテーマを適用できるように
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなく、権限がないことをダイアログで表示するように
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
- ロールが必要な絵文字をリアクションしようとした場合
- Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正

View file

@ -31,6 +31,7 @@ export class EmojiEntityService {
category: emoji.category, category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: emoji.publicUrl || emoji.originalUrl,
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
}; };

View file

@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
localOnly: {
type: 'boolean',
optional: true, nullable: false,
},
isSensitive: { isSensitive: {
type: 'boolean', type: 'boolean',
optional: true, nullable: false, optional: true, nullable: false,

View file

@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
showPinned?: boolean; showPinned?: boolean;
@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean; asDrawer?: boolean;
asWindow?: boolean; asWindow?: boolean;
asReactionPicker?: boolean; // 使使 asReactionPicker?: boolean; // 使使
targetNote?: Misskey.entities.Note;
}>(), { }>(), {
showPinned: true, showPinned: true,
}); });
@ -340,7 +342,7 @@ watch(q, () => {
}); });
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false; return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
} }
function focus() { function focus() {

View file

@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:showPinned="showPinned" :showPinned="showPinned"
:pinnedEmojis="pinnedEmojis" :pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker" :asReactionPicker="asReactionPicker"
:targetNote="targetNote"
:asDrawer="type === 'drawer'" :asDrawer="type === 'drawer'"
:max-height="maxHeight" :max-height="maxHeight"
@chosen="chosen" @chosen="chosen"
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { shallowRef } from 'vue'; import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
showPinned?: boolean; showPinned?: boolean;
pinnedEmojis?: string[], pinnedEmojis?: string[],
asReactionPicker?: boolean; asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note;
choseAndClose?: boolean; choseAndClose?: boolean;
}>(), { }>(), {
manualShowing: null, manualShowing: null,

View file

@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:front="true" :front="true"
@closed="emit('closed')" @closed="emit('closed')"
> >
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/> <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
</MkWindow> </MkWindow>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -26,6 +27,7 @@ withDefaults(defineProps<{
src?: HTMLElement; src?: HTMLElement;
showPinned?: boolean; showPinned?: boolean;
asReactionPicker?: boolean; asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note
}>(), { }>(), {
showPinned: true, showPinned: true,
}); });

View file

@ -385,7 +385,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value ?? null, reaction => { reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
if (props.mock) { if (props.mock) {

View file

@ -385,7 +385,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value ?? null, reaction => { reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', { misskeyApi('notes/reactions/create', {

View file

@ -32,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
import { customEmojis } from '@/custom-emojis.js';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
@ -48,13 +50,19 @@ const emit = defineEmits<{
const buttonEl = shallowRef<HTMLElement>(); const buttonEl = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); const isCustomEmoji = computed(() => props.reaction.includes(':'));
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
const canToggle = computed(() => {
return !props.reaction.match(/@\w/) && $i
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|| !isCustomEmoji.value;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
async function toggleReaction() { async function toggleReaction() {
if (!canToggle.value) return; if (!canToggle.value) return;
// TODO: 使
const oldReaction = props.note.myReaction; const oldReaction = props.note.myReaction;
if (oldReaction) { if (oldReaction) {
const confirm = await os.confirm({ const confirm = await os.confirm({
@ -101,8 +109,8 @@ async function toggleReaction() {
} }
async function menu(ev) { async function menu(ev) {
if (!canToggle.value) return; if (!canGetInfo.value) return;
if (!props.reaction.includes(':')) return;
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.info, text: i18n.ts.info,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',

View file

@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
const setDefaultEmoji = () => setDefault(pinnedEmojis); const setDefaultEmoji = () => setDefault(pinnedEmojis);
function previewReaction(ev: MouseEvent) { function previewReaction(ev: MouseEvent) {
reactionPicker.show(getHTMLElement(ev)); reactionPicker.show(getHTMLElement(ev), null);
} }
function previewEmoji(ev: MouseEvent) { function previewEmoji(ev: MouseEvent) {

View file

@ -0,0 +1,8 @@
import * as Misskey from 'misskey-js';
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
return !(emoji.localOnly && note.user.host !== me.host)
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
}

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import * as Misskey from 'misskey-js';
import { defineAsyncComponent, Ref, ref } from 'vue'; import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js';
class ReactionPicker { class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null); private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false); private manualShowing = ref(false);
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
private onChosen?: (reaction: string) => void; private onChosen?: (reaction: string) => void;
private onClosed?: () => void; private onClosed?: () => void;
@ -23,6 +25,7 @@ class ReactionPicker {
src: this.src, src: this.src,
pinnedEmojis: reactionsRef, pinnedEmojis: reactionsRef,
asReactionPicker: true, asReactionPicker: true,
targetNote: this.targetNote,
manualShowing: this.manualShowing, manualShowing: this.manualShowing,
}, { }, {
done: reaction => { done: reaction => {
@ -38,8 +41,9 @@ class ReactionPicker {
}); });
} }
public show(src: HTMLElement | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.src.value = src; this.src.value = src;
this.targetNote.value = targetNote;
this.manualShowing.value = true; this.manualShowing.value = true;
this.onChosen = onChosen; this.onChosen = onChosen;
this.onClosed = onClosed; this.onClosed = onClosed;

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.8 * version: 2024.2.0-beta.8
* generatedAt: 2024-02-04T11:51:13.598Z * generatedAt: 2024-02-04T16:51:09.469Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.8 * version: 2024.2.0-beta.8
* generatedAt: 2024-02-04T11:51:13.595Z * generatedAt: 2024-02-04T16:51:09.467Z
*/ */
import type { import type {

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.8 * version: 2024.2.0-beta.8
* generatedAt: 2024-02-04T11:51:13.593Z * generatedAt: 2024-02-04T16:51:09.466Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2024.2.0-beta.8 * version: 2024.2.0-beta.8
* generatedAt: 2024-02-04T11:51:13.592Z * generatedAt: 2024-02-04T16:51:09.465Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View file

@ -3,7 +3,7 @@
/* /*
* version: 2024.2.0-beta.8 * version: 2024.2.0-beta.8
* generatedAt: 2024-02-04T11:51:13.473Z * generatedAt: 2024-02-04T16:51:09.378Z
*/ */
/** /**
@ -4423,6 +4423,7 @@ export type components = {
name: string; name: string;
category: string | null; category: string | null;
url: string; url: string;
localOnly?: boolean;
isSensitive?: boolean; isSensitive?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
}; };