/* * SPDX-FileCopyrightText: syuilo and misskey-project * 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; 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(1000 * 60 * 30); // 30s this.redisForSub.on('message', this.onMessage); } @bindThis private async onMessage(_: string, data: string): Promise { 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, moderator?: MiUser): Promise { const created = await this.avatarDecorationsRepository.insertOne({ id: this.idService.gen(), ...options, }); 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, moderator?: MiUser): Promise { 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 { 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 { 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(); } }