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
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:
parent
03ed6aaa90
commit
7bb40d290a
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 ? {
|
||||
|
Loading…
Reference in New Issue
Block a user