Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 #13835

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
28a0511
fix: reply to my follower notes are not shown on the home timeline
anatawa12 May 19, 2024
5b6076e
fix: reply to follower note by non-following is on social timeline
anatawa12 May 19, 2024
ef5472a
docs: changelog
anatawa12 May 19, 2024
d7f8405
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 20, 2024
7ea5a7c
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 20, 2024
355dd5e
test: add endpoint test for changes
anatawa12 May 20, 2024
781029d
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 20, 2024
a0c0815
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 21, 2024
d73444b
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 24, 2024
80078f8
test(e2e): 自分のfollowers投稿に対するリプライが流れる
anatawa12 May 24, 2024
2cbdda4
test(e2e/streaming): 自分のfollowers投稿に対するリプライが流れる
anatawa12 May 24, 2024
b619c3e
test(e2e/streaming): フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示さ…
anatawa12 May 24, 2024
998e2f3
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 28, 2024
23490da
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
anatawa12 May 31, 2024
3a3a571
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
Sayamame-beans Jul 17, 2024
560c984
test(e2e/timelines): try fixing typecheck error
Sayamame-beans Jul 17, 2024
2a4c0da
Merge branch 'develop' into follower-only-reply-to-my-note-invisible
Sayamame-beans Jul 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`に対応)
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
];
}

const [
followings,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
]);

const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
Expand All @@ -153,6 +159,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}

Expand Down
12 changes: 10 additions & 2 deletions packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
62 changes: 62 additions & 0 deletions packages/backend/test/e2e/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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' });
Expand All @@ -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);

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
75 changes: 75 additions & 0 deletions packages/backend/test/e2e/timelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()]);

Expand Down Expand Up @@ -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()]);

Expand Down
Loading