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;
|
perUserNotificationsMaxCount?: number;
|
||||||
deactivateAntennaThreshold?: number;
|
deactivateAntennaThreshold?: number;
|
||||||
pidFile: string;
|
pidFile: string;
|
||||||
|
|
||||||
|
avatarDecorationAllowedHosts: string[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
@ -182,6 +184,7 @@ export type Config = {
|
|||||||
perUserNotificationsMaxCount: number;
|
perUserNotificationsMaxCount: number;
|
||||||
deactivateAntennaThreshold: number;
|
deactivateAntennaThreshold: number;
|
||||||
pidFile: string;
|
pidFile: string;
|
||||||
|
avatarDecorationAllowedHosts: string[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
@ -293,6 +296,7 @@ export function loadConfig(): Config {
|
|||||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||||
pidFile: config.pidFile,
|
pidFile: config.pidFile,
|
||||||
|
avatarDecorationAllowedHosts: config.avatarDecorationAllowedHosts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,19 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.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';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
type StpvRemoteUserDecorationsCacheType = {
|
||||||
|
id: string;
|
||||||
|
angle?: number;
|
||||||
|
flipH?: boolean;
|
||||||
|
offsetX?: number;
|
||||||
|
offsetY?: number;
|
||||||
|
url?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CacheService implements OnApplicationShutdown {
|
export class CacheService implements OnApplicationShutdown {
|
||||||
public userByIdCache: MemoryKVCache<MiUser>;
|
public userByIdCache: MemoryKVCache<MiUser>;
|
||||||
@ -26,6 +37,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||||
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||||
|
public stpvRemoteUserDecorationsCache: RedisKVCache<StpvRemoteUserDecorationsCacheType>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
@ -52,7 +64,12 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
|
||||||
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
@ -115,6 +132,43 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
fromRedisConverter: (value) => JSON.parse(value),
|
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で行っている
|
// NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている
|
||||||
|
|
||||||
this.redisForSub.on('message', this.onMessage);
|
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 { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
@ -91,6 +92,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
private federatedInstanceService: FederatedInstanceService;
|
private federatedInstanceService: FederatedInstanceService;
|
||||||
private idService: IdService;
|
private idService: IdService;
|
||||||
private avatarDecorationService: AvatarDecorationService;
|
private avatarDecorationService: AvatarDecorationService;
|
||||||
|
private cacheService: CacheService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
@ -146,6 +148,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||||
this.idService = this.moduleRef.get('IdService');
|
this.idService = this.moduleRef.get('IdService');
|
||||||
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
||||||
|
this.cacheService = this.moduleRef.get('CacheService');
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Validators
|
//#region Validators
|
||||||
@ -473,6 +476,24 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
|
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 = {
|
const packed = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
@ -480,14 +501,8 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
host: user.host,
|
host: user.host,
|
||||||
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
|
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
|
||||||
avatarBlurhash: user.avatarBlurhash,
|
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 => ({
|
createdAt: this.idService.parse(user.id).date.toISOString(),
|
||||||
id: ud.id,
|
avatarDecorations,
|
||||||
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,
|
|
||||||
}))) : [],
|
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||||
|
Loading…
Reference in New Issue
Block a user