diff --git a/routes/profileRoute.ts b/routes/profileRoute.ts index 10f3097..d742465 100644 --- a/routes/profileRoute.ts +++ b/routes/profileRoute.ts @@ -87,4 +87,14 @@ profileRouter.get('/search', async (req, res, next) => { }) }) +profileRouter.get('/emotes/:streamerName', async (req, res, next) => { + const emotes = await twitch.getEmotes(req.params.streamerName) + .catch(next) + + if(emotes) + res.send(emotes) + }) + + + export default profileRouter \ No newline at end of file diff --git a/routes/proxyRoute.ts b/routes/proxyRoute.ts index d2c64bd..03ffaed 100644 --- a/routes/proxyRoute.ts +++ b/routes/proxyRoute.ts @@ -82,8 +82,6 @@ proxyRouter.get('/stream/segment/:encodedUrl', async (req: Request, res: Respons res.send(buf) }) - - const twitchChatServer = new TwitchChatServer(); export const wsServer = new ws.Server({ noServer: true }); twitchChatServer.startWebSocketServer(wsServer); diff --git a/types/scraping/emotes/bttv.ts b/types/scraping/emotes/bttv.ts new file mode 100644 index 0000000..252072d --- /dev/null +++ b/types/scraping/emotes/bttv.ts @@ -0,0 +1,18 @@ +export type bttvData = SharedEmotesEntity[] + + export interface SharedEmotesEntity { + id: string; + code: string; + imageType: string; + animated: boolean; + user: User; + width?: number | null; + height?: number | null; + codeOriginal?: string | null; + } + export interface User { + id: string; + name: string; + displayName: string; + providerId: string; + } \ No newline at end of file diff --git a/types/scraping/emotes/ffz.ts b/types/scraping/emotes/ffz.ts new file mode 100644 index 0000000..adda0f5 --- /dev/null +++ b/types/scraping/emotes/ffz.ts @@ -0,0 +1,52 @@ +export interface ffzData { + room: Room; + sets: Sets; +} + +export interface Room { + _id: number; + twitch_id: number; + youtube_id?: null; + id: string; + is_group: boolean; + display_name: string; + set: number; +} + +export interface Sets { + [k: number]: SetData; +} + +export interface SetData { + id: number; + _type: number; + title: string; + emoticons: EmoteData[]; +} + +export interface EmoteData { + id: number; + name: string; + height: number; + width: number; + public: boolean; + hidden: boolean; + modifier: boolean; + modifier_flags: number; + owner: Owner; + urls: Urls; + status: number; + usage_count: number; + created_at: string; + last_updated: string; +} + +export interface Owner { + _id: number; + name: string; + display_name: string; +} + +export interface Urls { + [k: number]: string +} diff --git a/types/scraping/emotes/index.ts b/types/scraping/emotes/index.ts new file mode 100644 index 0000000..672bdcc --- /dev/null +++ b/types/scraping/emotes/index.ts @@ -0,0 +1,2 @@ +export * from './bttv' +export * from './ffz' diff --git a/util/scraping/extractor/emoteHandler.ts b/util/scraping/extractor/emoteHandler.ts new file mode 100644 index 0000000..85e8bda --- /dev/null +++ b/util/scraping/extractor/emoteHandler.ts @@ -0,0 +1,53 @@ +import { Urls, bttvData, ffzData } from "../../../types/scraping/emotes" + +interface Emote { + name: string + urls: Urls +} + +export const ffzEmotesHandler = (emoteData: ffzData) => { + const sets = emoteData.sets[emoteData.room.set] + const emotes: Emote[] = [] + + for (let emote of sets.emoticons) { + const data: Emote = { + name: emote.name, + urls: emote.urls + } + + emotes.push(data) + } + + return emotes +} + +const generateBttvEmoteUrls = (id: string) => { + const bttvApi = 'https://cdn.betterttv.net/emote/' + const urls: Urls = {} + + // Creates urls like "https://cdn.betterttv.net/emote/6368b11b9013520589f5ac0c/3x" from 1x to 3x + for (let i = 1; i < 4; i++) { + urls[i] = `${bttvApi}${id}/${i}x` + } + + return urls +} + +export const bttvEmotesHandler = (emoteData: bttvData) => { + const rawEmoteArray = emoteData + console.log(emoteData.length) + const emotes: Emote[] = [] + + for (let rawEmote of rawEmoteArray) { + const formattedEmote: Emote = { + name: rawEmote.code, + urls: generateBttvEmoteUrls(rawEmote.id) + } + + emotes.push(formattedEmote) + } + + console.log(emotes.length) + console.log(emotes) + return emotes +} \ No newline at end of file diff --git a/util/scraping/extractor/index.ts b/util/scraping/extractor/index.ts index bb4357e..21578f9 100644 --- a/util/scraping/extractor/index.ts +++ b/util/scraping/extractor/index.ts @@ -3,6 +3,7 @@ import { StreamerData, StreamData, Social, Badge } from "../../../types/scraping import { Category, Tag } from "../../../types/scraping/Category" import { CategoryData, CategoryMinifiedStream } from "../../../types/scraping/CategoryData" import { SearchResult } from "../../../types/scraping/Search" +import { bttvEmotesHandler, ffzEmotesHandler } from "./emoteHandler" const base64 = (data: String) => { return Buffer.from(data).toString('base64url') @@ -259,8 +260,6 @@ export class TwitchAPI { headers: this.headers }) - console.log(res.status) - return await res.text() } @@ -589,4 +588,54 @@ export class TwitchAPI { return finalData } + + public getStreamerId = async (channelName: string) => { + const payload = { + "operationName": "ChannelRoot_AboutPanel", + "variables": { + "channelLogin": channelName, + "skipSchedule": true + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "6089531acef6c09ece01b440c41978f4c8dc60cb4fa0124c9a9d3f896709b6c6" + } + } + } + + const res = await fetch(this.twitchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: this.headers + }) + const data = await res.json() + + if (!data.data.user) { + return Promise.reject(new Error(`Steamer ${channelName} is offline.`)) + } + + return Number(data.data.user.id) + } + + public getEmotes = async (channelName: string) => { + let id = await this.getStreamerId(channelName) + const ffzUrl = 'https://api.frankerfacez.com/v1/room/id/' + id + const bttvGlobalUrl = 'https://api.betterttv.net/3/cached/users/twitch/121059319' + const bttvChannelUrl = 'https://api.betterttv.net/3/cached/users/twitch/' + id + + let res = await fetch(ffzUrl) + let data = await res.json() + const ffzEmotes = ffzEmotesHandler(data) + + res = await fetch(bttvGlobalUrl) + data = await res.json() + const bttvGlobalEmotes = bttvEmotesHandler([...data.channelEmotes, ...data.sharedEmotes]) + + res = await fetch(bttvChannelUrl) + data = await res.json() + const bttvChannelEmotes = bttvEmotesHandler(data.channelEmotes) + + return [ ...ffzEmotes, ...bttvGlobalEmotes, ...bttvChannelEmotes] + } } \ No newline at end of file