fix(backend/channel): チャンネルへのノートがリストのFTTに保存されない問題を修正、個人ハイライトに反映されるように (MisskeyIO#245)

This commit is contained in:
まっちゃとーにゅ 2023-11-22 01:10:48 +09:00 committed by GitHub
parent 3f5d0c60dd
commit 0c0c8f5144
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 50 deletions

View file

@ -743,6 +743,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (renote.channelId != null) { if (renote.channelId != null) {
if (renote.replyId == null) { if (renote.replyId == null) {
this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5); this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5);
this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5);
} }
} else { } else {
if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) { if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) {
@ -843,6 +844,48 @@ export class NoteCreateService implements OnApplicationShutdown {
const r = this.redisForTimelines.pipeline(); const r = this.redisForTimelines.pipeline();
// TODO: キャッシュ?
// eslint-disable-next-line prefer-const
let [followings, userListMemberships] = await Promise.all([
this.followingsRepository.find({
where: {
followeeId: user.id,
followerHost: IsNull(),
isFollowerHibernated: false,
},
select: ['followerId', 'withReplies'],
}),
this.userListMembershipsRepository.find({
where: {
userId: user.id,
},
select: ['userListId', 'userListUserId', 'withReplies'],
}),
]);
if (note.visibility === 'followers') {
// TODO: 重そうだから何とかしたい Set 使う?
userListMemberships = userListMemberships.filter(x => x.userListUserId === user.id || followings.some(f => f.followerId === x.userListUserId));
}
for (const userListMembership of userListMemberships) {
// ダイレクトのとき、そのリストが対象外のユーザーの場合
if (
note.visibility === 'specified' &&
!note.visibleUserIds.some(v => v === userListMembership.userListUserId)
) continue;
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) {
if (!userListMembership.withReplies) continue;
}
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
}
}
if (note.channelId) { if (note.channelId) {
this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
@ -862,30 +905,6 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
} else { } else {
// TODO: キャッシュ?
// eslint-disable-next-line prefer-const
let [followings, userListMemberships] = await Promise.all([
this.followingsRepository.find({
where: {
followeeId: user.id,
followerHost: IsNull(),
isFollowerHibernated: false,
},
select: ['followerId', 'withReplies'],
}),
this.userListMembershipsRepository.find({
where: {
userId: user.id,
},
select: ['userListId', 'userListUserId', 'withReplies'],
}),
]);
if (note.visibility === 'followers') {
// TODO: 重そうだから何とかしたい Set 使う?
userListMemberships = userListMemberships.filter(x => x.userListUserId === user.id || followings.some(f => f.followerId === x.userListUserId));
}
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする // TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
for (const following of followings) { for (const following of followings) {
// 基本的にvisibleUserIdsには自身のidが含まれている前提であること // 基本的にvisibleUserIdsには自身のidが含まれている前提であること
@ -902,24 +921,6 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
for (const userListMembership of userListMemberships) {
// ダイレクトのとき、そのリストが対象外のユーザーの場合
if (
note.visibility === 'specified' &&
!note.visibleUserIds.some(v => v === userListMembership.userListUserId)
) continue;
// 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合
if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) {
if (!userListMembership.withReplies) continue;
}
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
}
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
@ -947,12 +948,12 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
} }
}
if (Math.random() < 0.1) { if (Math.random() < 0.1) {
process.nextTick(() => { process.nextTick(() => {
this.checkHibernation(followings); this.checkHibernation(followings);
}); });
}
} }
r.exec(); r.exec();

View file

@ -208,6 +208,7 @@ export class ReactionService {
if (note.channelId != null) { if (note.channelId != null) {
if (note.replyId == null) { if (note.replyId == null) {
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1); this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
} }
} else { } else {
if (note.visibility === 'public' && note.userHost == null && note.replyId == null) { if (note.visibility === 'public' && note.userHost == null && note.replyId == null) {

View file

@ -200,7 +200,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id }) .andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
.andWhere('note.channelId IS NULL') // チャンネルノートではない
.andWhere(new Brackets(qb => { .andWhere(new Brackets(qb => {
qb qb
.where('note.replyId IS NULL') // 返信ではない .where('note.replyId IS NULL') // 返信ではない

View file

@ -978,7 +978,7 @@ describe('Timelines', () => {
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
}); });
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => { test.concurrent('リスインしているユーザーのチャンネルノートが含まれ', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
@ -991,7 +991,8 @@ describe('Timelines', () => {
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
}); });
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {