├── .gitignore ├── LICENSE ├── README.md ├── config.json ├── package-lock.json ├── package.json ├── src ├── app.js ├── constants.js ├── emote.js ├── parsers │ ├── 7tvparser.js │ ├── bttvparser.js │ ├── ffzparser.js │ ├── index.js │ └── twitchparser.js ├── routes │ └── v1 │ │ ├── channel │ │ └── index.js │ │ ├── global │ │ └── index.js │ │ └── name │ │ └── index.js ├── server.js └── utils │ └── fetcher.js └── test ├── live.test.js └── parse.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | config.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 animekkk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emapi is discontinued 2 | You should checkout [adiq - temotes](https://github.com/adiq/temotes). 3 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 5021, 3 | "twitch": { 4 | "token": "", 5 | "clientId": "" 6 | }, 7 | "redis": { 8 | "host": "127.0.0.1", 9 | "port": 6379, 10 | "password": "" 11 | }, 12 | "cache": { 13 | "bttv": 300, 14 | "ffz": 300, 15 | "7tv": 300, 16 | "twitch": 3600, 17 | "all": 3600, 18 | "name": 86400 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emapi", 3 | "version": "1.0.0", 4 | "description": "Simple service to cache and access emotes from services like BTTV, FFZ, 7TV and Twitch.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node --experimental-specifier-resolution=node src/server.js", 8 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --colors" 9 | }, 10 | "keywords": [ 11 | "twitch", 12 | "bttv", 13 | "ffz", 14 | "7tv", 15 | "emotes" 16 | ], 17 | "author": "animekkk", 18 | "license": "MIT", 19 | "type": "module", 20 | "dependencies": { 21 | "@jest/globals": "^27.0.6", 22 | "cors": "^2.8.5", 23 | "express": "^4.17.1", 24 | "express-rate-limit": "^5.3.0", 25 | "node-fetch": "^2.6.1", 26 | "redis": "^3.1.2" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.26.0", 30 | "babel-preset-env": "^1.7.0", 31 | "jest": "^27.0.6", 32 | "superagent": "^6.1.0", 33 | "supertest": "^6.1.4" 34 | }, 35 | "jest": { 36 | "moduleFileExtensions": [ 37 | "js", 38 | "jsx" 39 | ], 40 | "moduleDirectories": [ 41 | "node_modules", 42 | "src", 43 | "test" 44 | ], 45 | "transform": {}, 46 | "verbose": false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import rateLimit from 'express-rate-limit'; 3 | import cors from 'cors'; 4 | import { globalApi } from './routes/v1/global'; 5 | import { channelApi } from './routes/v1/channel'; 6 | import { nameApi } from './routes/v1/name'; 7 | 8 | export const app = express(); 9 | 10 | const limiter = rateLimit({ 11 | windowMs: 1000, 12 | max: 5, 13 | message: { error: 'Too many requests in one second.' } 14 | }); 15 | 16 | app.use(cors({ 17 | origin: '*', 18 | optionsSuccessStatus: 200 19 | })); 20 | 21 | app.use(limiter); 22 | 23 | app.set('json spaces', 2) 24 | 25 | app.use('/v1/global', globalApi); 26 | app.use('/v1/channel', channelApi); 27 | app.use('/v1/name', nameApi); 28 | 29 | app.get('/', (request, response) => { 30 | response.status(200).json({ message: 'everything cool B)' }); 31 | }); -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANNEL_BTTV = 'https://api.betterttv.net/3/cached/users/twitch/{id}'; 2 | export const CHANNEL_FFZ = 'https://api.frankerfacez.com/v1/room/id/{id}'; 3 | export const CHANNEL_7TV = 'https://api.7tv.app/v2/users/{id}/emotes'; 4 | export const GLOBAL_BTTV = 'https://api.betterttv.net/3/cached/emotes/global'; 5 | export const GLOBAL_FFZ = 'https://api.frankerfacez.com/v1/set/global'; 6 | export const GLOBAL_7TV = 'https://api.7tv.app/v2/emotes/global'; 7 | export const CHANNEL_TWITCH = 'https://api.twitch.tv/helix/chat/emotes?broadcaster_id={id}'; 8 | export const GLOBAL_TWITCH = 'https://api.twitch.tv/helix/chat/emotes/global'; -------------------------------------------------------------------------------- /src/emote.js: -------------------------------------------------------------------------------- 1 | export class Emote { 2 | 3 | code; 4 | urls; 5 | 6 | constructor(code, urls) { 7 | this.code = code; 8 | this.urls = urls; 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /src/parsers/7tvparser.js: -------------------------------------------------------------------------------- 1 | import { Emote } from '../emote'; 2 | 3 | export function parse(json) { 4 | const emotes = []; 5 | if(json && json.status !== 404) { 6 | json.forEach(emoteJson => { 7 | const urls = { 8 | '1x': emoteJson.urls[0][1], 9 | '2x': emoteJson.urls[1][1], 10 | '4x': emoteJson.urls[3][1], 11 | } 12 | emotes.push(new Emote(emoteJson.name, urls)); 13 | }); 14 | } 15 | return emotes; 16 | } -------------------------------------------------------------------------------- /src/parsers/bttvparser.js: -------------------------------------------------------------------------------- 1 | import { Emote } from '../emote'; 2 | 3 | const CDN = 'https://cdn.betterttv.net/emote/{id}/{size}' 4 | 5 | export function parse(json) { 6 | const emotes = []; 7 | if(json && json.status !== 404) { 8 | json.channelEmotes.forEach(emoteJson => { 9 | emotes.push(parseEmote(emoteJson)); 10 | }); 11 | json.sharedEmotes.forEach(emoteJson => { 12 | emotes.push(parseEmote(emoteJson)); 13 | }); 14 | } 15 | return emotes; 16 | } 17 | 18 | export function parseGlobal(json) { 19 | const emotes = []; 20 | json.forEach(emote => { 21 | emotes.push(parseEmote(emote)); 22 | }); 23 | return emotes; 24 | } 25 | 26 | function parseEmote(emoteJson) { 27 | const urls = { 28 | '1x': CDN.replace('{id}', emoteJson.id).replace('{size}', '1x'), 29 | '2x': CDN.replace('{id}', emoteJson.id).replace('{size}', '2x'), 30 | '4x': CDN.replace('{id}', emoteJson.id).replace('{size}', '4x') 31 | } 32 | return new Emote(emoteJson.code, urls); 33 | } -------------------------------------------------------------------------------- /src/parsers/ffzparser.js: -------------------------------------------------------------------------------- 1 | import { Emote } from '../emote'; 2 | 3 | export function parse(json) { 4 | const emotes = []; 5 | if(json && json.status !== 404) { 6 | const sets = json.sets; 7 | if(sets !== undefined) { 8 | Object.keys(sets).forEach(set => { 9 | sets[set].emoticons.forEach(emoteJson => { 10 | const urls = { 11 | '1x': emoteJson.urls['1'], 12 | '2x': emoteJson.urls['2'], 13 | '4x': emoteJson.urls['4'] 14 | } 15 | emotes.push(new Emote(emoteJson.name, urls)); 16 | }); 17 | }) 18 | } 19 | } 20 | return emotes; 21 | } -------------------------------------------------------------------------------- /src/parsers/index.js: -------------------------------------------------------------------------------- 1 | import { parse as parseBTTV, parseGlobal as parseGlobalBTTV } from './bttvparser' 2 | import { parse as parseFFZ } from './ffzparser' 3 | import { parse as parse7TV } from './7tvparser' 4 | import { parse as parseTwitch } from './twitchparser' 5 | import { CHANNEL_BTTV, CHANNEL_FFZ, CHANNEL_7TV, CHANNEL_TWITCH, GLOBAL_BTTV, GLOBAL_FFZ, GLOBAL_7TV, GLOBAL_TWITCH } from '../constants'; 6 | import { client } from '../server'; 7 | import { fetchJson } from '../utils/fetcher' 8 | import { config } from '../server'; 9 | 10 | const services = ['bttv', 'ffz', '7tv', 'twitch', 'all']; 11 | 12 | export function exist(service) { 13 | return services.includes(service); 14 | } 15 | 16 | export async function parseChannel(id, service) { 17 | let emotes = []; 18 | console.info(`Parsing - ${id} - ${service}`); 19 | switch(service) { 20 | case 'bttv': 21 | emotes = parseBTTV(await fetchJson(CHANNEL_BTTV.replace('{id}', id))); 22 | break; 23 | case 'ffz': 24 | emotes = parseFFZ(await fetchJson(CHANNEL_FFZ.replace('{id}', id))); 25 | break; 26 | case '7tv': 27 | emotes = parse7TV(await fetchJson(CHANNEL_7TV.replace('{id}', id))); 28 | break; 29 | case 'twitch': 30 | emotes = parseTwitch(await fetchJson(CHANNEL_TWITCH.replace('{id}', id), { 31 | headers: { 32 | 'Authorization': `Bearer ${config.twitch.token}`, 33 | 'Client-Id': config.twitch.clientId 34 | } 35 | })); 36 | break; 37 | default: 38 | for(let name of services) { 39 | if(name !== 'all') { 40 | const data = await parseChannel(id, name); 41 | emotes = emotes.concat(data.emotes); 42 | } 43 | } 44 | } 45 | const data = { '_cache': Date.now(), 'emotes': emotes }; 46 | //TODO Don't save to service ALL, but just get them from other services. 47 | if(emotes.length > 1) client.hset('emotes-channel', `${id}-${service}`, JSON.stringify(data)); 48 | return data; 49 | } 50 | 51 | export async function parseGlobal(service) { 52 | let emotes = []; 53 | switch(service) { 54 | case 'bttv': 55 | emotes = parseGlobalBTTV(await fetchJson(GLOBAL_BTTV)); 56 | break; 57 | case 'ffz': 58 | emotes = parseFFZ(await fetchJson(GLOBAL_FFZ)); 59 | break; 60 | case '7tv': 61 | emotes = parse7TV(await fetchJson(GLOBAL_7TV)); 62 | break; 63 | case 'twitch': 64 | emotes = parseTwitch(await fetchJson(GLOBAL_TWITCH, { 65 | headers: { 66 | 'Authorization': `Bearer ${config.twitch.token}`, 67 | 'Client-Id': config.twitch.clientId 68 | } 69 | })); 70 | break; 71 | default: 72 | for(let name of services) { 73 | if(name !== 'all') { 74 | const data = await parseGlobal(name); 75 | emotes = emotes.concat(data.emotes); 76 | } 77 | } 78 | } 79 | const data = { '_cache': Date.now(), 'emotes': emotes }; 80 | //TODO Don't save to service ALL, but just get them from other services. 81 | if(emotes.length > 1) client.hset('emotes-global', service, JSON.stringify(data)); 82 | return data; 83 | } -------------------------------------------------------------------------------- /src/parsers/twitchparser.js: -------------------------------------------------------------------------------- 1 | import { Emote } from '../emote'; 2 | 3 | export function parse(json) { 4 | const emotes = []; 5 | if(json && json.status !== 404 && json.data && json.data.length > 0) { 6 | json.data.forEach(emoteJson => { 7 | const urls = { 8 | '1x': emoteJson.images['url_1x'].replace('/static/', '/default/'), 9 | '2x': emoteJson.images['url_2x'].replace('/static/', '/default/'), 10 | '4x': emoteJson.images['url_4x'].replace('/static/', '/default/'), 11 | } 12 | emotes.push(new Emote(emoteJson.name, urls)); 13 | }); 14 | } 15 | return emotes; 16 | } -------------------------------------------------------------------------------- /src/routes/v1/channel/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { client, config } from '../../../server'; 3 | import { parseChannel as parse, exist } from '../../../parsers'; 4 | 5 | export const channelApi = express.Router(); 6 | 7 | channelApi.get('/:id/:service', async (request, response) => { 8 | const id = request.params.id; 9 | let service = request.params.service.toLowerCase(); 10 | if(!exist(service)) service = 'all'; 11 | const data = await client.hget('emotes-channel', `${id}-${service}`); 12 | let json; 13 | if(data) { 14 | json = JSON.parse(data); 15 | if((Date.now() - json['_cache']) > (config.cache[service] * 1000)) json = await parse(id, service) 16 | } else json = await parse(id, service) 17 | response.status(200).json(json); 18 | }); -------------------------------------------------------------------------------- /src/routes/v1/global/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { exist, parseGlobal as parse } from '../../../parsers'; 4 | import { client, config } from '../../../server'; 5 | 6 | export const globalApi = express.Router(); 7 | 8 | globalApi.get('/:service', async (request, response) => { 9 | let service = request.params.service.toLowerCase(); 10 | if(!exist(service)) service = 'all'; 11 | const data = await client.hget('emotes-global', service); 12 | let json; 13 | if(data) { 14 | json = JSON.parse(data); 15 | if((Date.now() - json['_cache']) > (config.cache[service] * 1000)) json = await parse(service) 16 | } else json = await parse(service) 17 | response.status(200).json(json); 18 | }); -------------------------------------------------------------------------------- /src/routes/v1/name/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { client, config } from '../../../server'; 3 | import { fetchJson } from '../../../utils/fetcher'; 4 | 5 | export const nameApi = express.Router(); 6 | 7 | nameApi.get('/:name', async (request, response) => { 8 | const name = request.params.name.toLowerCase(); 9 | const data = await client.hget('names-id', name); 10 | let json; 11 | if(data) { 12 | json = JSON.parse(data); 13 | if((Date.now() - json['_cache']) > (config.cache['name'] * 1000)) json = await getId(name) 14 | } else json = await getId(name) 15 | if(!json.error) client.hset('names-id', `${name}`, JSON.stringify(json)); 16 | response.status(200).json(json); 17 | }); 18 | 19 | async function getId(name) { 20 | const json = await fetchJson(`https://api.twitch.tv/helix/users?login=${name}`, { 21 | headers: { 22 | 'Authorization': `Bearer ${config.twitch.token}`, 23 | 'Client-Id': config.twitch.clientId 24 | } 25 | }); 26 | if(json.error) return { error: 'This user does not exists.' }; 27 | if(json.data.length < 1) return { error: 'This user does not exists.' }; 28 | return { '_cache': Date.now(), 'id': json.data[0].id }; 29 | } -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import { app } from './app'; 2 | import fs from 'fs'; 3 | import redis from 'redis'; 4 | import util from 'util'; 5 | 6 | export const config = JSON.parse(fs.readFileSync('config.json', 'utf8')); 7 | 8 | export const client = redis.createClient({ 9 | host: config.redis.host, 10 | port: config.redis.port 11 | }); 12 | if(config.redis.password.length > 0) { 13 | console.info('Logging to redis with password...'); 14 | client.auth(config.redis.password); 15 | } 16 | 17 | client.hget = util.promisify(client.hget); 18 | 19 | app.listen(config.port, () => console.log(`Service running at port ${config.port}`)); -------------------------------------------------------------------------------- /src/utils/fetcher.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export async function fetchJson(url, options) { 4 | if(!options) options = {}; 5 | const data = await fetch(url, options); 6 | const json = await data.json(); 7 | json.status = data.status; 8 | return json; 9 | } -------------------------------------------------------------------------------- /test/live.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals'; 2 | import request from 'supertest'; 3 | import { app } from '../src/app'; 4 | 5 | //Not working idk why xD 6 | describe('Test for global path', () => { 7 | test('Status code should be 200', done => { 8 | request(app) 9 | .get('/') 10 | .then(response => { 11 | expect(response.statusCode).toBe(200); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | 17 | describe('Test for global emotes path', () => { 18 | test('Cache should exist', done => { 19 | request(app) 20 | .get('/v1/global/all') 21 | .then(response => { 22 | expect(JSON.parse(response.text)['_cache']).toBeDefined(); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | 28 | describe('Test for channel emotes path', () => { 29 | test('Cache should exist', done => { 30 | request(app) 31 | .get('/v1/channel/1535/all') 32 | .then(response => { 33 | expect(JSON.parse(response.text)['_cache']).toBeDefined(); 34 | done(); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | import { parse as parser7TV } from '../src/parsers/7tvparser'; 2 | import { parse as parserBTTV } from '../src/parsers/bttvparser'; 3 | import { parse as parserFFZ } from '../src/parsers/ffzparser'; 4 | import fetch from 'node-fetch'; 5 | import { expect } from '@jest/globals'; 6 | 7 | test('parsing 7tv', async () => { 8 | const json = await fetch('https://api.7tv.app/v2/users/87037696/emotes'); 9 | expect(parser7TV(await json.json())[0]).toBeDefined(); 10 | }) 11 | 12 | test('parsing bttv', async () => { 13 | const json = await fetch('https://api.betterttv.net/3/cached/users/twitch/87037696'); 14 | expect(parserBTTV(await json.json())[0]).toBeDefined(); 15 | }) 16 | 17 | test('parsing ffz', async () => { 18 | const json = await fetch('https://api.frankerfacez.com/v1/room/id/87037696'); 19 | expect(parserFFZ(await json.json())[0]).toBeDefined(); 20 | }) --------------------------------------------------------------------------------