re-type userFollowingsCache to match the others

This commit is contained in:
Hazelnoot 2025-06-07 21:27:25 -04:00
parent 0c84d73294
commit 853b548a43
17 changed files with 47 additions and 56 deletions

View file

@ -130,7 +130,8 @@ export class AntennaService implements OnApplicationShutdown {
} }
if (note.visibility === 'followers') { if (note.visibility === 'followers') {
const isFollowing = Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(antenna.userId), note.userId); const followings = await this.cacheService.userFollowingsCache.fetch(antenna.userId);
const isFollowing = followings.has(note.userId);
if (!isFollowing && antenna.userId !== note.userId) return false; if (!isFollowing && antenna.userId !== note.userId) return false;
} }

View file

@ -6,14 +6,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { In, IsNull } from 'typeorm'; import { In, IsNull } from 'typeorm';
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing, MiNote } from '@/models/_.js'; import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiNote } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { QuantumKVCache } from '@/misc/QuantumKVCache.js'; import { QuantumKVCache } from '@/misc/QuantumKVCache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents, InternalEventTypes } from '@/core/GlobalEventService.js'; import type { InternalEventTypes } from '@/core/GlobalEventService.js';
import { InternalEventService } from '@/core/InternalEventService.js'; import { InternalEventService } from '@/core/InternalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@ -46,7 +46,7 @@ export class CacheService implements OnApplicationShutdown {
public userBlockingCache: QuantumKVCache<Set<string>>; public userBlockingCache: QuantumKVCache<Set<string>>;
public userBlockedCache: QuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ public userBlockedCache: QuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: QuantumKVCache<Set<string>>; public renoteMutingsCache: QuantumKVCache<Set<string>>;
public userFollowingsCache: QuantumKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>; public userFollowingsCache: QuantumKVCache<Map<string, { withReplies: boolean }>>;
protected userFollowStatsCache = new MemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes protected userFollowStatsCache = new MemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes
protected translationsCache: RedisKVCache<CachedTranslationEntity>; protected translationsCache: RedisKVCache<CachedTranslationEntity>;
@ -110,15 +110,9 @@ export class CacheService implements OnApplicationShutdown {
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))), fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
}); });
this.userFollowingsCache = new QuantumKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.internalEventService, 'userFollowings', { this.userFollowingsCache = new QuantumKVCache<Map<string, { withReplies: boolean }>>(this.internalEventService, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m lifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => { fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => new Map(xs.map(f => [f.followeeId, { withReplies: f.withReplies }]))),
const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
for (const x of xs) {
obj[x.followeeId] = { withReplies: x.withReplies };
}
return obj;
}),
}); });
this.translationsCache = new RedisKVCache<CachedTranslationEntity>(this.redisClient, 'translations', { this.translationsCache = new RedisKVCache<CachedTranslationEntity>(this.redisClient, 'translations', {
@ -305,14 +299,14 @@ export class CacheService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async getUserFollowings(userIds: Iterable<string>): Promise<Map<string, Set<string>>> { public async getUserFollowings(userIds: Iterable<string>): Promise<Map<string, Map<string, { withReplies: boolean }>>> {
const followings = new Map<string, Set<string>>(); const followings = new Map<string, Map<string, { withReplies: boolean }>>();
const toFetch: string[] = []; const toFetch: string[] = [];
for (const userId of userIds) { for (const userId of userIds) {
const fromCache = this.userFollowingsCache.get(userId); const fromCache = this.userFollowingsCache.get(userId);
if (fromCache) { if (fromCache) {
followings.set(userId, new Set(Object.keys(fromCache))); followings.set(userId, fromCache);
} else { } else {
toFetch.push(userId); toFetch.push(userId);
} }
@ -331,25 +325,25 @@ export class CacheService implements OnApplicationShutdown {
}) })
.getMany(); .getMany();
const toCache = new Map<string, Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(); const toCache = new Map<string, Map<string, { withReplies: boolean }>>();
// Pivot to a map // Pivot to a map
for (const { followerId, followeeId, withReplies } of fetchedFollowings) { for (const { followerId, followeeId, withReplies } of fetchedFollowings) {
// Queue for cache // Queue for cache
let cacheSet = toCache.get(followerId); let cacheMap = toCache.get(followerId);
if (!cacheSet) { if (!cacheMap) {
cacheSet = {}; cacheMap = new Map();
toCache.set(followerId, cacheSet); toCache.set(followerId, cacheMap);
} }
cacheSet[followeeId] = { withReplies }; cacheMap.set(followeeId, { withReplies });
// Queue for return // Queue for return
let returnSet = followings.get(followerId); let returnSet = followings.get(followerId);
if (!returnSet) { if (!returnSet) {
returnSet = new Set(); returnSet = new Map();
followings.set(followerId, returnSet); followings.set(followerId, returnSet);
} }
returnSet.add(followeeId); returnSet.set(followeeId, { withReplies });
} }
// Update cache to speed up future calls // Update cache to speed up future calls

View file

@ -113,27 +113,27 @@ export class NotificationService implements OnApplicationShutdown {
} }
if (recieveConfig?.type === 'following') { if (recieveConfig?.type === 'following') {
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)); const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId));
if (!isFollowing) { if (!isFollowing) {
return null; return null;
} }
} else if (recieveConfig?.type === 'follower') { } else if (recieveConfig?.type === 'follower') {
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)); const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId));
if (!isFollower) { if (!isFollower) {
return null; return null;
} }
} else if (recieveConfig?.type === 'mutualFollow') { } else if (recieveConfig?.type === 'mutualFollow') {
const [isFollowing, isFollower] = await Promise.all([ const [isFollowing, isFollower] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
]); ]);
if (!(isFollowing && isFollower)) { if (!(isFollowing && isFollower)) {
return null; return null;
} }
} else if (recieveConfig?.type === 'followingOrFollower') { } else if (recieveConfig?.type === 'followingOrFollower') {
const [isFollowing, isFollower] = await Promise.all([ const [isFollowing, isFollower] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
]); ]);
if (!isFollowing && !isFollower) { if (!isFollowing && !isFollower) {
return null; return null;

View file

@ -133,7 +133,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis @bindThis
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: { public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: {
myFollowing?: ReadonlySet<string>, myFollowing?: ReadonlyMap<string, { withReplies: boolean }>,
myBlockers?: ReadonlySet<string>, myBlockers?: ReadonlySet<string>,
}): Promise<void> { }): Promise<void> {
if (meId === packedNote.userId) return; if (meId === packedNote.userId) return;
@ -193,7 +193,7 @@ export class NoteEntityService implements OnModuleInit {
} else { } else {
const isFollowing = hint?.myFollowing const isFollowing = hint?.myFollowing
? hint.myFollowing.has(packedNote.userId) ? hint.myFollowing.has(packedNote.userId)
: (await this.cacheService.userFollowingsCache.fetch(meId))[packedNote.userId] != null; : (await this.cacheService.userFollowingsCache.fetch(meId)).has(packedNote.userId);
hide = !isFollowing; hide = !isFollowing;
} }
@ -358,14 +358,10 @@ export class NoteEntityService implements OnModuleInit {
: this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)), : this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)),
hint?.myFollowing hint?.myFollowing
? hint.myFollowing.has(note.userId) ? hint.myFollowing.has(note.userId)
: this.followingsRepository.existsBy({ : this.cacheService.userFollowingsCache.fetch(meId).then(ids => ids.has(note.userId)),
followeeId: note.userId,
followerId: meId,
}),
hint?.me !== undefined hint?.me !== undefined
? (hint.me?.host ?? null) ? (hint.me?.host ?? null)
: this.cacheService.userByIdCache.fetch(meId, () => this.usersRepository.findOneByOrFail({ id: meId })) : this.cacheService.findUserById(meId).then(me => me.host),
.then(me => me.host),
]); ]);
if (blocked) return false; if (blocked) return false;
@ -420,7 +416,7 @@ export class NoteEntityService implements OnModuleInit {
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>; packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
mentionHandles: Record<string, string | undefined>; mentionHandles: Record<string, string | undefined>;
userFollowings: Map<string, Set<string>>; userFollowings: Map<string, Map<string, { withReplies: boolean }>>;
userBlockers: Map<string, Set<string>>; userBlockers: Map<string, Set<string>>;
polls: Map<string, MiPoll>; polls: Map<string, MiPoll>;
pollVotes: Map<string, Map<string, MiPollVote[]>>; pollVotes: Map<string, Map<string, MiPollVote[]>>;

View file

@ -164,7 +164,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: !ps.withBots, excludeBots: !ps.withBots,
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) && note.reply.userId !== me.id) return false; if (!followings.has(note.reply.userId) && note.reply.userId !== me.id) return false;
} }
return true; return true;

View file

@ -115,7 +115,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) && note.reply.userId !== me.id) return false; if (!followings.has(note.reply.userId) && note.reply.userId !== me.id) return false;
} }
if (!ps.withBots && note.user?.isBot) return false; if (!ps.withBots && note.user?.isBot) return false;

View file

@ -134,7 +134,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`);
if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`); if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`);
const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); const isFollowing = me && (await this.cacheService.userFollowingsCache.fetch(me.id)).has(ps.userId);
const timeline = await this.fanoutTimelineEndpointService.timeline({ const timeline = await this.fanoutTimelineEndpointService.timeline({
untilId, untilId,

View file

@ -36,7 +36,7 @@ export default class Connection {
private channels = new Map<string, Channel>(); private channels = new Map<string, Channel>();
private subscribingNotes = new Map<string, number>(); private subscribingNotes = new Map<string, number>();
public userProfile: MiUserProfile | null = null; public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; public following: Map<string, { withReplies: boolean }> = new Map();
public followingChannels: Set<string> = new Set(); public followingChannels: Set<string> = new Set();
public userIdsWhoMeMuting: Set<string> = new Set(); public userIdsWhoMeMuting: Set<string> = new Set();
public userIdsWhoBlockingMe: Set<string> = new Set(); public userIdsWhoBlockingMe: Set<string> = new Set();

View file

@ -70,7 +70,7 @@ export default abstract class Channel {
if (!this.user) return false; if (!this.user) return false;
if (this.user.id === note.userId) return true; if (this.user.id === note.userId) return true;
if (note.visibility === 'followers') { if (note.visibility === 'followers') {
return this.following[note.userId] != null; return this.following.has(note.userId);
} }
if (!note.visibleUserIds) return false; if (!note.visibleUserIds) return false;
return note.visibleUserIds.includes(this.user.id); return note.visibleUserIds.includes(this.user.id);
@ -84,7 +84,7 @@ export default abstract class Channel {
if (note.user.requireSigninToViewContents && !this.user) return true; if (note.user.requireSigninToViewContents && !this.user) return true;
// 流れてきたNoteがインスタンスミュートしたインスタンスが関わる // 流れてきたNoteがインスタンスミュートしたインスタンスが関わる
if (isInstanceMuted(note, this.userMutedInstances) && !this.following[note.userId]) return true; if (isInstanceMuted(note, this.userMutedInstances) && !this.following.has(note.userId)) return true;
// 流れてきたNoteがミュートしているユーザーが関わる // 流れてきたNoteがミュートしているユーザーが関わる
if (isUserRelated(note, this.userIdsWhoMeMuting)) return true; if (isUserRelated(note, this.userIdsWhoMeMuting)) return true;
@ -101,7 +101,7 @@ export default abstract class Channel {
if (note.user.isSilenced || note.user.instance?.isSilenced) { if (note.user.isSilenced || note.user.instance?.isSilenced) {
if (this.user == null) return true; if (this.user == null) return true;
if (this.user.id === note.userId) return false; if (this.user.id === note.userId) return false;
if (this.following[note.userId] == null) return true; if (!this.following.has(note.userId)) return true;
} }
// TODO muted threads // TODO muted threads

View file

@ -62,7 +62,7 @@ class BubbleTimelineChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies) { if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -63,7 +63,7 @@ class GlobalTimelineChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies) { if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -47,7 +47,7 @@ class HomeTimelineChannel extends Channel {
if (!this.followingChannels.has(note.channelId)) return; if (!this.followingChannels.has(note.channelId)) return;
} else { } else {
// その投稿のユーザーをフォローしていなかったら弾く // その投稿のユーザーをフォローしていなかったら弾く
if (!isMe && !Object.hasOwn(this.following, note.userId)) return; if (!isMe && !this.following.has(note.userId)) return;
} }
if (this.isNoteMutedOrBlocked(note)) return; if (this.isNoteMutedOrBlocked(note)) return;
@ -57,7 +57,7 @@ class HomeTimelineChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies) { if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -62,7 +62,7 @@ class HybridTimelineChannel extends Channel {
// フォローしているチャンネルの投稿 の場合だけ // フォローしているチャンネルの投稿 の場合だけ
if (!( if (!(
(note.channelId == null && isMe) || (note.channelId == null && isMe) ||
(note.channelId == null && Object.hasOwn(this.following, note.userId)) || (note.channelId == null && this.following.has(note.userId)) ||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) || (note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId)) (note.channelId != null && this.followingChannels.has(note.channelId))
)) return; )) return;
@ -74,7 +74,7 @@ class HybridTimelineChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies && !this.withReplies) { if (!this.following.get(note.userId)?.withReplies && !this.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -67,7 +67,7 @@ class LocalTimelineChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies) { if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -55,7 +55,7 @@ class RoleTimelineChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies) { if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -98,7 +98,7 @@ class UserListChannel extends Channel {
const reply = note.reply; const reply = note.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (!this.isNoteVisibleToMe(reply)) return; if (!this.isNoteVisibleToMe(reply)) return;
if (!this.following[note.userId]?.withReplies) { if (!this.following.get(note.userId)?.withReplies) {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
} }

View file

@ -6,7 +6,7 @@
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { FakeInternalEventService } from './FakeInternalEventService.js'; import { FakeInternalEventService } from './FakeInternalEventService.js';
import type { BlockingsRepository, FollowingsRepository, MiFollowing, MiUser, MiUserProfile, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { BlockingsRepository, FollowingsRepository, MiUser, MiUserProfile, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js'; import type { MiLocalUser } from '@/models/User.js';
import { MemoryKVCache, MemorySingleCache, RedisKVCache, RedisSingleCache } from '@/misc/cache.js'; import { MemoryKVCache, MemorySingleCache, RedisKVCache, RedisSingleCache } from '@/misc/cache.js';
import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js'; import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js';
@ -106,7 +106,7 @@ export class NoOpCacheService extends CacheService {
onSet: this.renoteMutingsCache.onSet, onSet: this.renoteMutingsCache.onSet,
onDelete: this.renoteMutingsCache.onDelete, onDelete: this.renoteMutingsCache.onDelete,
}); });
this.userFollowingsCache = new NoOpQuantumKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>({ this.userFollowingsCache = new NoOpQuantumKVCache<Map<string, { withReplies: boolean }>>({
internalEventService: fakeInternalEventService, internalEventService: fakeInternalEventService,
fetcher: this.userFollowingsCache.fetcher, fetcher: this.userFollowingsCache.fetcher,
onSet: this.userFollowingsCache.onSet, onSet: this.userFollowingsCache.onSet,