From bfd817ae1063a2853b98c06744f17f9aad473c20 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Fri, 27 Oct 2023 04:26:10 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=E3=82=A2=E3=83=90=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=87=E3=82=B3=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E9=80=A3=E5=90=88=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: mattyatea --- .../1698323592283-avatardecoration3.js | 13 +++++ .../src/core/activitypub/ApRendererService.ts | 35 ++++++++++++- .../core/activitypub/models/ApNoteService.ts | 52 +++++++++++++++++-- .../activitypub/models/ApPersonService.ts | 28 +++++++++- packages/backend/src/core/activitypub/type.ts | 12 +++++ .../backend/src/models/AvatarDecoration.ts | 4 ++ 6 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 packages/backend/migration/1698323592283-avatardecoration3.js diff --git a/packages/backend/migration/1698323592283-avatardecoration3.js b/packages/backend/migration/1698323592283-avatardecoration3.js new file mode 100644 index 0000000000..00f8f7a6a8 --- /dev/null +++ b/packages/backend/migration/1698323592283-avatardecoration3.js @@ -0,0 +1,13 @@ +export class Avatardecoration31698323592283 { + name = 'Avatardecoration31698323592283' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "host" character varying(128)`); + await queryRunner.query(`CREATE INDEX "IDX_3f8079d448095b8d867d318d12" ON "avatar_decoration" ("host") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_3f8079d448095b8d867d318d12"`); + await queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "host"`); + } +} diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index e29bc1d096..f54cd83a51 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -26,11 +26,14 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; +import { MiAvatarDecoration } from '@/models/_.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; +import type { IAccept, IActivity, IAdd, IAnnounce, + IApAvatarDecoration, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() export class ApRendererService { @@ -54,6 +57,7 @@ export class ApRendererService { private pollsRepository: PollsRepository, private customEmojiService: CustomEmojiService, + private avatarDecorationService: AvatarDecorationService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private ldSignatureService: LdSignatureService, @@ -184,7 +188,19 @@ export class ApRendererService { }, }; } - + @bindThis + public renderAvatarDecoration(avatarDecoration: MiAvatarDecoration): IApAvatarDecoration { + return { + id: avatarDecoration.url, + type: 'AvatarDecoration', + name: avatarDecoration.name, + updated: avatarDecoration.updatedAt != null ? avatarDecoration.updatedAt.toISOString() : new Date().toISOString(), + icon: { + type: 'Image', + url: avatarDecoration.url, + }, + }; + } // to anonymise reporters, the reporting actor must be a system user @bindThis public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag { @@ -472,11 +488,17 @@ export class ApRendererService { const emojis = await this.getEmojis(user.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); + const AvatarDecorations = await this.getAvatarDecorations(user.avatarDecorations); + const apAvatarDecorations = AvatarDecorations.map(decoration => this.renderAvatarDecoration(decoration)); + const hashtagTags = user.tags.map(tag => this.renderHashtag(tag)); + AvatarDecorations.forEach((decoration, index) => user.avatarDecorations[index].id = decoration.name ); //デコレーションのidのところにnameを突っ込んでる(これ以外思いつかなかった) + const tag = [ ...apemojis, ...hashtagTags, + ...apAvatarDecorations, ]; const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -503,6 +525,7 @@ export class ApRendererService { publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, attachment: attachment.length ? attachment : undefined, + AvatarDecorations: user.avatarDecorations, }; if (user.movedToUri) { @@ -720,4 +743,12 @@ export class ApRendererService { return emojis; } + @bindThis + private async getAvatarDecorations(decorations: { id: string; angle: number; flipH: boolean }[]): Promise { + if (decorations.length === 0) return []; + const allAvatarDecorations = await this.avatarDecorationService.getAll(); + //const matchingDecorations = allAvatarDecorations.find(item1 => decorations.some(item2 => item1.id === item2.id )).map; + const avatarDecorations = allAvatarDecorations.filter(item1 => decorations.some(item2 => item1.id === item2.id)); + return avatarDecorations ?? []; + } } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 1979cdda9c..995c31b205 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -7,12 +7,13 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository, AvatarDecorationsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { MiEmoji } from '@/models/Emoji.js'; +import type { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; import { MetaService } from '@/core/MetaService.js'; import { AppLockService } from '@/core/AppLockService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; @@ -24,7 +25,7 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; -import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; +import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType, isAvatarDecoration } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; @@ -37,7 +38,6 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; - @Injectable() export class ApNoteService { private logger: Logger; @@ -52,6 +52,9 @@ export class ApNoteService { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.avatarDecorationsRepository) + private avatarDecorationRepository: AvatarDecorationsRepository, + private idService: IdService, private apMfmService: ApMfmService, private apResolverService: ApResolverService, @@ -397,4 +400,47 @@ export class ApNoteService { }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); })); } + @bindThis + public async extractAvatarDecorations(AvatarDecorations: IObject | IObject[], host: string): Promise { + // eslint-disable-next-line no-param-reassign + host = this.utilityService.toPuny(host); + + const AvatarDecorationTags = toArray(AvatarDecorations).filter(isAvatarDecoration); + const existingAvatars = await this.avatarDecorationRepository.findBy({ + host, + name: In(AvatarDecorationTags.map((avatar) => avatar.name)), + }); + return await Promise.all(AvatarDecorationTags.map(async tag => { + const exists = existingAvatars.find(x => x.name === tag.name); + if (exists) { + if ((exists.updatedAt == null) + || (new Date(tag.updated) > exists.updatedAt) + || (tag.icon.url !== exists.url) + ) { + await this.avatarDecorationRepository.update({ + host, + name: tag.name, + }, { + url: tag.id, + updatedAt: new Date(), + }); + const avatarDecoration = await this.avatarDecorationRepository.findOneBy({ host, name: tag.name }); + if (avatarDecoration == null) throw new Error('avatardecoration update failed'); + return avatarDecoration; + } + return exists; + } + this.logger.info(`register avatarDecoration host=${host}, name=${tag.name}`); + + return await this.avatarDecorationRepository.insert({ + id: this.idService.gen(), + host, + name: tag.name, + url: tag.id, + description: '', + roleIdsThatCanBeUsedThisDecoration: [], + updatedAt: new Date(), + }).then(x => this.avatarDecorationRepository.findOneByOrFail(x.identifiers[0])); + })); + } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 47f8d7313e..2b597ae1f4 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -291,6 +291,15 @@ export class ApPersonService implements OnModuleInit { }); //#endregion + //#region アバターデコレーション取得 + const avatardecorations = await this.apNoteService.extractAvatarDecorations(person.tag ?? [], host) + .then(_decorations => _decorations.map(decorations => decorations.name)) + .catch(err => { + this.logger.error('error occurred while fetching user avatar decorations', { stack: err }); + return []; + }); + //#endregion + try { // Start transaction await this.db.transaction(async transactionalEntityManager => { @@ -317,6 +326,7 @@ export class ApPersonService implements OnModuleInit { isBot, isCat: (person as any).isCat === true, emojis, + avatarDecorations: avatardecorations, })) as MiRemoteUser; await transactionalEntityManager.save(new MiUserProfile({ @@ -419,12 +429,26 @@ export class ApPersonService implements OnModuleInit { this.logger.info(`Updating the Person: ${person.id}`); + const avatardecorations = await this.apNoteService.extractAvatarDecorations(person.tag ?? [], exist.host) + .catch(err => { + this.logger.error('error occurred while fetching user avatar decorations', { stack: err }); + return []; + }); + + avatardecorations.forEach((value, index) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + avatardecorations[index].flipH = person.AvatarDecorations[index].flipH; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + avatardecorations[index].angle = person.AvatarDecorations[index].angle; + }); + // カスタム絵文字取得 const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); return []; }); - const emojiNames = emojis.map(emoji => emoji.name); const fields = this.analyzeAttachments(person.attachment ?? []); @@ -454,9 +478,9 @@ export class ApPersonService implements OnModuleInit { movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, isExplorable: person.discoverable, + avatarDecorations: avatardecorations, ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), } as Partial & Pick; - const moving = ((): boolean => { // 移行先がない→ある if ( diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 16ff86e894..f48a619b33 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -178,6 +178,10 @@ export interface IActor extends IObject { endpoints?: { sharedInbox?: string; }; + AvatarDecorations?:{ + angle: string; + flipH: boolean; + } 'vcard:bday'?: string; 'vcard:Address'?: string; } @@ -228,10 +232,18 @@ export interface IApEmoji extends IObject { name: string; updated: string; } +export interface IApAvatarDecoration extends IObject { + type: 'AvatarDecoration'; + name: string; + updated: string; +} export const isEmoji = (object: IObject): object is IApEmoji => getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; +export const isAvatarDecoration = (object: IObject): object is IApEmoji => + getApType(object) === 'AvatarDecoration' && !Array.isArray(object.icon) && object.icon.url != null; + export interface IKey extends IObject { type: 'Key'; owner: string; diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index 08ebbdeac1..5776cf4bc4 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -16,6 +16,10 @@ export class MiAvatarDecoration { }) public updatedAt: Date | null; + @Column('varchar', { + length: 1024, nullable: true, + }) + public host: string; @Column('varchar', { length: 1024, })