From b9db59ce7f6bceb338a6262b9e654de1924f6fae Mon Sep 17 00:00:00 2001 From: fly_mc <me@flymc.cc> Date: Sun, 15 Sep 2024 21:42:54 +0800 Subject: [PATCH] remote avatar deco --- .../1696388600237-revert-note-edit.js | 16 ------ .../migration/1706162844037-edit-note.js | 13 ----- packages/backend/src/config.ts | 3 ++ packages/backend/src/core/CacheService.ts | 54 +++++++++++++++++++ .../src/core/entities/UserEntityService.ts | 45 ++++++++++++---- 5 files changed, 93 insertions(+), 38 deletions(-) delete mode 100644 packages/backend/migration/1696388600237-revert-note-edit.js delete mode 100644 packages/backend/migration/1706162844037-edit-note.js diff --git a/packages/backend/migration/1696388600237-revert-note-edit.js b/packages/backend/migration/1696388600237-revert-note-edit.js deleted file mode 100644 index d2f01a0d92..0000000000 --- a/packages/backend/migration/1696388600237-revert-note-edit.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RevertNoteEdit1696388600237 { - name = 'RevertNoteEdit1696388600237' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); - } -} \ No newline at end of file diff --git a/packages/backend/migration/1706162844037-edit-note.js b/packages/backend/migration/1706162844037-edit-note.js deleted file mode 100644 index fb8ebff669..0000000000 --- a/packages/backend/migration/1706162844037-edit-note.js +++ /dev/null @@ -1,13 +0,0 @@ -export class EditNote1706162844037 { - name = 'EditNote1706162844037' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`ALTER TABLE "note" ADD "history" jsonb`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "history"`); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); - } -} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 3e5a1e81cd..c70046d26d 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -96,6 +96,7 @@ type Source = { perUserNotificationsMaxCount?: number; deactivateAntennaThreshold?: number; pidFile: string; + avatarDecorationAllowedHosts: string[] | undefined; }; export type Config = { @@ -175,6 +176,7 @@ export type Config = { perUserNotificationsMaxCount: number; deactivateAntennaThreshold: number; pidFile: string; + avatarDecorationAllowedHosts: string[] | undefined; }; const _filename = fileURLToPath(import.meta.url); @@ -276,6 +278,7 @@ export function loadConfig(): Config { perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), pidFile: config.pidFile, + avatarDecorationAllowedHosts: config.avatarDecorationAllowedHosts, }; } diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 6725ebe75b..1d4b2eed7c 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -12,8 +12,19 @@ import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { Config } from '@/config.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +type PariRemoteUserDecorationsCacheType = { + id: string; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; + url?: string; +}[]; + @Injectable() export class CacheService implements OnApplicationShutdown { public userByIdCache: MemoryKVCache<MiUser>; @@ -26,6 +37,7 @@ export class CacheService implements OnApplicationShutdown { public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ public renoteMutingsCache: RedisKVCache<Set<string>>; public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>; + public pariRemoteUserDecorationsCache: RedisKVCache<PariRemoteUserDecorationsCacheType>; constructor( @Inject(DI.redis) @@ -52,7 +64,12 @@ export class CacheService implements OnApplicationShutdown { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.config) + private config: Config, + private userEntityService: UserEntityService, + + private httpRequestService: HttpRequestService, ) { //this.onMessage = this.onMessage.bind(this); @@ -117,6 +134,43 @@ export class CacheService implements OnApplicationShutdown { // NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている + this.pariRemoteUserDecorationsCache = new RedisKVCache<PariRemoteUserDecorationsCacheType>(this.redisClient, 'pariRemoteUserDecorationsCache', { + lifetime: 1000 * 60 * 30, // 30m + memoryCacheLifetime: 1000 * 60, // 1m + fetcher: (key) => this.userByIdCache.fetch(key, () => this.usersRepository.findOneBy({ + id: key, + }) as Promise<MiLocalUser>).then(user => { + if (user.host == null) return []; + if (!(this.config.avatarDecorationAllowedHosts?.includes(user.host))) return []; + return this.httpRequestService.send(`https://${user.host}/api/users/show`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + host: null, + username: user.username, + }), + }).then(res => res.json() as { avatarDecorations?: PariRemoteUserDecorationsCacheType }) + .then((res) => + res.avatarDecorations?.filter(ad => ad.url).map((ad) => ({ + id: `${ad.id}:${user.host}`, + angle: ad.angle ? Number(ad.angle) : undefined, + offsetX: ad.offsetX ? Number(ad.offsetX) : undefined, + offsetY: ad.offsetY ? Number(ad.offsetY) : undefined, + flipH: ad.flipH ? Boolean(ad.flipH) : undefined, + url: ad.url, + })) ?? [], + ) + .catch((err) => { + console.error(err); + return []; + }); + }), + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), + }); + this.redisForSub.on('message', this.onMessage); } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 9bf568bc90..f6d214cc29 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -47,6 +47,7 @@ import { IdService } from '@/core/IdService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { CacheService } from '@/core/CacheService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -91,6 +92,7 @@ export class UserEntityService implements OnModuleInit { private federatedInstanceService: FederatedInstanceService; private idService: IdService; private avatarDecorationService: AvatarDecorationService; + private cacheService: CacheService; constructor( private moduleRef: ModuleRef, @@ -146,6 +148,7 @@ export class UserEntityService implements OnModuleInit { this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.idService = this.moduleRef.get('IdService'); this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService'); + this.cacheService = this.moduleRef.get('CacheService'); } //#region Validators @@ -473,6 +476,29 @@ export class UserEntityService implements OnModuleInit { const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null; + const getLocalUserDecorations = () => + user.avatarDecorations.length > 0 + ? this.avatarDecorationService.getAll().then( + decorations => user.avatarDecorations.filter( + ud => decorations.some(d => d.id === ud.id)) + .map(ud => ({ + id: ud.id, + angle: ud.angle || undefined, + flipH: ud.flipH || undefined, + offsetX: ud.offsetX || undefined, + offsetY: ud.offsetY || undefined, + url: decorations.find(d => d.id === ud.id)!.url, + }))) + : []; + const avatarDecorations = user.host == null + ? getLocalUserDecorations() + : this.cacheService.stpvRemoteUserDecorationsCache.fetch(user.id).then(res => res.map(ad => ({ + ...ad, + url: ad.url && this.config.proxyRemoteFiles + ? `${this.config.mediaProxy}/static.webp?url=${(encodeURIComponent(ad.url))}` + : ad.url, + }))); + const packed = { id: user.id, name: user.name, @@ -480,14 +506,15 @@ export class UserEntityService implements OnModuleInit { host: user.host, avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user), avatarBlurhash: user.avatarBlurhash, - avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({ - id: ud.id, - angle: ud.angle || undefined, - flipH: ud.flipH || undefined, - offsetX: ud.offsetX || undefined, - offsetY: ud.offsetY || undefined, - url: decorations.find(d => d.id === ud.id)!.url, - }))) : [], + //avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({ + // id: ud.id, + // angle: ud.angle || undefined, + // flipH: ud.flipH || undefined, + // offsetX: ud.offsetX || undefined, + // offsetY: ud.offsetY || undefined, + // url: decorations.find(d => d.id === ud.id)!.url, + //}))) : [], + avatarDecorations, isBot: user.isBot, isCat: user.isCat, instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { @@ -508,7 +535,7 @@ export class UserEntityService implements OnModuleInit { name: r.name, iconUrl: r.iconUrl, displayOrder: r.displayOrder, - })) + })), ) : undefined, ...(isDetailed ? {