feat: federate avatar decorations
Some checks failed
Check copyright year / check_copyright_year (push) Successful in 15s
Lint / pnpm_install (push) Successful in 2m46s
Lint / locale_verify (push) Successful in 2m30s
Check Misskey JS version / Check version (push) Failing after 19s
Check SPDX-License-Identifier / check-spdx-license-id (push) Successful in 21s
Dockle / dockle (push) Successful in 1m7s
Storybook / build (push) Has been skipped
Test (backend) / unit (20.16.0) (push) Failing after 9m8s
Test (backend) / e2e (20.16.0) (push) Failing after 3m36s
Test (federation) / test (20.16.0) (push) Failing after 4m31s
Test (frontend) / vitest (20.16.0) (push) Failing after 3m32s
Test (frontend) / e2e (chrome, 20.16.0) (push) Failing after 3m28s
Test (production install and build) / production (20.16.0) (push) Successful in 2m15s
Test (backend) / validate-api-json (20.16.0) (push) Successful in 4m17s
Lint / lint (backend) (push) Successful in 4m45s
Lint / lint (frontend) (push) Failing after 14m54s
Lint / lint (frontend-embed) (push) Successful in 4m47s
Lint / lint (frontend-shared) (push) Successful in 5m43s
Lint / lint (misskey-bubble-game) (push) Successful in 6m10s
Lint / lint (misskey-js) (push) Successful in 4m42s
Lint / lint (misskey-reversi) (push) Successful in 5m11s
Lint / lint (sw) (push) Successful in 4m31s
Lint / typecheck (backend) (push) Successful in 5m12s
Lint / typecheck (misskey-js) (push) Successful in 3m43s
Lint / typecheck (sw) (push) Successful in 3m30s
Publish Docker image (develop) / Build (linux/amd64) (push) Failing after 8m30s
Publish Docker image (develop) / merge (push) Has been skipped

This commit is contained in:
Lhc_fl 2024-09-15 20:44:04 +08:00 committed by laoXong
parent 03ed6aaa90
commit 7bb40d290a
3 changed files with 81 additions and 8 deletions

View File

@ -99,6 +99,8 @@ type Source = {
perUserNotificationsMaxCount?: number;
deactivateAntennaThreshold?: number;
pidFile: string;
avatarDecorationAllowedHosts: string[] | undefined;
};
export type Config = {
@ -182,6 +184,7 @@ export type Config = {
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
pidFile: string;
avatarDecorationAllowedHosts: string[] | undefined;
};
const _filename = fileURLToPath(import.meta.url);
@ -293,6 +296,7 @@ export function loadConfig(): Config {
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
pidFile: config.pidFile,
avatarDecorationAllowedHosts: config.avatarDecorationAllowedHosts,
};
}

View File

@ -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 StpvRemoteUserDecorationsCacheType = {
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 stpvRemoteUserDecorationsCache: RedisKVCache<StpvRemoteUserDecorationsCacheType>;
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);
@ -115,6 +132,43 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => JSON.parse(value),
});
this.stpvRemoteUserDecorationsCache = new RedisKVCache<StpvRemoteUserDecorationsCacheType>(this.redisClient, 'stpvRemoteUserDecorationsCache', {
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 (!(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?: StpvRemoteUserDecorationsCacheType })
.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),
});
// NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている
this.redisForSub.on('message', this.onMessage);

View File

@ -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,24 @@ 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);
const packed = {
id: user.id,
name: user.name,
@ -480,14 +501,8 @@ 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,
}))) : [],
createdAt: this.idService.parse(user.id).date.toISOString(),
avatarDecorations,
isBot: user.isBot,
isCat: user.isCat,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {