自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 (#13835)
* 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>
This commit is contained in:
parent
86b4f49880
commit
8f40f932e4
7 changed files with 165 additions and 5 deletions
|
@ -109,6 +109,8 @@
|
||||||
- NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます
|
- NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます
|
||||||
Migrationではカラム定義の変更のみが行われます。
|
Migrationではカラム定義の変更のみが行われます。
|
||||||
サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です
|
サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です
|
||||||
|
- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正
|
||||||
|
- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
|
||||||
|
|
|
@ -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({
|
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
@ -153,6 +159,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
|
||||||
alwaysIncludeMyNotes: true,
|
alwaysIncludeMyNotes: true,
|
||||||
excludePureRenotes: !ps.withRenotes,
|
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({
|
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
||||||
untilId,
|
untilId,
|
||||||
sinceId,
|
sinceId,
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
excludePureRenotes: !ps.withRenotes,
|
excludePureRenotes: !ps.withRenotes,
|
||||||
noteFilter: note => {
|
noteFilter: note => {
|
||||||
if (note.reply && note.reply.visibility === 'followers') {
|
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;
|
return true;
|
||||||
|
|
|
@ -60,7 +60,7 @@ class HomeTimelineChannel extends Channel {
|
||||||
const reply = note.reply;
|
const reply = note.reply;
|
||||||
if (this.following[note.userId]?.withReplies) {
|
if (this.following[note.userId]?.withReplies) {
|
||||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
// 自分のフォローしていないユーザーの 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 {
|
} else {
|
||||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||||
|
@ -73,7 +73,7 @@ class HomeTimelineChannel extends Channel {
|
||||||
if (note.renote.reply) {
|
if (note.renote.reply) {
|
||||||
const reply = note.renote.reply;
|
const reply = note.renote.reply;
|
||||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
// 自分のフォローしていないユーザーの 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,14 +76,22 @@ class HybridTimelineChannel extends Channel {
|
||||||
const reply = note.reply;
|
const reply = note.reply;
|
||||||
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
|
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
|
||||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
// 自分のフォローしていないユーザーの 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 {
|
} else {
|
||||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
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 (this.user && note.renoteId && !note.text) {
|
||||||
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ describe('Streaming', () => {
|
||||||
let kyoko: misskey.entities.SignupResponse;
|
let kyoko: misskey.entities.SignupResponse;
|
||||||
let chitose: misskey.entities.SignupResponse;
|
let chitose: misskey.entities.SignupResponse;
|
||||||
let kanako: misskey.entities.SignupResponse;
|
let kanako: misskey.entities.SignupResponse;
|
||||||
|
let erin: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
// Remote users
|
// Remote users
|
||||||
let akari: misskey.entities.SignupResponse;
|
let akari: misskey.entities.SignupResponse;
|
||||||
|
@ -53,6 +54,7 @@ describe('Streaming', () => {
|
||||||
kyoko = await signup({ username: 'kyoko' });
|
kyoko = await signup({ username: 'kyoko' });
|
||||||
chitose = await signup({ username: 'chitose' });
|
chitose = await signup({ username: 'chitose' });
|
||||||
kanako = await signup({ username: 'kanako' });
|
kanako = await signup({ username: 'kanako' });
|
||||||
|
erin = await signup({ username: 'erin' }); // erin: A generic fifth participant
|
||||||
|
|
||||||
akari = await signup({ username: 'akari', host: 'example.com' });
|
akari = await signup({ username: 'akari', host: 'example.com' });
|
||||||
chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
|
chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
|
||||||
|
@ -71,6 +73,12 @@ describe('Streaming', () => {
|
||||||
// Follow: kyoko => chitose
|
// Follow: kyoko => chitose
|
||||||
await api('following/create', { userId: chitose.id }, kyoko);
|
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
|
// Mute: chitose => kanako
|
||||||
await api('mute/create', { userId: kanako.id }, chitose);
|
await api('mute/create', { userId: kanako.id }, chitose);
|
||||||
|
|
||||||
|
@ -297,6 +305,28 @@ describe('Streaming', () => {
|
||||||
|
|
||||||
assert.strictEqual(fired, true);
|
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
|
}); // Home
|
||||||
|
|
||||||
describe('Local Timeline', () => {
|
describe('Local Timeline', () => {
|
||||||
|
@ -475,6 +505,38 @@ describe('Streaming', () => {
|
||||||
|
|
||||||
assert.strictEqual(fired, false);
|
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', () => {
|
describe('Global Timeline', () => {
|
||||||
|
|
|
@ -127,6 +127,7 @@ describe('Timelines', () => {
|
||||||
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
|
||||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
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/create', { userId: bob.id }, alice);
|
||||||
await api('following/update', { userId: bob.id, withReplies: true }, alice);
|
await api('following/update', { userId: bob.id, withReplies: true }, alice);
|
||||||
await setTimeout(1000);
|
await setTimeout(1000);
|
||||||
|
@ -161,6 +162,24 @@ describe('Timelines', () => {
|
||||||
assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
|
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 () => {
|
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
|
||||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
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);
|
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 () => {
|
test.concurrent('他人の他人への返信が含まれない', async () => {
|
||||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue