mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように (#13477)
* fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように * Update CHANGELOG.md * test(backend): `notes/create`とWebSocket関連のテストを追加
This commit is contained in:
		
							parent
							
								
									39d6af135f
								
							
						
					
					
						commit
						16f16e6b08
					
				
					 6 changed files with 136 additions and 4 deletions
				
			
		| 
						 | 
				
			
			@ -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…
	
	Add table
		
		Reference in a new issue