|
| 1 | +import { Injectable, Logger, Inject } from '@nestjs/common'; |
| 2 | +import { CACHE_MANAGER } from '@nestjs/cache-manager'; |
| 3 | +import { Cache } from 'cache-manager'; |
| 4 | +import { TokenManagerService } from '../auth/token-manager.service'; |
| 5 | +import { ConfigService } from '@nestjs/config'; |
| 6 | +import axios from 'axios'; |
| 7 | +import qs from 'qs'; |
| 8 | + |
| 9 | +export interface ChannelAssets { |
| 10 | + globalBadges: any; |
| 11 | + channelBadges: any; |
| 12 | + bttvEmotes: any[]; |
| 13 | + ffzEmotes: any[]; |
| 14 | +} |
| 15 | + |
| 16 | +@Injectable() |
| 17 | +export class EmoteCacheService { |
| 18 | + private readonly logger = new Logger(EmoteCacheService.name); |
| 19 | + private readonly ASSETS_TTL_MS = 3600 * 1000; // 1 Hora em milisegundos |
| 20 | + |
| 21 | + constructor( |
| 22 | + @Inject(CACHE_MANAGER) private cacheManager: Cache, |
| 23 | + private tokenManager: TokenManagerService, |
| 24 | + private configService: ConfigService, |
| 25 | + ) {} |
| 26 | + |
| 27 | + private async axiosGet(url: string, params: any = {}) { |
| 28 | + const token = await this.tokenManager.getToken(); |
| 29 | + const clientId = this.configService.get<string>('TWITCH_CLIENT_ID'); |
| 30 | + |
| 31 | + return axios({ |
| 32 | + method: 'GET', |
| 33 | + url: Object.keys(params).length ? `${url}?${qs.stringify(params)}` : url, |
| 34 | + headers: { |
| 35 | + 'Client-ID': clientId, |
| 36 | + Authorization: `Bearer ${token}`, |
| 37 | + }, |
| 38 | + }); |
| 39 | + } |
| 40 | + |
| 41 | + private async getChannelID(channelName: string): Promise<string | null> { |
| 42 | + try { |
| 43 | + const res = await this.axiosGet('https://api.twitch.tv/helix/users', { login: channelName }); |
| 44 | + return res.data?.data?.[0]?.id || null; |
| 45 | + } catch (error: any) { |
| 46 | + this.logger.error(`Error fetching channel ID for ${channelName}: ${error.message}`); |
| 47 | + return null; |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + async getChannelAssets(channelName: string): Promise<ChannelAssets | null> { |
| 52 | + const cacheKey = `assets:${channelName.toLowerCase()}`; |
| 53 | + |
| 54 | + // Tenta buscar no Redis primeiro |
| 55 | + const cached = await this.cacheManager.get<ChannelAssets>(cacheKey); |
| 56 | + if (cached) { |
| 57 | + this.logger.log(`CACHE HIT: Loaded assets for ${channelName} from Redis.`); |
| 58 | + return cached; |
| 59 | + } |
| 60 | + |
| 61 | + this.logger.log(`CACHE MISS: Fetching external API assets for ${channelName}...`); |
| 62 | + |
| 63 | + const channelId = await this.getChannelID(channelName); |
| 64 | + if (!channelId) { |
| 65 | + this.logger.warn(`Could not resolve channel ID for ${channelName}`); |
| 66 | + return null; |
| 67 | + } |
| 68 | + |
| 69 | + try { |
| 70 | + const [ |
| 71 | + globalBadgesRes, |
| 72 | + channelBadgesRes, |
| 73 | + bttvChannelRes, |
| 74 | + bttvGlobalRes, |
| 75 | + ffzChannelRes, |
| 76 | + ffzGlobalRes |
| 77 | + ] = await Promise.allSettled([ |
| 78 | + this.axiosGet('https://api.twitch.tv/helix/chat/badges/global'), |
| 79 | + this.axiosGet('https://api.twitch.tv/helix/chat/badges', { broadcaster_id: channelId }), |
| 80 | + axios.get(`https://api.betterttv.net/3/cached/users/twitch/${channelId}`), |
| 81 | + axios.get(`https://api.betterttv.net/3/cached/emotes/global`), |
| 82 | + axios.get(`https://api.frankerfacez.com/v1/room/id/${channelId}`), |
| 83 | + axios.get(`https://api.frankerfacez.com/v1/set/global`), |
| 84 | + ]); |
| 85 | + |
| 86 | + const globalBadges = globalBadgesRes.status === 'fulfilled' ? globalBadgesRes.value.data : null; |
| 87 | + const channelBadges = channelBadgesRes.status === 'fulfilled' ? channelBadgesRes.value.data : null; |
| 88 | + |
| 89 | + const bttvEmotes: any[] = []; |
| 90 | + if (bttvChannelRes.status === 'fulfilled') { |
| 91 | + bttvEmotes.push(...(bttvChannelRes.value.data.channelEmotes || [])); |
| 92 | + bttvEmotes.push(...(bttvChannelRes.value.data.sharedEmotes || [])); |
| 93 | + } |
| 94 | + if (bttvGlobalRes.status === 'fulfilled') { |
| 95 | + bttvEmotes.push(...(bttvGlobalRes.value.data || [])); |
| 96 | + } |
| 97 | + |
| 98 | + const ffzEmotes: any[] = []; |
| 99 | + if (ffzChannelRes.status === 'fulfilled') { |
| 100 | + const sets = ffzChannelRes.value.data.sets; |
| 101 | + const setKey = Object.keys(sets)[0]; |
| 102 | + if (setKey) ffzEmotes.push(...sets[setKey].emoticons); |
| 103 | + } |
| 104 | + if (ffzGlobalRes.status === 'fulfilled') { |
| 105 | + ffzEmotes.push(...(ffzGlobalRes.value.data.sets['3']?.emoticons || [])); |
| 106 | + } |
| 107 | + |
| 108 | + const payload: ChannelAssets = { |
| 109 | + globalBadges, |
| 110 | + channelBadges, |
| 111 | + bttvEmotes, |
| 112 | + ffzEmotes, |
| 113 | + }; |
| 114 | + |
| 115 | + // Salva no Redis com TTL de 1 Hora |
| 116 | + await this.cacheManager.set(cacheKey, payload, this.ASSETS_TTL_MS); |
| 117 | + |
| 118 | + return payload; |
| 119 | + } catch (error: any) { |
| 120 | + this.logger.error(`Error resolving promises for assets: ${error.message}`); |
| 121 | + return null; |
| 122 | + } |
| 123 | + } |
| 124 | +} |
0 commit comments