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: 通知の受信設定に「フォロー中またはフォロワー」を追加
|
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
|
||||||
- Enhance: 通知の履歴をリセットできるように
|
- Enhance: 通知の履歴をリセットできるように
|
||||||
|
- Fix: ダイレクトなノートに対してはダイレクトでしか返信できないように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
|
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
|
||||||
|
|
|
@ -85,6 +85,12 @@ export const meta = {
|
||||||
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
|
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: {
|
cannotCreateAlreadyExpiredPoll: {
|
||||||
message: 'Poll is already expired.',
|
message: 'Poll is already expired.',
|
||||||
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
|
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);
|
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||||
|
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||||
|
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
|
|
|
@ -176,6 +176,87 @@ describe('Note', () => {
|
||||||
assert.strictEqual(deleteRes.status, 204);
|
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 () => {
|
test('文字数ぎりぎりで怒られない', async () => {
|
||||||
const post = {
|
const post = {
|
||||||
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
|
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
|
||||||
|
|
|
@ -227,6 +227,46 @@ describe('Streaming', () => {
|
||||||
assert.strictEqual(fired, false);
|
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 () => {
|
test('withRenotes: false のときリノートが流れない', async () => {
|
||||||
const fired = await waitFire(
|
const fired = await waitFire(
|
||||||
ayano, 'homeTimeline', // ayano:home
|
ayano, 'homeTimeline', // ayano:home
|
||||||
|
|
|
@ -172,7 +172,7 @@ const emit = defineEmits<{
|
||||||
const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
|
const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
|
||||||
const cwInputEl = shallowRef<HTMLInputElement | null>(null);
|
const cwInputEl = shallowRef<HTMLInputElement | null>(null);
|
||||||
const hashtagsInputEl = 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 posting = ref(false);
|
||||||
const posted = ref(false);
|
const posted = ref(false);
|
||||||
|
@ -461,6 +461,7 @@ function setVisibility() {
|
||||||
isSilenced: $i.isSilenced,
|
isSilenced: $i.isSilenced,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
src: visibilityButton.value,
|
src: visibilityButton.value,
|
||||||
|
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
|
||||||
}, {
|
}, {
|
||||||
changeVisibility: v => {
|
changeVisibility: v => {
|
||||||
visibility.value = v;
|
visibility.value = v;
|
||||||
|
|
|
@ -9,21 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="[$style.label, $style.item]">
|
<div :class="[$style.label, $style.item]">
|
||||||
{{ i18n.ts.visibility }}
|
{{ i18n.ts.visibility }}
|
||||||
</div>
|
</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.icon"><i class="ti ti-world"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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.icon"><i class="ti ti-home"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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.icon"><i class="ti ti-lock"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
|
<span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
|
||||||
|
@ -54,6 +54,7 @@ const props = withDefaults(defineProps<{
|
||||||
isSilenced: boolean;
|
isSilenced: boolean;
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
|
isReplyVisibilitySpecified?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue