feat: Avatar decoration (#12096)
* wip * Update ja-JP.yml * Update profile.vue * .js * Update home.test.ts
This commit is contained in:
parent
101e5d622d
commit
2c0a139da6
38 changed files with 888 additions and 19 deletions
129
packages/backend/src/core/AvatarDecorationService.ts
Normal file
129
packages/backend/src/core/AvatarDecorationService.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AvatarDecorationService implements OnApplicationShutdown {
|
||||
public cache: MemorySingleCache<MiAvatarDecoration[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.avatarDecorationsRepository)
|
||||
private avatarDecorationsRepository: AvatarDecorationsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
|
||||
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'avatarDecorationCreated':
|
||||
case 'avatarDecorationUpdated':
|
||||
case 'avatarDecorationDeleted': {
|
||||
this.cache.delete();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
|
||||
const created = await this.avatarDecorationsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
...options,
|
||||
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
|
||||
|
||||
if (moderator) {
|
||||
this.moderationLogService.log(moderator, 'createAvatarDecoration', {
|
||||
avatarDecorationId: created.id,
|
||||
avatarDecoration: created,
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(id: MiAvatarDecoration['id'], params: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<void> {
|
||||
const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
|
||||
|
||||
const date = new Date();
|
||||
await this.avatarDecorationsRepository.update(avatarDecoration.id, {
|
||||
updatedAt: date,
|
||||
...params,
|
||||
});
|
||||
|
||||
const updated = await this.avatarDecorationsRepository.findOneByOrFail({ id: avatarDecoration.id });
|
||||
this.globalEventService.publishInternalEvent('avatarDecorationUpdated', updated);
|
||||
|
||||
if (moderator) {
|
||||
this.moderationLogService.log(moderator, 'updateAvatarDecoration', {
|
||||
avatarDecorationId: avatarDecoration.id,
|
||||
before: avatarDecoration,
|
||||
after: updated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise<void> {
|
||||
const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
|
||||
|
||||
await this.avatarDecorationsRepository.delete({ id: avatarDecoration.id });
|
||||
this.globalEventService.publishInternalEvent('avatarDecorationDeleted', avatarDecoration);
|
||||
|
||||
if (moderator) {
|
||||
this.moderationLogService.log(moderator, 'deleteAvatarDecoration', {
|
||||
avatarDecorationId: avatarDecoration.id,
|
||||
avatarDecoration: avatarDecoration,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getAll(noCache = false): Promise<MiAvatarDecoration[]> {
|
||||
if (noCache) {
|
||||
this.cache.delete();
|
||||
}
|
||||
return this.cache.fetch(() => this.avatarDecorationsRepository.find());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import { AnnouncementService } from './AnnouncementService.js';
|
|||
import { AntennaService } from './AntennaService.js';
|
||||
import { AppLockService } from './AppLockService.js';
|
||||
import { AchievementService } from './AchievementService.js';
|
||||
import { AvatarDecorationService } from './AvatarDecorationService.js';
|
||||
import { CaptchaService } from './CaptchaService.js';
|
||||
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
||||
import { CustomEmojiService } from './CustomEmojiService.js';
|
||||
|
@ -140,6 +141,7 @@ const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExis
|
|||
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
||||
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
|
||||
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
||||
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
|
||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
||||
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
|
||||
|
@ -273,6 +275,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
|
@ -399,6 +402,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
|
@ -526,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
|
@ -651,6 +656,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
|
|
|
@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js';
|
|||
import type { MiPage } from '@/models/Page.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -188,6 +188,9 @@ export interface InternalEventTypes {
|
|||
antennaCreated: MiAntenna;
|
||||
antennaDeleted: MiAntenna;
|
||||
antennaUpdated: MiAntenna;
|
||||
avatarDecorationCreated: MiAvatarDecoration;
|
||||
avatarDecorationDeleted: MiAvatarDecoration;
|
||||
avatarDecorationUpdated: MiAvatarDecoration;
|
||||
metaUpdated: MiMeta;
|
||||
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||
|
|
|
@ -227,6 +227,12 @@ export class RoleService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoles() {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
return roles;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserAssigns(userId: MiUser['id']) {
|
||||
const now = Date.now();
|
||||
|
|
|
@ -21,9 +21,10 @@ import { RoleService } from '@/core/RoleService.js';
|
|||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { AnnouncementService } from '../AnnouncementService.js';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
|
@ -62,6 +63,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
private roleService: RoleService;
|
||||
private federatedInstanceService: FederatedInstanceService;
|
||||
private idService: IdService;
|
||||
private avatarDecorationService: AvatarDecorationService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
@ -126,6 +128,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
this.roleService = this.moduleRef.get('RoleService');
|
||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
||||
}
|
||||
|
||||
//#region Validators
|
||||
|
@ -328,8 +331,6 @@ export class UserEntityService implements OnModuleInit {
|
|||
...announcement,
|
||||
})) : null;
|
||||
|
||||
const falsy = opts.detail ? false : undefined;
|
||||
|
||||
const packed = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
|
@ -337,6 +338,10 @@ 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 => decorations.filter(decoration => user.avatarDecorations.includes(decoration.id)).map(decoration => ({
|
||||
id: decoration.id,
|
||||
url: decoration.url,
|
||||
}))) : [],
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue