Feat:アバターデコレーションを連合するように

Signed-off-by: mattyatea <mattyacocacora0@gmail.com>
This commit is contained in:
mattyatea 2023-10-27 04:26:10 +09:00
parent 4dd4a11cef
commit bfd817ae10
No known key found for this signature in database
GPG Key ID: 068E54E2C33BEF9A
6 changed files with 137 additions and 7 deletions

View File

@ -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"`);
}
}

View File

@ -26,11 +26,14 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MiAvatarDecoration } from '@/models/_.js';
import { LdSignatureService } from './LdSignatureService.js'; import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.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() @Injectable()
export class ApRendererService { export class ApRendererService {
@ -54,6 +57,7 @@ export class ApRendererService {
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private avatarDecorationService: AvatarDecorationService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService, 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 // to anonymise reporters, the reporting actor must be a system user
@bindThis @bindThis
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag { 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 emojis = await this.getEmojis(user.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); 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)); const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
AvatarDecorations.forEach((decoration, index) => user.avatarDecorations[index].id = decoration.name ); //デコレーションのidのところにnameを突っ込んでる(これ以外思いつかなかった)
const tag = [ const tag = [
...apemojis, ...apemojis,
...hashtagTags, ...hashtagTags,
...apAvatarDecorations,
]; ];
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
@ -503,6 +525,7 @@ export class ApRendererService {
publicKey: this.renderKey(user, keypair, '#main-key'), publicKey: this.renderKey(user, keypair, '#main-key'),
isCat: user.isCat, isCat: user.isCat,
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
AvatarDecorations: user.avatarDecorations,
}; };
if (user.movedToUri) { if (user.movedToUri) {
@ -720,4 +743,12 @@ export class ApRendererService {
return emojis; return emojis;
} }
@bindThis
private async getAvatarDecorations(decorations: { id: string; angle: number; flipH: boolean }[]): Promise<MiAvatarDecoration[]> {
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 ?? [];
}
} }

View File

@ -7,12 +7,13 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; 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 { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import type { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { AppLockService } from '@/core/AppLockService.js'; import { AppLockService } from '@/core/AppLockService.js';
import type { MiDriveFile } from '@/models/DriveFile.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 { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.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 { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js'; import { ApDbResolverService } from '../ApDbResolverService.js';
@ -37,7 +38,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js'; import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js'; import type { IObject, IPost } from '../type.js';
@Injectable() @Injectable()
export class ApNoteService { export class ApNoteService {
private logger: Logger; private logger: Logger;
@ -52,6 +52,9 @@ export class ApNoteService {
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
@Inject(DI.avatarDecorationsRepository)
private avatarDecorationRepository: AvatarDecorationsRepository,
private idService: IdService, private idService: IdService,
private apMfmService: ApMfmService, private apMfmService: ApMfmService,
private apResolverService: ApResolverService, private apResolverService: ApResolverService,
@ -397,4 +400,47 @@ export class ApNoteService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
})); }));
} }
@bindThis
public async extractAvatarDecorations(AvatarDecorations: IObject | IObject[], host: string): Promise<MiAvatarDecoration[]> {
// 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]));
}));
}
} }

View File

@ -291,6 +291,15 @@ export class ApPersonService implements OnModuleInit {
}); });
//#endregion //#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 { try {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
@ -317,6 +326,7 @@ export class ApPersonService implements OnModuleInit {
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
emojis, emojis,
avatarDecorations: avatardecorations,
})) as MiRemoteUser; })) as MiRemoteUser;
await transactionalEntityManager.save(new MiUserProfile({ await transactionalEntityManager.save(new MiUserProfile({
@ -419,12 +429,26 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the Person: ${person.id}`); 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 => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return []; return [];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
const fields = this.analyzeAttachments(person.attachment ?? []); const fields = this.analyzeAttachments(person.attachment ?? []);
@ -454,9 +478,9 @@ export class ApPersonService implements OnModuleInit {
movedToUri: person.movedTo ?? null, movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null, alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: person.discoverable, isExplorable: person.discoverable,
avatarDecorations: avatardecorations,
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; } as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = ((): boolean => { const moving = ((): boolean => {
// 移行先がない→ある // 移行先がない→ある
if ( if (

View File

@ -178,6 +178,10 @@ export interface IActor extends IObject {
endpoints?: { endpoints?: {
sharedInbox?: string; sharedInbox?: string;
}; };
AvatarDecorations?:{
angle: string;
flipH: boolean;
}
'vcard:bday'?: string; 'vcard:bday'?: string;
'vcard:Address'?: string; 'vcard:Address'?: string;
} }
@ -228,10 +232,18 @@ export interface IApEmoji extends IObject {
name: string; name: string;
updated: string; updated: string;
} }
export interface IApAvatarDecoration extends IObject {
type: 'AvatarDecoration';
name: string;
updated: string;
}
export const isEmoji = (object: IObject): object is IApEmoji => export const isEmoji = (object: IObject): object is IApEmoji =>
getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; 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 { export interface IKey extends IObject {
type: 'Key'; type: 'Key';
owner: string; owner: string;

View File

@ -16,6 +16,10 @@ export class MiAvatarDecoration {
}) })
public updatedAt: Date | null; public updatedAt: Date | null;
@Column('varchar', {
length: 1024, nullable: true,
})
public host: string;
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
}) })