From 2b8056a8525e1b38536aa0406ba9446a88b365b9 Mon Sep 17 00:00:00 2001 From: Acid Chicken Date: Sat, 1 Jun 2024 11:16:44 +0900 Subject: fix(backend): use insertOne insteadof insert/findOneOrFail combination (#13908) * fix(backend): use insertOne insteadof insert/findOneOrFail combination * fix: typo * fix(backend): inherit mainAlias? * refactor(backend): use extend * fix(backend): invalid entityTarget * fix(backend): fake where * chore: debug * chore: debug * test: log * fix(backend): column names * fix(backend): remove dummy from * revert: log * fix(backend): position * fix(backend): automatic aliasing * chore(backend): alias * chore(backend): remove from * fix(backend): type * fix(backend): avoid pure name * test(backend): fix type * chore(backend): use cte * fix(backend): avoid useless alias * fix(backend): fix typo * fix(backend): __disambiguation__ * fix(backend): quote * chore(backend): t * chore(backend): accessible * chore(backend): concrete returning * fix(backend): quote * chore: log more * chore: log metadata * chore(backend): use raw * fix(backend): returning column name * fix(backend): transform * build(backend): wanna logging * build(backend): transform empty * build(backend): build alias * build(backend): restore name * chore: return entity * fix: test case * test(backend): 204 * chore(backend): log sql * chore(backend): assert user joined * fix(backend): typo * chore(backend): log long sql * chore(backend): log join * chore(backend): log join depth null * chore(backend): joinAttributes * chore(backend): override createJoinExpression * chore: join log * fix(backend): escape * test(backend): log log * chore(backend): join gonna success? * chore(backend): relations * chore(backend): undefined * chore(backend): target * chore(backend): remove log * chore(backend): log chart update * chore(backend): log columns * chore(backend): check hasMetadata * chore(backend): unshift id when not included * chore(backend): missing select * chore(backend): remove debug code --- packages/backend/src/server/api/endpoints/notes/polls/vote.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/notes') diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index a91c506afd..f33f49075b 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -144,12 +144,12 @@ export default class extends Endpoint { // eslint- } // Create vote - const vote = await this.pollVotesRepository.insert({ + const vote = await this.pollVotesRepository.insertOne({ id: this.idService.gen(createdAt.getTime()), noteId: note.id, userId: me.id, choice: ps.choice, - }).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0])); + }); // Increment votes count const index = ps.choice + 1; // In SQL, array index is 1 based -- cgit v1.2.3-freya From 961cb6c5eeb7745dc156327d2041241b70098b70 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sat, 22 Jun 2024 19:49:38 +0900 Subject: fix(backend): fix creating reactions bugs (#13901) * fix(backend): add fallback for empty string when creating reaction * fix(backend): prohibit reactions to Renote * test(backend): add some tests for `notes/reactions/create` endpoint * Update CHANGELOG.md * lint * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 + packages/backend/src/core/ReactionService.ts | 8 ++- .../server/api/endpoints/notes/reactions/create.ts | 7 +++ packages/backend/test/e2e/endpoints.ts | 61 ++++++++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api/endpoints/notes') diff --git a/CHANGELOG.md b/CHANGELOG.md index ca74d71719..354bbd20fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 +- Fix: 空文字列のリアクションはフォールバックされるように +- Fix: リノートにリアクションできないように ## 2024.5.0 diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index cb0b079df0..64c7b2ed03 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { trackPromise } from '@/misc/promise-tracker.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; const FALLBACK = '\u2764'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; @@ -117,11 +118,16 @@ export class ReactionService { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } + // Check if note is Renote + if (isRenote(note) && !isQuote(note)) { + throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.'); + } + let reaction = _reaction ?? FALLBACK; if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { reaction = '\u2764'; - } else if (_reaction) { + } else if (_reaction != null) { const custom = reaction.match(isCustomEmojiRegexp); if (custom) { const reacterHost = this.utilityService.toPunyNullable(user.host); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index b9899608bf..0f0dcca605 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -36,6 +36,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', }, + + cannotReactToRenote: { + message: 'You cannot react to Renote.', + code: 'CANNOT_REACT_TO_RENOTE', + id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', + }, }, } as const; @@ -62,6 +68,7 @@ export default class extends Endpoint { // eslint- await this.reactionService.create(me, note, ps.reaction).catch(err => { if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); + if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote); throw err; }); return; diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index bc89dc37f4..de5e8ba95e 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -266,6 +266,67 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); + test('リノートにリアクションできない', async () => { + const bobNote = await post(bob, { text: 'hi' }); + const bobRenote = await post(bob, { renoteId: bobNote.id }); + + const res = await api('notes/reactions/create', { + noteId: bobRenote.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE'); + }); + + test('引用にリアクションできる', async () => { + const bobNote = await post(bob, { text: 'hi' }); + const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id }); + + const res = await api('notes/reactions/create', { + noteId: bobRenote.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('空文字列のリアクションは\u2764にフォールバックされる', async () => { + const bobNote = await post(bob, { text: 'hi' }); + + const res = await api('notes/reactions/create', { + noteId: bobNote.id, + reaction: '', + }, alice); + + assert.strictEqual(res.status, 204); + + const reaction = await api('notes/reactions', { + noteId: bobNote.id, + }); + + assert.strictEqual(reaction.body.length, 1); + assert.strictEqual(reaction.body[0].type, '\u2764'); + }); + + test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => { + const bobNote = await post(bob, { text: 'hi' }); + + const res = await api('notes/reactions/create', { + noteId: bobNote.id, + reaction: 'Hello!', + }, alice); + + assert.strictEqual(res.status, 204); + + const reaction = await api('notes/reactions', { + noteId: bobNote.id, + }); + + assert.strictEqual(reaction.body.length, 1); + assert.strictEqual(reaction.body[0].type, '\u2764'); + }); + test('空のパラメータで怒られる', async () => { // @ts-expect-error param must not be empty const res = await api('notes/reactions/create', {}, alice); -- cgit v1.2.3-freya From cfdad45092ac2497a66b6b25f90bef7c466292a8 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 18 Jul 2024 02:06:31 +0900 Subject: fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 (#13978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts | 1 + 2 files changed, 2 insertions(+) (limited to 'packages/backend/src/server/api/endpoints/notes') diff --git a/CHANGELOG.md b/CHANGELOG.md index 395716bbe2..10c44c4f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) - Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正 - Fix: エラーメッセージの誤字を修正 (#14213) +- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 ### Misskey.js - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 5acc9706d3..f6084b5763 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -139,6 +139,7 @@ export default class extends Endpoint { // eslint- timelineConfig = [ `homeTimeline:${me.id}`, 'localTimeline', + `localTimelineWithReplyTo:${me.id}`, ]; } -- cgit v1.2.3-freya From 8f40f932e4bac6b4a90a464041a00df315a3c693 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Tue, 30 Jul 2024 19:44:08 +0900 Subject: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 (#13835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: reply to my follower notes are not shown on the home timeline * fix: reply to follower note by non-following is on social timeline * docs: changelog * test: add endpoint test for changes * test(e2e): 自分のfollowers投稿に対するリプライが流れる * test(e2e/streaming): 自分のfollowers投稿に対するリプライが流れる * test(e2e/streaming): フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題 * test(e2e/timelines): try fixing typecheck error --------- Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- CHANGELOG.md | 2 + .../server/api/endpoints/notes/hybrid-timeline.ts | 13 ++++ .../src/server/api/endpoints/notes/timeline.ts | 2 +- .../server/api/stream/channels/home-timeline.ts | 4 +- .../server/api/stream/channels/hybrid-timeline.ts | 12 +++- packages/backend/test/e2e/streaming.ts | 62 ++++++++++++++++++ packages/backend/test/e2e/timelines.ts | 75 ++++++++++++++++++++++ 7 files changed, 165 insertions(+), 5 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/notes') diff --git a/CHANGELOG.md b/CHANGELOG.md index 4051566651..28e4b236eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,8 @@ - NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます Migrationではカラム定義の変更のみが行われます。 サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です +- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 +- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正 ### Misskey.js - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index f6084b5763..2a2c659942 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -143,6 +143,12 @@ export default class extends Endpoint { // eslint- ]; } + const [ + followings, + ] = await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + ]); + const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -153,6 +159,13 @@ export default class extends Endpoint { // eslint- useDbFallback: serverSettings.enableFanoutTimelineDbFallback, alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, + noteFilter: note => { + if (note.reply && note.reply.visibility === 'followers') { + if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; + } + + return true; + }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 8b87908bd3..c9b43b5359 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -114,7 +114,7 @@ export default class extends Endpoint { // eslint- excludePureRenotes: !ps.withRenotes, noteFilter: note => { if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId)) return false; + if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; } return true; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 1f440732a6..66644ed58c 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -60,7 +60,7 @@ class HomeTimelineChannel extends Channel { const reply = note.reply; if (this.following[note.userId]?.withReplies) { // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; } else { // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; @@ -73,7 +73,7 @@ class HomeTimelineChannel extends Channel { if (note.renote.reply) { const reply = note.renote.reply; // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; } } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 6938b6e3ea..75bd13221f 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -76,14 +76,22 @@ class HybridTimelineChannel extends Channel { const reply = note.reply; if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) { // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; } else { // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; } } - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + // 純粋なリノート(引用リノートでないリノート)の場合 + if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { + if (!this.withRenotes) return; + if (note.renote.reply) { + const reply = note.renote.reply; + // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; + } + } if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index b0a70074c6..72f26a38e0 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -34,6 +34,7 @@ describe('Streaming', () => { let kyoko: misskey.entities.SignupResponse; let chitose: misskey.entities.SignupResponse; let kanako: misskey.entities.SignupResponse; + let erin: misskey.entities.SignupResponse; // Remote users let akari: misskey.entities.SignupResponse; @@ -53,6 +54,7 @@ describe('Streaming', () => { kyoko = await signup({ username: 'kyoko' }); chitose = await signup({ username: 'chitose' }); kanako = await signup({ username: 'kanako' }); + erin = await signup({ username: 'erin' }); // erin: A generic fifth participant akari = await signup({ username: 'akari', host: 'example.com' }); chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); @@ -71,6 +73,12 @@ describe('Streaming', () => { // Follow: kyoko => chitose await api('following/create', { userId: chitose.id }, kyoko); + // Follow: erin <=> ayano each other. + // erin => ayano: withReplies: true + await api('following/create', { userId: ayano.id, withReplies: true }, erin); + // ayano => erin: withReplies: false + await api('following/create', { userId: erin.id, withReplies: false }, ayano); + // Mute: chitose => kanako await api('mute/create', { userId: kanako.id }, chitose); @@ -297,6 +305,28 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); + + test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { + const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + erin, 'homeTimeline', // erin:home + () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, true); + }); + + test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { + const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post + msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin + ); + + assert.strictEqual(fired, true); + }); }); // Home describe('Local Timeline', () => { @@ -475,6 +505,38 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); + + test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { + const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + erin, 'homeTimeline', // erin:home + () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, true); + }); + + test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { + const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post + msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin + ); + + assert.strictEqual(fired, true); + }); + + test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => { + const fired = await waitFire( + erin, 'homeTimeline', // erin:home + () => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, false); + }); }); describe('Global Timeline', () => { diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index ab65781f70..b7069dd82c 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -127,6 +127,7 @@ describe('Timelines', () => { test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await api('following/create', { userId: carol.id }, bob); await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); await setTimeout(1000); @@ -161,6 +162,24 @@ describe('Timelines', () => { assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); }); + test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: alice.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); + test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); @@ -768,6 +787,62 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); + test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('following/create', { userId: carol.id }, bob); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + await api('following/create', { userId: carol.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: alice.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); + test.concurrent('他人の他人への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); -- cgit v1.2.3-freya