fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように (#13477)
* fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように * Update CHANGELOG.md * test(backend): `notes/create`とWebSocket関連のテストを追加
This commit is contained in:
parent
39d6af135f
commit
16f16e6b08
|
@ -19,6 +19,7 @@
|
|||
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
|
||||
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
|
||||
- Enhance: 通知の履歴をリセットできるように
|
||||
- Fix: ダイレクトなノートに対してはダイレクトでしか返信できないように
|
||||
|
||||
### Client
|
||||
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
|
||||
|
|
|
@ -85,6 +85,12 @@ export const meta = {
|
|||
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
|
||||
},
|
||||
|
||||
cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
|
||||
message: 'You cannot reply to a specified visibility note with extended visibility.',
|
||||
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
|
||||
id: 'ed940410-535c-4d5e-bfa3-af798671e93c',
|
||||
},
|
||||
|
||||
cannotCreateAlreadyExpiredPoll: {
|
||||
message: 'Poll is already expired.',
|
||||
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
|
||||
|
@ -313,6 +319,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
|
|
|
@ -176,6 +176,87 @@ describe('Note', () => {
|
|||
assert.strictEqual(deleteRes.status, 204);
|
||||
});
|
||||
|
||||
test('visibility: followersなノートに対してフォロワーはリプライできる', async () => {
|
||||
await api('/following/create', {
|
||||
userId: alice.id,
|
||||
}, bob);
|
||||
|
||||
const aliceNote = await api('/notes/create', {
|
||||
text: 'direct note to bob',
|
||||
visibility: 'followers',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(aliceNote.status, 200);
|
||||
|
||||
const replyId = aliceNote.body.createdNote.id;
|
||||
const bobReply = await api('/notes/create', {
|
||||
text: 'reply to alice note',
|
||||
replyId,
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(bobReply.status, 200);
|
||||
assert.strictEqual(bobReply.body.createdNote.replyId, replyId);
|
||||
|
||||
await api('/following/delete', {
|
||||
userId: alice.id,
|
||||
}, bob);
|
||||
});
|
||||
|
||||
test('visibility: followersなノートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => {
|
||||
const aliceNote = await api('/notes/create', {
|
||||
text: 'direct note to bob',
|
||||
visibility: 'followers',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(aliceNote.status, 200);
|
||||
|
||||
const bobReply = await api('/notes/create', {
|
||||
text: 'reply to alice note',
|
||||
replyId: aliceNote.body.createdNote.id,
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(bobReply.status, 400);
|
||||
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
|
||||
});
|
||||
|
||||
test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => {
|
||||
const aliceNote = await api('/notes/create', {
|
||||
text: 'direct note to bob',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [bob.id],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(aliceNote.status, 200);
|
||||
|
||||
const bobReply = await api('/notes/create', {
|
||||
text: 'reply to alice note',
|
||||
replyId: aliceNote.body.createdNote.id,
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [alice.id],
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(bobReply.status, 200);
|
||||
});
|
||||
|
||||
test('visibility: specifiedなノートに対してvisibility: follwersで返信しようとすると怒られる', async () => {
|
||||
const aliceNote = await api('/notes/create', {
|
||||
text: 'direct note to bob',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [bob.id],
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(aliceNote.status, 200);
|
||||
|
||||
const bobReply = await api('/notes/create', {
|
||||
text: 'reply to alice note with visibility: followers',
|
||||
replyId: aliceNote.body.createdNote.id,
|
||||
visibility: 'followers',
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(bobReply.status, 400);
|
||||
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
|
||||
});
|
||||
|
||||
test('文字数ぎりぎりで怒られない', async () => {
|
||||
const post = {
|
||||
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
|
||||
|
|
|
@ -227,6 +227,46 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO: 落ちる
|
||||
* @see https://github.com/misskey-dev/misskey/issues/13474
|
||||
test('visibility: specified なノートで visibleUserIds に自分が含まれているときそのノートへのリプライが流れてくる', async () => {
|
||||
const chitoseToKyokoAndAyano = await post(chitose, { text: 'direct note from chitose to kyoko and ayano', visibility: 'specified', visibleUserIds: [kyoko.id, ayano.id] });
|
||||
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
() => api('notes/create', { text: 'direct reply from kyoko to chitose and ayano', replyId: chitoseToKyokoAndAyano.id, visibility: 'specified', visibleUserIds: [chitose.id, ayano.id] }, kyoko),
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
*/
|
||||
|
||||
test('visibility: specified な投稿に対するリプライで visibleUserIds が拡張されたとき、その拡張されたユーザーの HTL にはそのリプライが流れない', async () => {
|
||||
const chitoseToKyoko = await post(chitose, { text: 'direct note from chitose to kyoko', visibility: 'specified', visibleUserIds: [kyoko.id] });
|
||||
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
() => api('notes/create', { text: 'direct reply from kyoko to chitose and ayano', replyId: chitoseToKyoko.id, visibility: 'specified', visibleUserIds: [chitose.id, ayano.id] }, kyoko),
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
test('visibility: specified な投稿に対するリプライで visibleUserIds が収縮されたとき、その収縮されたユーザーの HTL にはそのリプライが流れない', async () => {
|
||||
const chitoseToKyokoAndAyano = await post(chitose, { text: 'direct note from chitose to kyoko and ayano', visibility: 'specified', visibleUserIds: [kyoko.id, ayano.id] });
|
||||
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
() => api('notes/create', { text: 'direct reply from kyoko to chitose', replyId: chitoseToKyokoAndAyano.id, visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko),
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
test('withRenotes: false のときリノートが流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
|
|
|
@ -172,7 +172,7 @@ const emit = defineEmits<{
|
|||
const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
|
||||
const cwInputEl = shallowRef<HTMLInputElement | null>(null);
|
||||
const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null);
|
||||
const visibilityButton = shallowRef<HTMLElement | null>(null);
|
||||
const visibilityButton = shallowRef<HTMLElement>();
|
||||
|
||||
const posting = ref(false);
|
||||
const posted = ref(false);
|
||||
|
@ -461,6 +461,7 @@ function setVisibility() {
|
|||
isSilenced: $i.isSilenced,
|
||||
localOnly: localOnly.value,
|
||||
src: visibilityButton.value,
|
||||
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
|
||||
}, {
|
||||
changeVisibility: v => {
|
||||
visibility.value = v;
|
||||
|
|
|
@ -9,21 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="[$style.label, $style.item]">
|
||||
{{ i18n.ts.visibility }}
|
||||
</div>
|
||||
<button key="public" :disabled="isSilenced" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
|
||||
<button key="public" :disabled="isSilenced || isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
|
||||
<div :class="$style.icon"><i class="ti ti-world"></i></div>
|
||||
<div :class="$style.body">
|
||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
|
||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button key="home" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
|
||||
<button key="home" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
|
||||
<div :class="$style.icon"><i class="ti ti-home"></i></div>
|
||||
<div :class="$style.body">
|
||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
|
||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button key="followers" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
|
||||
<button key="followers" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
|
||||
<div :class="$style.icon"><i class="ti ti-lock"></i></div>
|
||||
<div :class="$style.body">
|
||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
|
||||
|
@ -54,6 +54,7 @@ const props = withDefaults(defineProps<{
|
|||
isSilenced: boolean;
|
||||
localOnly: boolean;
|
||||
src?: HTMLElement;
|
||||
isReplyVisibilitySpecified?: boolean;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue