implement note mutings and move favorited/renoted status into note entity directly

This commit is contained in:
Hazelnoot 2025-06-10 00:46:52 -04:00
parent 9bebf7718f
commit 7200c3d6c8
24 changed files with 342 additions and 181 deletions

View file

@ -47,6 +47,7 @@ export class CacheService implements OnApplicationShutdown {
public userBlockedCache: QuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: QuantumKVCache<Set<string>>;
public threadMutingsCache: QuantumKVCache<Set<string>>;
public noteMutingsCache: QuantumKVCache<Set<string>>;
public userFollowingsCache: QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
public userFollowersCache: QuantumKVCache<Map<string, Omit<MiFollowing, 'isFollowerHibernated'>>>;
public hibernatedUserCache: QuantumKVCache<boolean>;
@ -152,13 +153,27 @@ export class CacheService implements OnApplicationShutdown {
this.threadMutingsCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'threadMutings', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: muterId => this.noteThreadMutingsRepository
.find({ where: { userId: muterId }, select: { threadId: true } })
.find({ where: { userId: muterId, isPostMute: false }, select: { threadId: true } })
.then(ms => new Set(ms.map(m => m.threadId))),
bulkFetcher: muterIds => this.noteThreadMutingsRepository
.createQueryBuilder('muting')
.select('"muting"."userId"', 'userId')
.addSelect('array_agg("muting"."threadId")', 'threadIds')
.where({ userId: In(muterIds) })
.where({ userId: In(muterIds), isPostMute: false })
.getRawMany<{ userId: string, threadIds: string[] }>()
.then(ms => ms.map(m => [m.userId, new Set(m.threadIds)])),
});
this.noteMutingsCache = new QuantumKVCache<Set<string>>(this.internalEventService, 'noteMutings', {
lifetime: 1000 * 60 * 30, // 30m
fetcher: muterId => this.noteThreadMutingsRepository
.find({ where: { userId: muterId, isPostMute: true }, select: { threadId: true } })
.then(ms => new Set(ms.map(m => m.threadId))),
bulkFetcher: muterIds => this.noteThreadMutingsRepository
.createQueryBuilder('muting')
.select('"muting"."userId"', 'userId')
.addSelect('array_agg("muting"."threadId")', 'threadIds')
.where({ userId: In(muterIds), isPostMute: true })
.getRawMany<{ userId: string, threadIds: string[] }>()
.then(ms => ms.map(m => [m.userId, new Set(m.threadIds)])),
});
@ -290,6 +305,8 @@ export class CacheService implements OnApplicationShutdown {
this.userFollowingsCache.delete(body.id),
this.userFollowersCache.delete(body.id),
this.hibernatedUserCache.delete(body.id),
this.threadMutingsCache.delete(body.id),
this.noteMutingsCache.delete(body.id),
]);
}
} else {
@ -560,7 +577,11 @@ export class CacheService implements OnApplicationShutdown {
this.userBlockingCache.dispose();
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();
this.threadMutingsCache.dispose();
this.noteMutingsCache.dispose();
this.userFollowingsCache.dispose();
this.userFollowersCache.dispose();
this.hibernatedUserCache.dispose();
}
@bindThis

View file

@ -394,6 +394,7 @@ export class WebhookTestService {
private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise<Packed<'Note'>> {
return {
id: note.id,
threadId: note.threadId ?? note.id,
createdAt: new Date().toISOString(),
deletedAt: null,
text: note.text,
@ -403,6 +404,10 @@ export class WebhookTestService {
replyId: note.replyId,
renoteId: note.renoteId,
isHidden: false,
isMutingThread: false,
isMutingNote: false,
isFavorited: false,
isRenoted: false,
visibility: note.visibility,
mentions: note.mentions,
visibleUserIds: note.visibleUserIds,

View file

@ -11,11 +11,12 @@ import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel, MiFollowing } from '@/models/_.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel, MiFollowing, NoteFavoritesRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { QueryService } from '@/core/QueryService.js';
import { isPackedPureRenote } from '@/misc/is-renote.js';
import type { Config } from '@/config.js';
import type { OnModuleInit } from '@nestjs/common';
@ -55,6 +56,7 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
return appearNoteIds;
}
// noinspection ES6MissingAwait
@Injectable()
export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
@ -96,6 +98,10 @@ export class NoteEntityService implements OnModuleInit {
@Inject(DI.config)
private readonly config: Config,
@Inject(DI.noteFavoritesRepository)
private noteFavoritesRepository: NoteFavoritesRepository,
private readonly queryService: QueryService,
//private userEntityService: UserEntityService,
//private driveFileEntityService: DriveFileEntityService,
//private customEmojiService: CustomEmojiService,
@ -423,6 +429,9 @@ export class NoteEntityService implements OnModuleInit {
channels: Map<string, MiChannel>;
notes: Map<string, MiNote>;
mutedThreads: Set<string>;
mutedNotes: Set<string>;
favoriteNotes: Set<string>;
renotedNotes: Set<string>;
};
},
): Promise<Packed<'Note'>> {
@ -462,6 +471,23 @@ export class NoteEntityService implements OnModuleInit {
const packedUsers = options?._hint_?.packedUsers;
const threadId = note.threadId ?? note.id;
const [mutedThreads, mutedNotes, isFavorited, isRenoted] = await Promise.all([
// mutedThreads
opts._hint_?.mutedThreads
?? (meId ? this.cacheService.threadMutingsCache.fetch(meId) : new Set<string>()),
// mutedNotes
opts._hint_?.mutedNotes
?? (meId ? this.cacheService.noteMutingsCache.fetch(meId) : new Set<string>),
// isFavorited
opts._hint_?.favoriteNotes.has(note.id)
?? (meId ? this.noteFavoritesRepository.existsBy({ userId: meId, noteId: note.id }) : false),
// isRenoted
opts._hint_?.renotedNotes.has(note.id)
?? (meId ? this.queryService
.andIsRenote(this.notesRepository.createQueryBuilder('note'), 'note')
.andWhere({ renoteId: note.id, userId: meId })
.getExists() : false),
]);
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
@ -505,8 +531,10 @@ export class NoteEntityService implements OnModuleInit {
poll: opts._hint_?.polls.get(note.id),
myVotes: opts._hint_?.pollVotes.get(note.id)?.get(note.userId),
}) : undefined,
isMuting: opts._hint_?.mutedThreads.has(threadId)
?? (meId != null && this.cacheService.threadMutingsCache.fetch(meId).then(ms => ms.has(threadId))),
isMutingThread: mutedThreads.has(threadId),
isMutingNote: mutedNotes.has(note.id),
isFavorited,
isRenoted,
...(meId && Object.keys(reactions).length > 0 ? {
myReaction: this.populateMyReaction({
@ -654,7 +682,7 @@ export class NoteEntityService implements OnModuleInit {
const fileIds = new Set(targetNotes.flatMap(n => n.fileIds));
const mentionedUsers = new Set(targetNotes.flatMap(note => note.mentions));
const [{ bufferedReactions, myReactionsMap }, packedFiles, packedUsers, mentionHandles, userFollowings, userBlockers, polls, pollVotes, channels, mutedThreads] = await Promise.all([
const [{ bufferedReactions, myReactionsMap }, packedFiles, packedUsers, mentionHandles, userFollowings, userBlockers, polls, pollVotes, channels, mutedThreads, mutedNotes, favoriteNotes, renotedNotes] = await Promise.all([
// bufferedReactions & myReactionsMap
this.getReactions(targetNotes, me),
// packedFiles
@ -692,6 +720,22 @@ export class NoteEntityService implements OnModuleInit {
this.getChannels(targetNotes),
// mutedThreads
me ? this.cacheService.threadMutingsCache.fetch(me.id) : new Set<string>(),
// mutedNotes
me ? this.cacheService.noteMutingsCache.fetch(me.id) : new Set<string>(),
// favoriteNotes
me ? this.noteFavoritesRepository
.createQueryBuilder('favorite')
.select('favorite.noteId', 'noteId')
.where({ userId: me.id, noteId: In(noteIds) })
.getRawMany<{ noteId: string }>()
.then(fs => new Set(fs.map(f => f.noteId))) : new Set<string>(),
// renotedNotes
me ? this.queryService
.andIsRenote(this.notesRepository.createQueryBuilder('note'), 'note')
.andWhere({ renoteId: In(noteIds), userId: me.id })
.select('note.id', 'id')
.getRawMany<{ id: string }>()
.then(ns => new Set(ns.map(n => n.id))) : new Set<string>(),
// (not returned)
this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)),
]);
@ -711,6 +755,9 @@ export class NoteEntityService implements OnModuleInit {
channels,
notes: new Map(targetNotes.map(n => [n.id, n])),
mutedThreads,
mutedNotes,
favoriteNotes,
renotedNotes,
},
})));
}