From 93eccf033764dd32705b4c4137388d313ea0ae8d Mon Sep 17 00:00:00 2001 From: Crownel Date: Sat, 31 Aug 2024 19:44:05 +0900 Subject: [PATCH] 2024.8.0+monster1 (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 폰트 변경(suite, zen maru gothic) * Relay GTL patch * 버전명명(2024.8.0+monster) * Support Remote Avatar Decoration view + fix 7891331 + fix !avatarDecorations * Remote Avatar 데코레이션용 캐시 * Avatar decoration 연합에 offsetX, offsetY 추가 * 리모트 유저의 여러 아바타 장식 연합 지원 * 버전명명(2024.8.0+monster1) --------- Co-authored-by: Yunochi Co-authored-by: caipira113 --- package.json | 2 +- .../1699432324194-remoteAvaterDecoration.js | 13 ++ .../src/core/AvatarDecorationService.ts | 120 +++++++++++++++++- .../activitypub/models/ApPersonService.ts | 7 + .../src/core/entities/UserEntityService.ts | 2 +- .../backend/src/models/AvatarDecoration.ts | 10 ++ 6 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 packages/backend/migration/1699432324194-remoteAvaterDecoration.js diff --git a/package.json b/package.json index 74688c66bd4a..e6ab2ae5e320 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.8.0+monster", + "version": "2024.8.0+monster1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1699432324194-remoteAvaterDecoration.js b/packages/backend/migration/1699432324194-remoteAvaterDecoration.js new file mode 100644 index 000000000000..5b2762b47642 --- /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 4efd6122b18b..271296e9ff5c 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,23 +13,39 @@ 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; + public cacheWithRemote: 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); // 30s + this.cacheWithRemote = new MemorySingleCache(1000 * 60 * 30); this.redisForSub.on('message', this.onMessage); } @@ -94,6 +110,99 @@ 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 userAvatarDecorations = userData.avatarDecorations ?? undefined; + + if (!userAvatarDecorations || userAvatarDecorations.length === 0) { + const updates = {} as Partial; + updates.avatarDecorations = []; + await this.usersRepository.update({id: user.id}, updates); + return; + } + + 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(); + const updates = {} as Partial; + updates.avatarDecorations = []; + for (const avatarDecoration of userAvatarDecorations) { + let name; + let description; + const avatarDecorationId = avatarDecoration.id + 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(avatarDecoration.url, 'static'), + remoteId: avatarDecorationId, + host: userHost, + }; + if (existingDecoration == null) { + await this.create(decorationData); + this.cacheWithRemote.delete(); + } else { + await this.update(existingDecoration.id, decorationData); + this.cacheWithRemote.delete(); + } + const findDecoration = await this.avatarDecorationsRepository.findOneBy({ + host: userHost, + remoteId: avatarDecorationId + }); + + updates.avatarDecorations.push({ + id: findDecoration?.id ?? '', + angle: avatarDecoration.angle ?? 0, + flipH: avatarDecoration.flipH ?? false, + offsetX: avatarDecoration.offsetX ?? 0, + offsetY: avatarDecoration.offsetY ?? 0, + }); + } + 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 +219,16 @@ 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(); + this.cacheWithRemote.delete(); + } + if (!withRemote) { + return this.cache.fetch(() => this.avatarDecorationsRepository.find({ where: { host: IsNull() } })); + } else { + return this.cacheWithRemote.fetch(() => this.avatarDecorationsRepository.find()); } - 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 f3ddf3952c84..b61de86f1893 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -49,6 +49,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, ICollection, IObject, IOrderedCollection } from '../type.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; const nameLength = 128; const summaryLength = 2048; @@ -103,6 +104,8 @@ export class ApPersonService implements OnModuleInit { private followingsRepository: FollowingsRepository, private roleService: RoleService, + + private avatarDecorationService: AvatarDecorationService, ) { } @@ -420,6 +423,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); @@ -545,6 +550,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 9bf568bc9088..2d244b7f7749 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -480,7 +480,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 13f0b0566740..de9ef0a99ae2 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; }