diff --git a/packages/backend/migration/1699432324194-remoteAvaterDecoration.js b/packages/backend/migration/1699432324194-remoteAvaterDecoration.js new file mode 100644 index 0000000000..5b2762b476 --- /dev/null +++ b/packages/backend/migration/1699432324194-remoteAvaterDecoration.js @@ -0,0 +1,13 @@ +export class RemoteAvaterDecoration1699432324194 { + name = 'RemoteAvaterDecoration1699432324194' + + async up(queryRunner) { + queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "remoteId" varchar(32)`); + queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "host" varchar(128)`); + } + + async down(queryRunner) { + queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "host"`); + queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "remoteId"`); + } +} diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts index 632449bcb8..33398b64a0 100644 --- a/packages/backend/src/core/AvatarDecorationService.ts +++ b/packages/backend/src/core/AvatarDecorationService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js'; +import type { AvatarDecorationsRepository, InstancesRepository, UsersRepository, MiAvatarDecoration, MiUser } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -13,21 +13,35 @@ import { bindThis } from '@/decorators.js'; import { MemorySingleCache } from '@/misc/cache.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { HttpRequestService } from "@/core/HttpRequestService.js"; +import { appendQuery, query } from '@/misc/prelude/url.js'; +import type { Config } from '@/config.js'; +import {IsNull} from "typeorm"; @Injectable() export class AvatarDecorationService implements OnApplicationShutdown { public cache: MemorySingleCache; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.avatarDecorationsRepository) private avatarDecorationsRepository: AvatarDecorationsRepository, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private idService: IdService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, + private httpRequestService: HttpRequestService, ) { this.cache = new MemorySingleCache(1000 * 60 * 30); @@ -94,6 +108,87 @@ export class AvatarDecorationService implements OnApplicationShutdown { } } + @bindThis + private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string { + return appendQuery( + `${this.config.mediaProxy}/${mode ?? 'image'}.webp`, + query({ + url, + ...(mode ? { [mode]: '1' } : {}), + }), + ); + } + + @bindThis + public async remoteUserUpdate(user: MiUser) { + const userHost = user.host ?? ''; + const instance = await this.instancesRepository.findOneBy({ host: userHost }); + const userHostUrl = `https://${user.host}`; + const showUserApiUrl = `${userHostUrl}/api/users/show`; + + if (instance?.softwareName !== 'misskey' && instance?.softwareName !== 'cherrypick') { + return; + } + + const res = await this.httpRequestService.send(showUserApiUrl, { + method: 'POST', + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ "username": user.username }), + }); + + const userData: any = await res.json(); + const avatarDecorations = userData.avatarDecorations[0]; + + if (avatarDecorations != null) { + const avatarDecorationId = avatarDecorations.id; + const instanceHost = instance?.host; + const decorationApiUrl = `https://${instanceHost}/api/get-avatar-decorations`; + const allRes = await this.httpRequestService.send(decorationApiUrl, { + method: 'POST', + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + + const allDecorations: any = await allRes.json(); + let name; + let description; + + for (const decoration of allDecorations) { + if (decoration.id == avatarDecorationId) { + name = decoration.name; + description = decoration.description; + break; + } + } + + const existingDecoration = await this.avatarDecorationsRepository.findOneBy({ host: userHost, remoteId: avatarDecorationId }); + + const decorationData = { + name: name, + description: description, + url: this.getProxiedUrl(avatarDecorations.url, 'static'), + remoteId: avatarDecorationId, + host: userHost, + }; + + if (existingDecoration == null) { + await this.create(decorationData); + } else { + await this.update(existingDecoration.id, decorationData); + } + + const findDecoration = await this.avatarDecorationsRepository.findOneBy({ host: userHost, remoteId: avatarDecorationId }); + const updates = {} as Partial; + updates.avatarDecorations = [{ + id: findDecoration?.id ?? '', + angle: avatarDecorations.angle ?? 0, + flipH: avatarDecorations.flipH ?? false, + }]; + + await this.usersRepository.update({ id: user.id }, updates); + } + } + @bindThis public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise { const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id }); @@ -110,11 +205,15 @@ export class AvatarDecorationService implements OnApplicationShutdown { } @bindThis - public async getAll(noCache = false): Promise { + public async getAll(noCache = false, withRemote = false): Promise { if (noCache) { this.cache.delete(); } - return this.cache.fetch(() => this.avatarDecorationsRepository.find()); + if (!withRemote) { + return this.cache.fetch(() => this.avatarDecorationsRepository.find({ where: { host: IsNull() } })); + } else { + return this.cache.fetch(() => this.avatarDecorationsRepository.find()); + } } @bindThis diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index e95637996a..f7739db933 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -48,6 +48,7 @@ import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; import type { IActor, IObject } from '../type.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; const nameLength = 128; const summaryLength = 2048; @@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + + private avatarDecorationService: AvatarDecorationService, ) { } @@ -462,6 +465,8 @@ export class ApPersonService implements OnModuleInit { // ハッシュタグ更新 this.hashtagService.updateUsertags(user, tags); + this.avatarDecorationService.remoteUserUpdate(user); + //#region アバターとヘッダー画像をフェッチ try { const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image); @@ -639,6 +644,8 @@ export class ApPersonService implements OnModuleInit { if (moving) updates.movedAt = new Date(); // Update user + const user = await this.usersRepository.findOneByOrFail({ id: exist.id }); + await this.avatarDecorationService.remoteUserUpdate(user); await this.usersRepository.update(exist.id, updates); if (person.publicKey) { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 1e6d628f2c..72e4caf7ab 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -394,7 +394,7 @@ 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 => ({ + avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll(false, true).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, diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index a2d17babf4..327857115a 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -36,4 +36,14 @@ export class MiAvatarDecoration { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisDecoration: string[]; + + @Column('varchar', { + length: 32, + }) + public remoteId: string; + + @Column('varchar', { + length: 128, nullable: true + }) + public host: string | null; }