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 ? {