From 30d699268450af375dabc2226ec4f3196a53f7f7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 Apr 2023 14:06:57 +0900 Subject: perf(backend): 通知をRedisに保存するように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #10168 --- .../src/server/api/endpoints/admin/suspend-user.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/admin') diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 3ad6c7c484..770b61850a 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -36,9 +36,6 @@ export default class extends Endpoint { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.notificationsRepository) - private notificationsRepository: NotificationsRepository, - private userEntityService: UserEntityService, private userFollowingService: UserFollowingService, private userSuspendService: UserSuspendService, @@ -73,7 +70,6 @@ export default class extends Endpoint { (async () => { await this.userSuspendService.doPostSuspend(user).catch(e => {}); await this.unFollowAll(user).catch(e => {}); - await this.readAllNotify(user).catch(e => {}); })(); }); } @@ -96,14 +92,4 @@ export default class extends Endpoint { await this.userFollowingService.unfollow(follower, followee, true); } } - - @bindThis - private async readAllNotify(notifier: User) { - await this.notificationsRepository.update({ - notifierId: notifier.id, - isRead: false, - }, { - isRead: true, - }); - } } -- cgit v1.2.3-freya From f44504097c360fc84179161abee47b79a936b455 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 5 Apr 2023 10:21:10 +0900 Subject: enhance(backend): improve cache --- packages/backend/src/core/CacheService.ts | 85 +++++++++- packages/backend/src/core/DeleteAccountService.ts | 3 - packages/backend/src/core/GlobalEventService.ts | 6 - packages/backend/src/core/NotificationService.ts | 6 +- packages/backend/src/core/UserBlockingService.ts | 130 ++------------ packages/backend/src/core/UserFollowingService.ts | 9 +- packages/backend/src/core/UserMutingService.ts | 31 +++- packages/backend/src/misc/cache.ts | 66 ++++---- .../CheckExpiredMutingsProcessorService.ts | 14 +- .../src/server/api/StreamingApiServerService.ts | 21 ++- .../server/api/endpoints/admin/accounts/delete.ts | 5 - .../src/server/api/endpoints/admin/suspend-user.ts | 5 - .../src/server/api/endpoints/channels/follow.ts | 3 - .../src/server/api/endpoints/channels/unfollow.ts | 4 - .../src/server/api/endpoints/i/regenerate-token.ts | 5 - .../src/server/api/endpoints/i/revoke-token.ts | 3 - .../backend/src/server/api/endpoints/i/update.ts | 1 - .../src/server/api/endpoints/mute/create.ts | 21 +-- .../src/server/api/endpoints/mute/delete.ts | 13 +- .../src/server/api/endpoints/renote-mute/delete.ts | 2 - packages/backend/src/server/api/stream/channel.ts | 12 +- .../src/server/api/stream/channels/antenna.ts | 6 +- .../src/server/api/stream/channels/channel.ts | 6 +- .../server/api/stream/channels/global-timeline.ts | 6 +- .../src/server/api/stream/channels/hashtag.ts | 6 +- .../server/api/stream/channels/home-timeline.ts | 11 +- .../server/api/stream/channels/hybrid-timeline.ts | 8 +- .../server/api/stream/channels/local-timeline.ts | 6 +- .../backend/src/server/api/stream/channels/main.ts | 4 +- .../src/server/api/stream/channels/user-list.ts | 6 +- packages/backend/src/server/api/stream/index.ts | 187 +++++---------------- packages/backend/src/server/api/stream/types.ts | 21 +-- 32 files changed, 264 insertions(+), 448 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/admin') diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 887baeb2c2..f0c311b9b0 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; -import type { UserProfile, UsersRepository } from '@/models/index.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; @@ -16,7 +16,12 @@ export class CacheService implements OnApplicationShutdown { public localUserByIdCache: MemoryKVCache; public uriPersonCache: MemoryKVCache; public userProfileCache: RedisKVCache; - public userMutingsCache: RedisKVCache; + public userMutingsCache: RedisKVCache>; + public userBlockingCache: RedisKVCache>; + public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ + public renoteMutingsCache: RedisKVCache>; + public userFollowingsCache: RedisKVCache>; + public userFollowingChannelsCache: RedisKVCache>; constructor( @Inject(DI.redis) @@ -28,6 +33,24 @@ export class CacheService implements OnApplicationShutdown { @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + private userEntityService: UserEntityService, ) { //this.onMessage = this.onMessage.bind(this); @@ -36,8 +59,62 @@ export class CacheService implements OnApplicationShutdown { this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); this.localUserByIdCache = new MemoryKVCache(Infinity); this.uriPersonCache = new MemoryKVCache(Infinity); - this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', 1000 * 60 * 60 * 24, 1000 * 60); - this.userMutingsCache = new RedisKVCache(this.redisClient, 'userMutings', 1000 * 60 * 60 * 24, 1000 * 60); + + this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }), + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮 + }); + + this.userMutingsCache = new RedisKVCache>(this.redisClient, 'userMutings', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + + this.userBlockingCache = new RedisKVCache>(this.redisClient, 'userBlocking', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + + this.userBlockedCache = new RedisKVCache>(this.redisClient, 'userBlocked', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + + this.renoteMutingsCache = new RedisKVCache>(this.redisClient, 'renoteMutings', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + + this.userFollowingsCache = new RedisKVCache>(this.redisClient, 'userFollowings', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); + + this.userFollowingChannelsCache = new RedisKVCache>(this.redisClient, 'userFollowingChannels', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), + toRedisConverter: (value) => JSON.stringify(Array.from(value)), + fromRedisConverter: (value) => new Set(JSON.parse(value)), + }); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 2acb5f2303..327283106f 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -36,8 +36,5 @@ export class DeleteAccountService { await this.usersRepository.update(user.id, { isDeleted: true, }); - - // Terminate streaming - this.globalEventService.publishUserEvent(user.id, 'terminate', {}); } } diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d261a6c657..25c064a2b4 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -14,7 +14,6 @@ import type { MainStreamTypes, NoteStreamTypes, UserListStreamTypes, - UserStreamTypes, } from '@/server/api/stream/types.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; @@ -49,11 +48,6 @@ export class GlobalEventService { this.publish('internal', type, typeof value === 'undefined' ? null : value); } - @bindThis - public publishUserEvent(userId: User['id'], type: K, value?: UserStreamTypes[K]): void { - this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); - } - @bindThis public publishBroadcastStream(type: K, value?: BroadcastTypes[K]): void { this.publish('broadcast', type, typeof value === 'undefined' ? null : value); diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 9c179f9318..366dc08c02 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -73,7 +73,7 @@ export class NotificationService implements OnApplicationShutdown { type: Notification['type'], data: Partial, ): Promise { - const profile = await this.cacheService.userProfileCache.fetch(notifieeId, () => this.userProfilesRepository.findOneByOrFail({ userId: notifieeId })); + const profile = await this.cacheService.userProfileCache.fetch(notifieeId); const isMuted = profile.mutingNotificationTypes.includes(type); if (isMuted) return null; @@ -82,8 +82,8 @@ export class NotificationService implements OnApplicationShutdown { return null; } - const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId, () => this.mutingsRepository.findBy({ muterId: notifieeId }).then(xs => xs.map(x => x.muteeId))); - if (mutings.includes(data.notifierId)) { + const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId); + if (mutings.has(data.notifierId)) { return null; } } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 040b6de2ef..6eaef8f68a 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -1,40 +1,26 @@ -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import Redis from 'ioredis'; +import { Inject, Injectable } from '@nestjs/common'; import { IdService } from '@/core/IdService.js'; import type { User } from '@/models/entities/User.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; -import { MemoryKVCache } from '@/misc/cache.js'; -import { StreamMessages } from '@/server/api/stream/types.js'; +import { CacheService } from '@/core/CacheService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; @Injectable() -export class UserBlockingService implements OnApplicationShutdown { +export class UserBlockingService { private logger: Logger; - // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ - private blockingsByUserIdCache: MemoryKVCache; - constructor( - @Inject(DI.redisSubscriber) - private redisSubscriber: Redis.Redis, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -47,47 +33,17 @@ export class UserBlockingService implements OnApplicationShutdown { @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, + private cacheService: CacheService, + private userFollowingService: UserFollowingService, private userEntityService: UserEntityService, private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, private webhookService: WebhookService, private apRendererService: ApRendererService, - private perUserFollowingChart: PerUserFollowingChart, private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('user-block'); - - this.blockingsByUserIdCache = new MemoryKVCache(Infinity); - - this.redisSubscriber.on('message', this.onMessage); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as StreamMessages['internal']['payload']; - switch (type) { - case 'blockingCreated': { - const cached = this.blockingsByUserIdCache.get(body.blockerId); - if (cached) { - this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]); - } - break; - } - case 'blockingDeleted': { - const cached = this.blockingsByUserIdCache.get(body.blockerId); - if (cached) { - this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId)); - } - break; - } - default: - break; - } - } } @bindThis @@ -95,8 +51,8 @@ export class UserBlockingService implements OnApplicationShutdown { await Promise.all([ this.cancelRequest(blocker, blockee), this.cancelRequest(blockee, blocker), - this.unFollow(blocker, blockee), - this.unFollow(blockee, blocker), + this.userFollowingService.unfollow(blocker, blockee), + this.userFollowingService.unfollow(blockee, blocker), this.removeFromList(blockee, blocker), ]); @@ -111,6 +67,9 @@ export class UserBlockingService implements OnApplicationShutdown { await this.blockingsRepository.insert(blocking); + this.cacheService.userBlockingCache.refresh(blocker.id); + this.cacheService.userBlockedCache.refresh(blockee.id); + this.globalEventService.publishInternalEvent('blockingCreated', { blockerId: blocker.id, blockeeId: blockee.id, @@ -148,7 +107,6 @@ export class UserBlockingService implements OnApplicationShutdown { this.userEntityService.pack(followee, follower, { detail: true, }).then(async packed => { - this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); @@ -173,54 +131,6 @@ export class UserBlockingService implements OnApplicationShutdown { } } - @bindThis - private async unFollow(follower: User, followee: User) { - const following = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (following == null) { - return; - } - - await Promise.all([ - this.followingsRepository.delete(following.id), - this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), - this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), - this.perUserFollowingChart.update(follower, followee, false), - ]); - - // Publish unfollow event - if (this.userEntityService.isLocalUser(follower)) { - this.userEntityService.pack(followee, follower, { - detail: true, - }).then(async packed => { - this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); - this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'unfollow', { - user: packed, - }); - } - }); - } - - // リモートにフォローをしていたらUndoFollow送信 - if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); - this.queueService.deliver(follower, content, followee.inbox, false); - } - - // リモートからフォローをされていたらRejectFollow送信 - if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { - const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); - this.queueService.deliver(followee, content, follower.inbox, false); - } - } - @bindThis private async removeFromList(listOwner: User, user: User) { const userLists = await this.userListsRepository.findBy({ @@ -254,6 +164,9 @@ export class UserBlockingService implements OnApplicationShutdown { await this.blockingsRepository.delete(blocking.id); + this.cacheService.userBlockingCache.refresh(blocker.id); + this.cacheService.userBlockedCache.refresh(blockee.id); + this.globalEventService.publishInternalEvent('blockingDeleted', { blockerId: blocker.id, blockeeId: blockee.id, @@ -268,17 +181,6 @@ export class UserBlockingService implements OnApplicationShutdown { @bindThis public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise { - const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({ - where: { - blockerId, - }, - select: ['blockeeId'], - }).then(records => records.map(record => record.blockeeId))); - return blockedUserIds.includes(blockeeId); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined) { - this.redisSubscriber.off('message', this.onMessage); + return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId); } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index b51b553c70..4f22c5cd46 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -18,6 +18,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { MetaService } from '@/core/MetaService.js'; +import { CacheService } from '@/core/CacheService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -53,6 +54,7 @@ export class UserFollowingService { @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, + private cacheService: CacheService, private userEntityService: UserEntityService, private userBlockingService: UserBlockingService, private idService: IdService, @@ -172,6 +174,8 @@ export class UserFollowingService { } }); + this.cacheService.userFollowingsCache.refresh(follower.id); + const req = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, @@ -225,7 +229,6 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { - this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); @@ -279,6 +282,8 @@ export class UserFollowingService { await this.followingsRepository.delete(following.id); + this.cacheService.userFollowingsCache.refresh(follower.id); + this.decrementFollowing(follower, followee); // Publish unfollow event @@ -286,7 +291,6 @@ export class UserFollowingService { this.userEntityService.pack(followee.id, follower, { detail: true, }).then(async packed => { - this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); @@ -579,7 +583,6 @@ export class UserFollowingService { detail: true, }); - this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee); this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index e98f11709f..657b5764b9 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -1,34 +1,47 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, MutingsRepository } from '@/models/index.js'; +import { In } from 'typeorm'; +import type { MutingsRepository, Muting } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { CacheService } from '@/core/CacheService'; @Injectable() export class UserMutingService { constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, private idService: IdService, - private queueService: QueueService, - private globalEventService: GlobalEventService, + private cacheService: CacheService, ) { } @bindThis - public async mute(user: User, target: User): Promise { + public async mute(user: User, target: User, expiresAt: Date | null = null): Promise { await this.mutingsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), + expiresAt: expiresAt ?? null, muterId: user.id, muteeId: target.id, }); + + this.cacheService.userMutingsCache.refresh(user.id); + } + + @bindThis + public async unmute(mutings: Muting[]): Promise { + if (mutings.length === 0) return; + + await this.mutingsRepository.delete({ + id: In(mutings.map(m => m.id)), + }); + + const muterIds = [...new Set(mutings.map(m => m.muterId))]; + for (const muterId of muterIds) { + this.cacheService.userMutingsCache.refresh(muterId); + } } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 870dfd237c..ef6f610125 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,29 +1,29 @@ import Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; -// redis通すとDateのインスタンスはstringに変換されるので -type Serialized = { - [K in keyof T]: - T[K] extends Date - ? string - : T[K] extends (Date | null) - ? (string | null) - : T[K] extends Record - ? Serialized - : T[K]; -}; - export class RedisKVCache { private redisClient: Redis.Redis; private name: string; private lifetime: number; private memoryCache: MemoryKVCache; + private fetcher: (key: string) => Promise; + private toRedisConverter: (value: T) => string; + private fromRedisConverter: (value: string) => T; - constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], lifetime: RedisKVCache['lifetime'], memoryCacheLifetime: number) { + constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], opts: { + lifetime: RedisKVCache['lifetime']; + memoryCacheLifetime: number; + fetcher: RedisKVCache['fetcher']; + toRedisConverter: RedisKVCache['toRedisConverter']; + fromRedisConverter: RedisKVCache['fromRedisConverter']; + }) { this.redisClient = redisClient; this.name = name; - this.lifetime = lifetime; - this.memoryCache = new MemoryKVCache(memoryCacheLifetime); + this.lifetime = opts.lifetime; + this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime); + this.fetcher = opts.fetcher; + this.toRedisConverter = opts.toRedisConverter; + this.fromRedisConverter = opts.fromRedisConverter; } @bindThis @@ -32,25 +32,25 @@ export class RedisKVCache { if (this.lifetime === Infinity) { await this.redisClient.set( `kvcache:${this.name}:${key}`, - JSON.stringify(value), + this.toRedisConverter(value), ); } else { await this.redisClient.set( `kvcache:${this.name}:${key}`, - JSON.stringify(value), + this.toRedisConverter(value), 'ex', Math.round(this.lifetime / 1000), ); } } @bindThis - public async get(key: string): Promise | T | undefined> { + public async get(key: string): Promise { const memoryCached = this.memoryCache.get(key); if (memoryCached !== undefined) return memoryCached; const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`); if (cached == null) return undefined; - return JSON.parse(cached); + return this.fromRedisConverter(cached); } @bindThis @@ -60,29 +60,29 @@ export class RedisKVCache { } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - */ + * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します + */ @bindThis - public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: Serialized | T) => boolean): Promise | T> { + public async fetch(key: string): Promise { const cachedValue = await this.get(key); if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; - } + // Cache HIT + return cachedValue; } // Cache MISS - const value = await fetcher(); + const value = await this.fetcher(key); this.set(key, value); return value; } + + @bindThis + public async refresh(key: string) { + const value = await this.fetcher(key); + this.set(key, value); + + // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする + } } // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index f4cd560fc9..2476d71a5e 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -4,10 +4,10 @@ import { DI } from '@/di-symbols.js'; import type { MutingsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { bindThis } from '@/decorators.js'; +import { UserMutingService } from '@/core/UserMutingService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import { bindThis } from '@/decorators.js'; @Injectable() export class CheckExpiredMutingsProcessorService { @@ -20,7 +20,7 @@ export class CheckExpiredMutingsProcessorService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - private globalEventService: GlobalEventService, + private userMutingService: UserMutingService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings'); @@ -37,13 +37,7 @@ export class CheckExpiredMutingsProcessorService { .getMany(); if (expired.length > 0) { - await this.mutingsRepository.delete({ - id: In(expired.map(m => m.id)), - }); - - for (const m of expired) { - this.globalEventService.publishUserEvent(m.muterId, 'unmute', m.mutee!); - } + await this.userMutingService.unmute(expired); } this.logger.succ('All expired mutings checked.'); diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 13526f277d..bd2d436a23 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -9,6 +9,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; +import { CacheService } from '@/core/CacheService.js'; import { AuthenticateService } from './AuthenticateService.js'; import MainStreamConnection from './stream/index.js'; import { ChannelsService } from './stream/ChannelsService.js'; @@ -45,7 +46,7 @@ export class StreamingApiServerService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private globalEventService: GlobalEventService, + private cacheService: CacheService, private noteReadService: NoteReadService, private authenticateService: AuthenticateService, private channelsService: ChannelsService, @@ -73,8 +74,6 @@ export class StreamingApiServerService { return; } - const connection = request.accept(); - const ev = new EventEmitter(); async function onRedisMessage(_: string, data: string): Promise { @@ -85,19 +84,19 @@ export class StreamingApiServerService { this.redisSubscriber.on('message', onRedisMessage); const main = new MainStreamConnection( - this.followingsRepository, - this.mutingsRepository, - this.renoteMutingsRepository, - this.blockingsRepository, - this.channelFollowingsRepository, - this.userProfilesRepository, this.channelsService, - this.globalEventService, this.noteReadService, this.notificationService, - connection, ev, user, miapp, + this.cacheService, + ev, user, miapp, ); + await main.init(); + + const connection = request.accept(); + + main.init2(connection); + const intervalId = user ? setInterval(() => { this.usersRepository.update(user.id, { lastActiveDate: new Date(), diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index e9f72676f0..16232813a8 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -61,11 +61,6 @@ export default class extends Endpoint { await this.usersRepository.update(user.id, { isDeleted: true, }); - - if (this.userEntityService.isLocalUser(user)) { - // Terminate streaming - this.globalEventService.publishUserEvent(user.id, 'terminate', {}); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 770b61850a..3c99225272 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -62,11 +62,6 @@ export default class extends Endpoint { targetId: user.id, }); - // Terminate streaming - if (this.userEntityService.isLocalUser(user)) { - this.globalEventService.publishUserEvent(user.id, 'terminate', {}); - } - (async () => { await this.userSuspendService.doPostSuspend(user).catch(e => {}); await this.unFollowAll(user).catch(e => {}); diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 91693918f2..8ab59991c7 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -41,7 +41,6 @@ export default class extends Endpoint { private channelFollowingsRepository: ChannelFollowingsRepository, private idService: IdService, - private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const channel = await this.channelsRepository.findOneBy({ @@ -58,8 +57,6 @@ export default class extends Endpoint { followerId: me.id, followeeId: channel.id, }); - - this.globalEventService.publishUserEvent(me.id, 'followChannel', channel); }); } } diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index ac2ef825be..855ba47f8c 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -38,8 +38,6 @@ export default class extends Endpoint { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, - - private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const channel = await this.channelsRepository.findOneBy({ @@ -54,8 +52,6 @@ export default class extends Endpoint { followerId: me.id, followeeId: channel.id, }); - - this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 786e64374c..23ff63f5e9 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -54,11 +54,6 @@ export default class extends Endpoint { // Publish event this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken }); this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated'); - - // Terminate streaming - setTimeout(() => { - this.globalEventService.publishUserEvent(me.id, 'terminate', {}); - }, 5000); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 5e1dddb6b7..93daeb0cd7 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -35,9 +35,6 @@ export default class extends Endpoint { id: ps.tokenId, userId: me.id, }); - - // Terminate streaming - this.globalEventService.publishUserEvent(me.id, 'terminate'); } }); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 46b16e9dce..c20f2b7913 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -284,7 +284,6 @@ export default class extends Endpoint { // Publish meUpdated event this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj); - this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', updatedProfile); // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 if (user.isLocked && ps.isLocked === false) { diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index fd062e1cab..6e24e1024d 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,13 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; import type { MutingsRepository } from '@/models/index.js'; -import type { Muting } from '@/models/entities/Muting.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; -import { CacheService } from '@/core/CacheService.js'; +import { UserMutingService } from '@/core/UserMutingService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -63,10 +60,8 @@ export default class extends Endpoint { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - private globalEventService: GlobalEventService, private getterService: GetterService, - private idService: IdService, - private cacheService: CacheService, + private userMutingService: UserMutingService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -96,17 +91,7 @@ export default class extends Endpoint { return; } - // Create mute - await this.mutingsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, - muterId: muter.id, - muteeId: mutee.id, - } as Muting); - - this.cacheService.userMutingsCache.delete(muter.id); - this.globalEventService.publishUserEvent(me.id, 'mute', mutee); + await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null); }); } } diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index 612c4a4c04..90b74590be 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -1,10 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { MutingsRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { UserMutingService } from '@/core/UserMutingService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['account'], @@ -49,7 +49,7 @@ export default class extends Endpoint { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - private globalEventService: GlobalEventService, + private userMutingService: UserMutingService, private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { @@ -76,12 +76,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.notMuting); } - // Delete mute - await this.mutingsRepository.delete({ - id: exist.id, - }); - - this.globalEventService.publishUserEvent(me.id, 'unmute', mutee); + await this.userMutingService.unmute([exist]); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 51a895fb7e..70901a1406 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -80,8 +80,6 @@ export default class extends Endpoint { await this.renoteMutingsRepository.delete({ id: exist.id, }); - - // publishUserEvent(user.id, 'unmute', mutee); }); } } diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 32935325aa..e67aec9ecd 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -23,16 +23,16 @@ export default abstract class Channel { return this.connection.following; } - protected get muting() { - return this.connection.muting; + protected get userIdsWhoMeMuting() { + return this.connection.userIdsWhoMeMuting; } - protected get renoteMuting() { - return this.connection.renoteMuting; + protected get userIdsWhoMeMutingRenotes() { + return this.connection.userIdsWhoMeMutingRenotes; } - protected get blocking() { - return this.connection.blocking; + protected get userIdsWhoBlockingMe() { + return this.connection.userIdsWhoBlockingMe; } protected get followingChannels() { diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index e2a42fbfe9..d48dea7258 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -35,11 +35,11 @@ class AntennaChannel extends Channel { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 12caa7f233..9e5b40997b 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -47,11 +47,11 @@ class ChannelChannel extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index d79247cd6e..5454836fe1 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -64,11 +64,11 @@ class GlobalTimelineChannel extends Channel { if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 98dc858ded..0268fdedde 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -46,11 +46,11 @@ class HashtagChannel extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index c623fef64a..ee874ad81e 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -24,7 +24,6 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { - // Subscribe events this.subscriber.on('notesStream', this.onNote); } @@ -38,7 +37,7 @@ class HomeTimelineChannel extends Channel { } // Ignore notes from instances the user has muted - if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; if (['followers', 'specified'].includes(note.visibility)) { note = await this.noteEntityService.pack(note.id, this.user!, { @@ -71,18 +70,18 @@ class HomeTimelineChannel extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; + if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index f54767bc9d..4f7b4e78b6 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -72,7 +72,7 @@ class HybridTimelineChannel extends Channel { } // Ignore notes from instances the user has muted - if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; // 関係ない返信は除外 if (note.reply && !this.user!.showTimelineReplies) { @@ -82,11 +82,11 @@ class HybridTimelineChannel extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index eb0642900d..836c5aae6c 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -61,11 +61,11 @@ class LocalTimelineChannel extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; // 流れてきたNoteがミュートすべきNoteだったら無視する // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 4dd16b530a..139320ce35 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -26,7 +26,7 @@ class MainChannel extends Channel { case 'notification': { // Ignore notifications from instances the user has muted if (isUserFromMutedInstance(data.body, new Set(this.userProfile?.mutedInstances ?? []))) return; - if (data.body.userId && this.muting.has(data.body.userId)) return; + if (data.body.userId && this.userIdsWhoMeMuting.has(data.body.userId)) return; if (data.body.note && data.body.note.isHidden) { const note = await this.noteEntityService.pack(data.body.note.id, this.user, { @@ -40,7 +40,7 @@ class MainChannel extends Channel { case 'mention': { if (isInstanceMuted(data.body, new Set(this.userProfile?.mutedInstances ?? []))) return; - if (this.muting.has(data.body.userId)) return; + if (this.userIdsWhoMeMuting.has(data.body.userId)) return; if (data.body.isHidden) { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true, diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 8a42e99a54..8802fc5ab8 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -89,11 +89,11 @@ class UserListChannel extends Channel { } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 2f473cd012..a6f9145952 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,13 +1,11 @@ import type { User } from '@/models/entities/User.js'; -import type { Channel as ChannelModel } from '@/models/entities/Channel.js'; -import type { FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; -import type { UserProfile } from '@/models/entities/UserProfile.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; +import { CacheService } from '@/core/CacheService.js'; +import { UserProfile } from '@/models/index.js'; import type { ChannelsService } from './ChannelsService.js'; import type * as websocket from 'websocket'; import type { EventEmitter } from 'events'; @@ -19,106 +17,71 @@ import type { StreamEventEmitter, StreamMessages } from './types.js'; */ export default class Connection { public user?: User; - public userProfile?: UserProfile | null; - public following: Set = new Set(); - public muting: Set = new Set(); - public renoteMuting: Set = new Set(); - public blocking: Set = new Set(); // "被"blocking - public followingChannels: Set = new Set(); public token?: AccessToken; private wsConnection: websocket.connection; public subscriber: StreamEventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; + public userProfile: UserProfile | null = null; + public following: Set = new Set(); + public followingChannels: Set = new Set(); + public userIdsWhoMeMuting: Set = new Set(); + public userIdsWhoBlockingMe: Set = new Set(); + public userIdsWhoMeMutingRenotes: Set = new Set(); + private fetchIntervalId: NodeJS.Timer | null = null; constructor( - private followingsRepository: FollowingsRepository, - private mutingsRepository: MutingsRepository, - private renoteMutingsRepository: RenoteMutingsRepository, - private blockingsRepository: BlockingsRepository, - private channelFollowingsRepository: ChannelFollowingsRepository, - private userProfilesRepository: UserProfilesRepository, private channelsService: ChannelsService, - private globalEventService: GlobalEventService, private noteReadService: NoteReadService, private notificationService: NotificationService, + private cacheService: CacheService, - wsConnection: websocket.connection, subscriber: EventEmitter, user: User | null | undefined, token: AccessToken | null | undefined, ) { - this.wsConnection = wsConnection; this.subscriber = subscriber; if (user) this.user = user; if (token) this.token = token; + } - //this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this); - //this.onUserEvent = this.onUserEvent.bind(this); - //this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this); - //this.onBroadcastMessage = this.onBroadcastMessage.bind(this); - - this.wsConnection.on('message', this.onWsConnectionMessage); - - this.subscriber.on('broadcast', data => { - this.onBroadcastMessage(data); - }); + @bindThis + public async fetch() { + if (this.user == null) return; + const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([ + this.cacheService.userProfileCache.fetch(this.user.id), + this.cacheService.userFollowingsCache.fetch(this.user.id), + this.cacheService.userFollowingChannelsCache.fetch(this.user.id), + this.cacheService.userMutingsCache.fetch(this.user.id), + this.cacheService.userBlockedCache.fetch(this.user.id), + this.cacheService.renoteMutingsCache.fetch(this.user.id), + ]); + this.userProfile = userProfile; + this.following = following; + this.followingChannels = followingChannels; + this.userIdsWhoMeMuting = userIdsWhoMeMuting; + this.userIdsWhoBlockingMe = userIdsWhoBlockingMe; + this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; + } - if (this.user) { - this.updateFollowing(); - this.updateMuting(); - this.updateRenoteMuting(); - this.updateBlocking(); - this.updateFollowingChannels(); - this.updateUserProfile(); + @bindThis + public async init() { + if (this.user != null) { + await this.fetch(); - this.subscriber.on(`user:${this.user.id}`, this.onUserEvent); + this.fetchIntervalId = setInterval(this.fetch, 1000 * 10); } } @bindThis - private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう - switch (data.type) { - case 'follow': - this.following.add(data.body.id); - break; - - case 'unfollow': - this.following.delete(data.body.id); - break; - - case 'mute': - this.muting.add(data.body.id); - break; - - case 'unmute': - this.muting.delete(data.body.id); - break; - - // TODO: renote mute events - // TODO: block events - - case 'followChannel': - this.followingChannels.add(data.body.id); - break; - - case 'unfollowChannel': - this.followingChannels.delete(data.body.id); - break; - - case 'updateUserProfile': - this.userProfile = data.body; - break; - - case 'terminate': - this.wsConnection.close(); - this.dispose(); - break; - - default: - break; - } + public async init2(wsConnection: websocket.connection) { + this.wsConnection = wsConnection; + this.wsConnection.on('message', this.onWsConnectionMessage); + + this.subscriber.on('broadcast', data => { + this.onBroadcastMessage(data); + }); } /** @@ -318,78 +281,12 @@ export default class Connection { } } - @bindThis - private async updateFollowing() { - const followings = await this.followingsRepository.find({ - where: { - followerId: this.user!.id, - }, - select: ['followeeId'], - }); - - this.following = new Set(followings.map(x => x.followeeId)); - } - - @bindThis - private async updateMuting() { - const mutings = await this.mutingsRepository.find({ - where: { - muterId: this.user!.id, - }, - select: ['muteeId'], - }); - - this.muting = new Set(mutings.map(x => x.muteeId)); - } - - @bindThis - private async updateRenoteMuting() { - const renoteMutings = await this.renoteMutingsRepository.find({ - where: { - muterId: this.user!.id, - }, - select: ['muteeId'], - }); - - this.renoteMuting = new Set(renoteMutings.map(x => x.muteeId)); - } - - @bindThis - private async updateBlocking() { // ここでいうBlockingは被Blockingの意 - const blockings = await this.blockingsRepository.find({ - where: { - blockeeId: this.user!.id, - }, - select: ['blockerId'], - }); - - this.blocking = new Set(blockings.map(x => x.blockerId)); - } - - @bindThis - private async updateFollowingChannels() { - const followings = await this.channelFollowingsRepository.find({ - where: { - followerId: this.user!.id, - }, - select: ['followeeId'], - }); - - this.followingChannels = new Set(followings.map(x => x.followeeId)); - } - - @bindThis - private async updateUserProfile() { - this.userProfile = await this.userProfilesRepository.findOneBy({ - userId: this.user!.id, - }); - } - /** * ストリームが切れたとき */ @bindThis public dispose() { + if (this.fetchIntervalId) clearInterval(this.fetchIntervalId); for (const c of this.channels.filter(c => c.dispose)) { if (c.dispose) c.dispose(); } diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index f4eedc3964..ed73897e73 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -38,6 +38,11 @@ export interface InternalStreamTypes { antennaDeleted: Antenna; antennaUpdated: Antenna; metaUpdated: Meta; + followChannel: { userId: User['id']; channelId: Channel['id']; }; + unfollowChannel: { userId: User['id']; channelId: Channel['id']; }; + updateUserProfile: UserProfile; + mute: { muterId: User['id']; muteeId: User['id']; }; + unmute: { muterId: User['id']; muteeId: User['id']; }; } export interface BroadcastTypes { @@ -56,18 +61,6 @@ export interface BroadcastTypes { }; } -export interface UserStreamTypes { - terminate: Record; - followChannel: Channel; - unfollowChannel: Channel; - updateUserProfile: UserProfile; - mute: User; - unmute: User; - follow: Packed<'UserDetailedNotMe'>; - unfollow: Packed<'User'>; - userAdded: Packed<'User'>; -} - export interface MainStreamTypes { notification: Packed<'Notification'>; mention: Packed<'Note'>; @@ -200,10 +193,6 @@ export type StreamMessages = { name: 'broadcast'; payload: EventUnionFromDictionary>; }; - user: { - name: `user:${User['id']}`; - payload: EventUnionFromDictionary>; - }; main: { name: `mainStream:${User['id']}`; payload: EventUnionFromDictionary>; -- cgit v1.2.3-freya From 73203a3d72b355e3c230c46771292ff9520675c0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 6 Apr 2023 11:14:43 +0900 Subject: perf(backend): cache local custom emojis --- packages/backend/src/core/CustomEmojiService.ts | 187 +++++++++++++++++---- packages/backend/src/core/InstanceActorService.ts | 6 +- packages/backend/src/core/NoteCreateService.ts | 4 +- packages/backend/src/core/ReactionService.ts | 42 ++--- packages/backend/src/core/RelayService.ts | 6 +- packages/backend/src/core/RoleService.ts | 6 +- .../src/core/activitypub/ApRendererService.ts | 19 +-- .../backend/src/core/entities/NoteEntityService.ts | 26 ++- packages/backend/src/misc/cache.ts | 88 +++++++++- .../queue/processors/DeliverProcessorService.ts | 6 +- .../backend/src/server/NodeinfoServerService.ts | 4 +- .../api/endpoints/admin/emoji/add-aliases-bulk.ts | 34 +--- .../src/server/api/endpoints/admin/emoji/copy.ts | 2 - .../api/endpoints/admin/emoji/delete-bulk.ts | 35 +--- .../src/server/api/endpoints/admin/emoji/delete.ts | 36 +--- .../endpoints/admin/emoji/remove-aliases-bulk.ts | 34 +--- .../api/endpoints/admin/emoji/set-aliases-bulk.ts | 30 +--- .../api/endpoints/admin/emoji/set-category-bulk.ts | 30 +--- .../src/server/api/endpoints/admin/emoji/update.ts | 46 +---- .../backend/src/server/api/endpoints/emojis.ts | 4 - 20 files changed, 335 insertions(+), 310 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/admin') diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 1c3b60e5d7..604a94707f 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -1,24 +1,28 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In, IsNull } from 'typeorm'; +import Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; -import type { EmojisRepository, Note } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Config } from '@/config.js'; -import { ReactionService } from '@/core/ReactionService.js'; import { query } from '@/misc/prelude/url.js'; @Injectable() export class CustomEmojiService { private cache: MemoryKVCache; + public localEmojisCache: RedisSingleCache>; constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.config) private config: Config, @@ -32,9 +36,16 @@ export class CustomEmojiService { private idService: IdService, private emojiEntityService: EmojiEntityService, private globalEventService: GlobalEventService, - private reactionService: ReactionService, ) { this.cache = new MemoryKVCache(1000 * 60 * 60 * 12); + + this.localEmojisCache = new RedisSingleCache>(this.redisClient, 'localEmojis', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60 * 3, // 3m + fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), + toRedisConverter: (value) => JSON.stringify(value.values()), + fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換 + }); } @bindThis @@ -60,7 +71,7 @@ export class CustomEmojiService { }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { - await this.db.queryResultCache?.remove(['meta_emojis']); + this.localEmojisCache.refresh(); this.globalEventService.publishBroadcastStream('emojiAdded', { emoji: await this.emojiEntityService.packDetailed(emoji.id), @@ -70,6 +81,146 @@ export class CustomEmojiService { return emoji; } + @bindThis + public async update(id: Emoji['id'], data: { + name?: string; + category?: string | null; + aliases?: string[]; + license?: string | null; + }): Promise { + const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); + const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); + if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); + + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + name: data.name, + category: data.category, + aliases: data.aliases, + license: data.license, + }); + + this.localEmojisCache.refresh(); + + const updated = await this.emojiEntityService.packDetailed(emoji.id); + + if (emoji.name === data.name) { + this.globalEventService.publishBroadcastStream('emojiUpdated', { + emojis: [updated], + }); + } else { + this.globalEventService.publishBroadcastStream('emojiDeleted', { + emojis: [await this.emojiEntityService.packDetailed(emoji)], + }); + + this.globalEventService.publishBroadcastStream('emojiAdded', { + emoji: updated, + }); + } + } + + @bindThis + public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) { + const emojis = await this.emojisRepository.findBy({ + id: In(ids), + }); + + for (const emoji of emojis) { + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + aliases: [...new Set(emoji.aliases.concat(aliases))], + }); + } + + this.localEmojisCache.refresh(); + + this.globalEventService.publishBroadcastStream('emojiUpdated', { + emojis: await this.emojiEntityService.packDetailedMany(ids), + }); + } + + @bindThis + public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) { + await this.emojisRepository.update({ + id: In(ids), + }, { + updatedAt: new Date(), + aliases: aliases, + }); + + this.localEmojisCache.refresh(); + + this.globalEventService.publishBroadcastStream('emojiUpdated', { + emojis: await this.emojiEntityService.packDetailedMany(ids), + }); + } + + @bindThis + public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) { + const emojis = await this.emojisRepository.findBy({ + id: In(ids), + }); + + for (const emoji of emojis) { + await this.emojisRepository.update(emoji.id, { + updatedAt: new Date(), + aliases: emoji.aliases.filter(x => !aliases.includes(x)), + }); + } + + this.localEmojisCache.refresh(); + + this.globalEventService.publishBroadcastStream('emojiUpdated', { + emojis: await this.emojiEntityService.packDetailedMany(ids), + }); + } + + @bindThis + public async setCategoryBulk(ids: Emoji['id'][], category: string | null) { + await this.emojisRepository.update({ + id: In(ids), + }, { + updatedAt: new Date(), + category: category, + }); + + this.localEmojisCache.refresh(); + + this.globalEventService.publishBroadcastStream('emojiUpdated', { + emojis: await this.emojiEntityService.packDetailedMany(ids), + }); + } + + @bindThis + public async delete(id: Emoji['id']) { + const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); + + await this.emojisRepository.delete(emoji.id); + + this.localEmojisCache.refresh(); + + this.globalEventService.publishBroadcastStream('emojiDeleted', { + emojis: [await this.emojiEntityService.packDetailed(emoji)], + }); + } + + @bindThis + public async deleteBulk(ids: Emoji['id'][]) { + const emojis = await this.emojisRepository.findBy({ + id: In(ids), + }); + + for (const emoji of emojis) { + await this.emojisRepository.delete(emoji.id); + } + + this.localEmojisCache.refresh(); + + this.globalEventService.publishBroadcastStream('emojiDeleted', { + emojis: await this.emojiEntityService.packDetailedMany(emojis), + }); + } + @bindThis private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { // クエリに使うホスト @@ -84,7 +235,7 @@ export class CustomEmojiService { } @bindThis - private parseEmojiStr(emojiName: string, noteUserHost: string | null) { + public parseEmojiStr(emojiName: string, noteUserHost: string | null) { const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); if (!match) return { name: null, host: null }; @@ -143,30 +294,6 @@ export class CustomEmojiService { return res; } - @bindThis - public aggregateNoteEmojis(notes: Note[]) { - let emojis: { name: string | null; host: string | null; }[] = []; - for (const note of notes) { - emojis = emojis.concat(note.emojis - .map(e => this.parseEmojiStr(e, note.userHost))); - if (note.renote) { - emojis = emojis.concat(note.renote.emojis - .map(e => this.parseEmojiStr(e, note.renote!.userHost))); - if (note.renote.user) { - emojis = emojis.concat(note.renote.user.emojis - .map(e => this.parseEmojiStr(e, note.renote!.userHost))); - } - } - const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis; - emojis = emojis.concat(customReactions); - if (note.user) { - emojis = emojis.concat(note.user.emojis - .map(e => this.parseEmojiStr(e, note.userHost))); - } - } - return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[]; - } - /** * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します */ diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 898fb4ce85..4fb3fc5b4f 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import type { LocalUser } from '@/models/entities/User.js'; import type { UsersRepository } from '@/models/index.js'; -import { MemoryCache } from '@/misc/cache.js'; +import { MemorySingleCache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { bindThis } from '@/decorators.js'; @@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: MemoryCache; + private cache: MemorySingleCache; constructor( @Inject(DI.usersRepository) @@ -19,7 +19,7 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new MemoryCache(Infinity); + this.cache = new MemorySingleCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index fcc17ace1e..5c4d13f178 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js import { checkWordMute } from '@/misc/check-word-mute.js'; import type { Channel } from '@/models/entities/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemoryCache } from '@/misc/cache.js'; +import { MemorySingleCache } from '@/misc/cache.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -47,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; -const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 97a0b5ee66..a274b19e4b 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { RemoteUser, User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; @@ -20,6 +19,7 @@ import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; const FALLBACK = '❤'; @@ -60,9 +60,6 @@ export class ReactionService { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -74,6 +71,7 @@ export class ReactionService { private utilityService: UtilityService, private metaService: MetaService, + private customEmojiService: CustomEmojiService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, @@ -104,7 +102,6 @@ export class ReactionService { if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { reaction = '❤️'; } else { - // TODO: cache reaction = await this.toDbReaction(reaction, user.host); } @@ -158,21 +155,22 @@ export class ReactionService { // カスタム絵文字リアクションだったら絵文字情報も送る const decodedReaction = this.decodeReaction(reaction); - // TODO: Cache - const emoji = await this.emojisRepository.findOne({ - where: { - name: decodedReaction.name, - host: decodedReaction.host ?? IsNull(), - }, - select: ['name', 'host', 'originalUrl', 'publicUrl'], - }); + const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null + ? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name) + : await this.emojisRepository.findOne( + { + where: { + name: decodedReaction.name, + host: decodedReaction.host, + }, + }); this.globalEventService.publishNoteStream(note.id, 'reacted', { reaction: decodedReaction.reaction, - emoji: emoji != null ? { - name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, + emoji: customEmoji != null ? { + name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url: emoji.publicUrl || emoji.originalUrl, + url: customEmoji.publicUrl || customEmoji.originalUrl, } : null, userId: user.id, }); @@ -311,10 +309,12 @@ export class ReactionService { const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); if (custom) { const name = custom[1]; - const emoji = await this.emojisRepository.findOneBy({ - host: reacterHost ?? IsNull(), - name, - }); + const emoji = reacterHost == null + ? (await this.customEmojiService.localEmojisCache.fetch()).get(name) + : await this.emojisRepository.findOneBy({ + host: reacterHost, + name, + }); if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; } diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 4df7fb3bff..9d34d82be2 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -3,7 +3,7 @@ import { IsNull } from 'typeorm'; import type { LocalUser, User } from '@/models/entities/User.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { MemoryCache } from '@/misc/cache.js'; +import { MemorySingleCache } from '@/misc/cache.js'; import type { Relay } from '@/models/entities/Relay.js'; import { QueueService } from '@/core/QueueService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; @@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const; @Injectable() export class RelayService { - private relaysCache: MemoryCache; + private relaysCache: MemorySingleCache; constructor( @Inject(DI.usersRepository) @@ -30,7 +30,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new MemoryCache(1000 * 60 * 10); + this.relaysCache = new MemorySingleCache(1000 * 60 * 10); } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 52e6292a1e..54e098ea52 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { In } from 'typeorm'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; -import { MemoryKVCache, MemoryCache } from '@/misc/cache.js'; +import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -57,7 +57,7 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown { - private rolesCache: MemoryCache; + private rolesCache: MemorySingleCache; private roleAssignmentByUserIdCache: MemoryKVCache; public static AlreadyAssignedError = class extends Error {}; @@ -84,7 +84,7 @@ export class RoleService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.rolesCache = new MemoryCache(Infinity); + this.rolesCache = new MemorySingleCache(Infinity); this.roleAssignmentByUserIdCache = new MemoryKVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 15512c8f47..b250b796d6 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -21,6 +21,8 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @@ -50,6 +52,7 @@ export class ApRendererService { @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, + private customEmojiService: CustomEmojiService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private ldSignatureService: LdSignatureService, @@ -272,11 +275,7 @@ export class ApRendererService { if (reaction.startsWith(':')) { const name = reaction.replaceAll(':', ''); - // TODO: cache - const emoji = await this.emojisRepository.findOneBy({ - name, - host: IsNull(), - }); + const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name); if (emoji) object.tag = [this.renderEmoji(emoji)]; } @@ -701,13 +700,9 @@ export class ApRendererService { private async getEmojis(names: string[]): Promise { if (names == null || names.length === 0) return []; - const emojis = await Promise.all( - names.map(name => this.emojisRepository.findOneBy({ - name, - host: IsNull(), - })), - ); + const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); + const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); - return emojis.filter(emoji => emoji != null) as Emoji[]; + return emojis; } } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 5660600692..94b3029c58 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -406,7 +406,7 @@ export class NoteEntityService implements OnModuleInit { } } - await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); + await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds); @@ -420,6 +420,30 @@ export class NoteEntityService implements OnModuleInit { }))); } + @bindThis + public aggregateNoteEmojis(notes: Note[]) { + let emojis: { name: string | null; host: string | null; }[] = []; + for (const note of notes) { + emojis = emojis.concat(note.emojis + .map(e => this.customEmojiService.parseEmojiStr(e, note.userHost))); + if (note.renote) { + emojis = emojis.concat(note.renote.emojis + .map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost))); + if (note.renote.user) { + emojis = emojis.concat(note.renote.user.emojis + .map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost))); + } + } + const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis; + emojis = emojis.concat(customReactions); + if (note.user) { + emojis = emojis.concat(note.user.emojis + .map(e => this.customEmojiService.parseEmojiStr(e, note.userHost))); + } + } + return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[]; + } + @bindThis public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise { // 指定したユーザーの指定したノートのリノートがいくつあるか数える diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index ef6f610125..d35414acf7 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -85,6 +85,90 @@ export class RedisKVCache { } } +export class RedisSingleCache { + private redisClient: Redis.Redis; + private name: string; + private lifetime: number; + private memoryCache: MemorySingleCache; + private fetcher: () => Promise; + private toRedisConverter: (value: T) => string; + private fromRedisConverter: (value: string) => T; + + constructor(redisClient: RedisSingleCache['redisClient'], name: RedisSingleCache['name'], opts: { + lifetime: RedisSingleCache['lifetime']; + memoryCacheLifetime: number; + fetcher: RedisSingleCache['fetcher']; + toRedisConverter: RedisSingleCache['toRedisConverter']; + fromRedisConverter: RedisSingleCache['fromRedisConverter']; + }) { + this.redisClient = redisClient; + this.name = name; + this.lifetime = opts.lifetime; + this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime); + this.fetcher = opts.fetcher; + this.toRedisConverter = opts.toRedisConverter; + this.fromRedisConverter = opts.fromRedisConverter; + } + + @bindThis + public async set(value: T): Promise { + this.memoryCache.set(value); + if (this.lifetime === Infinity) { + await this.redisClient.set( + `singlecache:${this.name}`, + this.toRedisConverter(value), + ); + } else { + await this.redisClient.set( + `singlecache:${this.name}`, + this.toRedisConverter(value), + 'ex', Math.round(this.lifetime / 1000), + ); + } + } + + @bindThis + public async get(): Promise { + const memoryCached = this.memoryCache.get(); + if (memoryCached !== undefined) return memoryCached; + + const cached = await this.redisClient.get(`singlecache:${this.name}`); + if (cached == null) return undefined; + return this.fromRedisConverter(cached); + } + + @bindThis + public async delete(): Promise { + this.memoryCache.delete(); + await this.redisClient.del(`singlecache:${this.name}`); + } + + /** + * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します + */ + @bindThis + public async fetch(): Promise { + const cachedValue = await this.get(); + if (cachedValue !== undefined) { + // Cache HIT + return cachedValue; + } + + // Cache MISS + const value = await this.fetcher(); + this.set(value); + return value; + } + + @bindThis + public async refresh() { + const value = await this.fetcher(); + this.set(value); + + // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする + } +} + // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? export class MemoryKVCache { @@ -173,12 +257,12 @@ export class MemoryKVCache { } } -export class MemoryCache { +export class MemorySingleCache { private cachedAt: number | null = null; private value: T | undefined; private lifetime: number; - constructor(lifetime: MemoryCache['lifetime']) { + constructor(lifetime: MemorySingleCache['lifetime']) { this.lifetime = lifetime; } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index a9af22ad09..0e99b7bcd2 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { MemoryCache } from '@/misc/cache.js'; +import { MemorySingleCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; @@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js'; @Injectable() export class DeliverProcessorService { private logger: Logger; - private suspendedHostsCache: MemoryCache; + private suspendedHostsCache: MemorySingleCache; private latest: string | null; constructor( @@ -46,7 +46,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new MemoryCache(1000 * 60 * 60); + this.suspendedHostsCache = new MemorySingleCache(1000 * 60 * 60); } @bindThis diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 66c1faaac2..666a91fcee 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { MemoryCache } from '@/misc/cache.js'; +import { MemorySingleCache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -118,7 +118,7 @@ export class NodeinfoServerService { }; }; - const cache = new MemoryCache>>(1000 * 60 * 10); + const cache = new MemorySingleCache>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { const base = await cache.fetch(() => nodeinfo2()); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 4e4f845b0b..6e604ed885 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -1,10 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], @@ -26,38 +22,14 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - const emojis = await this.emojisRepository.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await this.emojisRepository.update(emoji.id, { - updatedAt: new Date(), - aliases: [...new Set(emoji.aliases.concat(ps.aliases))], - }); - } - - await this.db.queryResultCache?.remove(['meta_emojis']); - - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packDetailedMany(ps.ids), - }); + await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index fea11a67d6..82dca9cc70 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -90,8 +90,6 @@ export default class extends Endpoint { license: emoji.license, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); - await this.db.queryResultCache?.remove(['meta_emojis']); - this.globalEventService.publishBroadcastStream('emojiAdded', { emoji: await this.emojiEntityService.packDetailed(copied.id), }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index 84aad020af..d5acee36a8 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -1,11 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService'; export const meta = { tags: ['admin'], @@ -24,38 +19,14 @@ export const paramDef = { required: ['ids'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private moderationLogService: ModerationLogService, - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - const emojis = await this.emojisRepository.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await this.emojisRepository.delete(emoji.id); - await this.db.queryResultCache?.remove(['meta_emojis']); - this.moderationLogService.insertModerationLog(me, 'deleteEmoji', { - emoji: emoji, - }); - } - - this.globalEventService.publishBroadcastStream('emojiDeleted', { - emojis: await this.emojiEntityService.packDetailedMany(emojis), - }); + await this.customEmojiService.deleteBulk(ps.ids); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 90a5856a1b..429c819fe0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -1,12 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { ApiError } from '../../../error.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], @@ -31,38 +25,14 @@ export const paramDef = { required: ['id'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private moderationLogService: ModerationLogService, - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - - await this.emojisRepository.delete(emoji.id); - - await this.db.queryResultCache?.remove(['meta_emojis']); - - this.globalEventService.publishBroadcastStream('emojiDeleted', { - emojis: [await this.emojiEntityService.packDetailed(emoji)], - }); - - this.moderationLogService.insertModerationLog(me, 'deleteEmoji', { - emoji: emoji, - }); + await this.customEmojiService.delete(ps.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 3935183502..83f882cac5 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -1,10 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], @@ -26,38 +22,14 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - const emojis = await this.emojisRepository.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await this.emojisRepository.update(emoji.id, { - updatedAt: new Date(), - aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)), - }); - } - - await this.db.queryResultCache?.remove(['meta_emojis']); - - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packDetailedMany(ps.ids), - }); + await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 6a875f9c83..1d3a432bb7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -1,10 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], @@ -26,34 +22,14 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.emojisRepository.update({ - id: In(ps.ids), - }, { - updatedAt: new Date(), - aliases: ps.aliases, - }); - - await this.db.queryResultCache?.remove(['meta_emojis']); - - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packDetailedMany(ps.ids), - }); + await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index d3b999c0ed..453968c7a9 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -1,10 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], @@ -28,34 +24,14 @@ export const paramDef = { required: ['ids'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - await this.emojisRepository.update({ - id: In(ps.ids), - }, { - updatedAt: new Date(), - category: ps.category, - }); - - await this.db.queryResultCache?.remove(['meta_emojis']); - - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packDetailedMany(ps.ids), - }); + await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index bc0475e05c..f63348b60b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,10 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -45,51 +41,19 @@ export const paramDef = { required: ['id', 'name', 'aliases'], } as const; -// TODO: ロジックをサービスに切り出す - // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - - private emojiEntityService: EmojiEntityService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { - const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - const sameNameEmoji = await this.emojisRepository.findOneBy({ name: ps.name, host: IsNull() }); - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - if (sameNameEmoji != null && sameNameEmoji.id !== ps.id) throw new ApiError(meta.errors.sameNameEmojiExists); - await this.emojisRepository.update(emoji.id, { - updatedAt: new Date(), + await this.customEmojiService.update(ps.id, { name: ps.name, - category: ps.category, + category: ps.category ?? null, aliases: ps.aliases, - license: ps.license, + license: ps.license ?? null, }); - - await this.db.queryResultCache?.remove(['meta_emojis']); - - const updated = await this.emojiEntityService.packDetailed(emoji.id); - - if (emoji.name === ps.name) { - this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: [updated], - }); - } else { - this.globalEventService.publishBroadcastStream('emojiDeleted', { - emojis: [await this.emojiEntityService.packDetailed(emoji)], - }); - - this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: updated, - }); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 0711fe4a57..13cc709d31 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -58,10 +58,6 @@ export default class extends Endpoint { category: 'ASC', name: 'ASC', }, - cache: { - id: 'meta_emojis', - milliseconds: 3600000, // 1 hour - }, }); return { -- cgit v1.2.3-freya From a574d0fbcb1575ac7a3fe161ab91e117071f3648 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 6 Apr 2023 11:36:20 +0900 Subject: fix --- packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/server/api/endpoints/admin') diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index d5acee36a8..9f8263629b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; export const meta = { tags: ['admin'], -- cgit v1.2.3-freya