fix user list API schema, access checks, and caching
This commit is contained in:
parent
2a948b7710
commit
73cc50fe90
22 changed files with 233 additions and 135 deletions
|
|
@ -6,7 +6,7 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { In, IsNull, Not } from 'typeorm';
|
||||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing, NoteThreadMutingsRepository, ChannelFollowingsRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing, NoteThreadMutingsRepository, ChannelFollowingsRepository, UserListMembershipsRepository, UserListFavoritesRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import type { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { isLocalUser, isRemoteUser } from '@/models/User.js';
|
||||
|
|
@ -41,6 +41,20 @@ export class CacheService implements OnApplicationShutdown {
|
|||
public readonly userBlockedCache: ManagedQuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||
public readonly userListMembershipsCache: ManagedQuantumKVCache<Map<string, MiUserListMembership>>;
|
||||
public readonly listUserMembershipsCache: ManagedQuantumKVCache<Map<string, MiUserListMembership>>;
|
||||
|
||||
/**
|
||||
* Maps user IDs (key) to the set of list IDs (value) that are favorited by that user
|
||||
*/
|
||||
public readonly userListFavoritesCache: ManagedQuantumKVCache<Set<string>>;
|
||||
|
||||
/**
|
||||
* Maps list IDs (key) to the set of user IDs (value) who have favorited this list.
|
||||
*/
|
||||
public readonly listUserFavoritesCache: ManagedQuantumKVCache<Set<string>>;
|
||||
|
||||
/**
|
||||
* Maps user IDs (key) to the set of user IDs (value) who's renotes are muted by that user.
|
||||
*/
|
||||
public readonly renoteMutingsCache: ManagedQuantumKVCache<Set<string>>;
|
||||
public readonly threadMutingsCache: ManagedQuantumKVCache<Set<string>>;
|
||||
public readonly noteMutingsCache: ManagedQuantumKVCache<Set<string>>;
|
||||
|
|
@ -84,6 +98,9 @@ export class CacheService implements OnApplicationShutdown {
|
|||
@Inject(DI.userListMembershipsRepository)
|
||||
private readonly userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.userListFavoritesRepository)
|
||||
private readonly userListFavoritesRepository: UserListFavoritesRepository,
|
||||
|
||||
private readonly internalEventService: InternalEventService,
|
||||
private readonly cacheManagementService: CacheManagementService,
|
||||
) {
|
||||
|
|
@ -173,6 +190,32 @@ export class CacheService implements OnApplicationShutdown {
|
|||
}, new Map<string, Map<string, MiUserListMembership>>)),
|
||||
});
|
||||
|
||||
this.userListFavoritesCache = cacheManagementService.createQuantumKVCache<Set<string>>('userListFavorites', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
fetcher: async userId => await this.userListFavoritesRepository.findBy({ userId }).then(fs => new Set(fs.map(f => f.userListId))),
|
||||
bulkFetcher: async userIds => await this.userListFavoritesRepository
|
||||
.createQueryBuilder('favorite')
|
||||
.select('"favorite"."userId"', 'userId')
|
||||
.addSelect('array_agg("favorite"."userListId")', 'userListIds')
|
||||
.where({ userId: In(userIds) })
|
||||
.groupBy('favorite.userId')
|
||||
.getRawMany<{ userId: string, userListIds: string[] }>()
|
||||
.then(fs => fs.map(f => [f.userId, new Set(f.userListIds)])),
|
||||
});
|
||||
|
||||
this.listUserFavoritesCache = cacheManagementService.createQuantumKVCache<Set<string>>('listUserFavorites', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
fetcher: async userListId => await this.userListFavoritesRepository.findBy({ userListId }).then(fs => new Set(fs.map(f => f.userId))),
|
||||
bulkFetcher: async userListIds => await this.userListFavoritesRepository
|
||||
.createQueryBuilder('favorite')
|
||||
.select('"favorite"."userListId"', 'userListId')
|
||||
.addSelect('array_agg("favorite"."userId")', 'userIds')
|
||||
.where({ userListId: In(userListIds) })
|
||||
.groupBy('favorite.userListId')
|
||||
.getRawMany<{ userListId: string, userIds: string[] }>()
|
||||
.then(fs => fs.map(f => [f.userListId, new Set(f.userIds)])),
|
||||
});
|
||||
|
||||
this.renoteMutingsCache = this.cacheManagementService.createQuantumKVCache<Set<string>>('renoteMutings', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||
|
|
|
|||
|
|
@ -21,11 +21,14 @@ import type { RoleService } from '@/core/RoleService.js';
|
|||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { InternalEventService } from '@/global/InternalEventService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { CacheManagementService, type ManagedQuantumKVCache } from '@/global/CacheManagementService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListService implements OnModuleInit {
|
||||
public static TooManyUsersError = class extends Error {};
|
||||
|
||||
public readonly userListsCache: ManagedQuantumKVCache<MiUserList>;
|
||||
|
||||
private roleService: RoleService;
|
||||
|
||||
constructor(
|
||||
|
|
@ -53,7 +56,15 @@ export class UserListService implements OnModuleInit {
|
|||
private systemAccountService: SystemAccountService,
|
||||
private readonly internalEventService: InternalEventService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
|
||||
cacheManagementService: CacheManagementService,
|
||||
) {
|
||||
this.userListsCache = cacheManagementService.createQuantumKVCache('userLists', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
fetcher: async id => await this.userListsRepository.findOneBy({ id }),
|
||||
bulkFetcher: async ids => await this.userListsRepository.findBy({ id: In(ids) }).then(ls => ls.map(l => [l.id, l])),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
async onModuleInit() {
|
||||
|
|
@ -120,14 +131,10 @@ export class UserListService implements OnModuleInit {
|
|||
@bindThis
|
||||
public async bulkAddMember(target: { id: MiUser['id'] }, memberships: { userListId: MiUserList['id'], withReplies?: boolean }[]): Promise<void> {
|
||||
const userListIds = memberships.map(m => m.userListId);
|
||||
const userLists = await this.userListsCache.fetchMany(userListIds);
|
||||
|
||||
// Map userListId => userListUserId
|
||||
const listUserIds = await this.userListsRepository
|
||||
.find({
|
||||
where: { id: In(userListIds) },
|
||||
select: { id: true, userId: true },
|
||||
})
|
||||
.then(ls => new Map(ls.map(l => [l.id, l.userId])));
|
||||
const listUserIds = new Map(userLists.values.map(l => [l.id, l.userId]));
|
||||
|
||||
const toInsert = memberships.map(membership => ({
|
||||
id: this.idService.gen(),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { MiUserList } from '@/models/UserList.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -26,26 +27,36 @@ export class UserListEntityService {
|
|||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly userListService: UserListService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: MiUserList['id'] | MiUserList,
|
||||
meId: string | null | undefined,
|
||||
): Promise<Packed<'UserList'>> {
|
||||
const srcId = typeof(src) === 'object' ? src.id : src;
|
||||
|
||||
const [userList, users] = await Promise.all([
|
||||
typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }),
|
||||
const [userList, users, favorites] = await Promise.all([
|
||||
typeof src === 'object' ? src : this.userListService.userListsCache.fetch(src),
|
||||
this.cacheService.listUserMembershipsCache.fetch(srcId),
|
||||
this.cacheService.listUserFavoritesCache.fetch(srcId),
|
||||
]);
|
||||
|
||||
return {
|
||||
id: userList.id,
|
||||
createdAt: this.idService.parse(userList.id).date.toISOString(),
|
||||
createdBy: userList.userId,
|
||||
name: userList.name,
|
||||
userIds: users.keys().toArray(),
|
||||
isPublic: userList.isPublic,
|
||||
isLiked: meId != null
|
||||
? favorites.has(meId)
|
||||
: undefined,
|
||||
likedCount: userList.isPublic || meId === userList.userId
|
||||
? favorites.size
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue