summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzyoshoka <107108195+zyoshoka@users.noreply.github.com>2024-02-29 20:42:02 +0900
committerGitHub <noreply@github.com>2024-02-29 20:42:02 +0900
commit16f16e6b0879199a78f0f9ef2da7e1e44ee8d355 (patch)
tree87a4cc793674350420d41bf42bb8b9eca2960875
parentenhance: 通知の履歴をリセットできるように (#13335) (diff)
downloadsharkey-16f16e6b0879199a78f0f9ef2da7e1e44ee8d355.tar.gz
sharkey-16f16e6b0879199a78f0f9ef2da7e1e44ee8d355.tar.bz2
sharkey-16f16e6b0879199a78f0f9ef2da7e1e44ee8d355.zip
fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように (#13477)
* fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように * Update CHANGELOG.md * test(backend): `notes/create`とWebSocket関連のテストを追加
-rw-r--r--CHANGELOG.md1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts8
-rw-r--r--packages/backend/test/e2e/note.ts81
-rw-r--r--packages/backend/test/e2e/streaming.ts40
-rw-r--r--packages/frontend/src/components/MkPostForm.vue3
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue7
6 files changed, 136 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae611875dc..995b37f24a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
- Enhance: 通知の履歴をリセットできるように
+- Fix: ダイレクトなノートに対してはダイレクトでしか返信できないように
### Client
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 2fa0bd099f..27463577fe 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -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
diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts
index a5742d6e77..23de94889d 100644
--- a/packages/backend/test/e2e/note.ts
+++ b/packages/backend/test/e2e/note.ts
@@ -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文字
diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts
index 13d5a683ba..57ce73ba60 100644
--- a/packages/backend/test/e2e/streaming.ts
+++ b/packages/backend/test/e2e/streaming.ts
@@ -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
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 819f0f692c..e03faeaf55 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -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;
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 3439a751a0..5ecd41bfdf 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -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;
}>(), {
});