import * as cheerio from 'cheerio'; import { CommandInteraction, MessageEmbed } from 'discord.js'; import { IncomingMessage } from 'http'; import http from 'https'; import { emsg } from './util'; const uniteApiRegex = { //$1 = name, $2 = id ogtitle: /unite api - (.+) \((.*)\)/i, //$1 = level, $2 = rank, $3 = elo/class (rest is found by splitting each line) ogdescription: [ //just the first line /lv\.(\d+) (\w+) \((\d+)\)/i, //master /lv\.(\d+) (\w+): class (\d+)/i //other ] } type uniteApiData = { name: string, id: string, avatar: string, level: string, rank: string, class: string|null, elo: string|null, battles: string, wins: string, winrate: string } /** * gets the html of the uniteApi page for the player * @param name name of player * @returns the html of the page through a promise (rejects if the page status is not 200) */ function getHTML(name: string): Promise { //name = name.replace(/[^\w\d]/g, ''); return new Promise((resolve, reject) => { const init = { host: 'uniteapi.dev', path: `/p/${encodeURIComponent(name)}`, method: 'GET', }; const callback = (response: IncomingMessage) => { if (response.statusCode !== 200) { reject(`HTTP ERROR ${response.statusCode}: ${response.statusMessage}`); return; } let result = Buffer.alloc(0); response.on('data', chunk => { result = Buffer.concat([result, chunk]); }); response.on('end', () => { resolve(result.toString()); }); }; const req = http.request(init, callback); req.end(); }); } /** * interprets the html from getHTML() * @param name name of player * @returns player data from site * @throws errorMessage class if the request fails */ function readHTML(html: string): uniteApiData { let $ = cheerio.load(html) let metaElems = $('meta').toArray(), foundData: uniteApiData = { name: "", id: "", avatar: "", level: "", rank: "", elo: null, class: null, battles: "", wins: "", winrate: "" }; //filter down to just ones named "og:..." metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:')); metaElems.forEach(el => { let attr = el.attribs; if (attr.property === 'og:title') { let data = uniteApiRegex.ogtitle.exec(attr.content); if (data !== null && data.length >= 3) { foundData.name = data[1]; foundData.id = data[2]; } } else if (attr.property === 'og:description') { //all lines let lines = attr.content.split('\n').map(l => l.trim()), extraLines: string[] = []; //ensure first line is correct while (lines.length && !/pok.mon unite/i.test(lines[0])) { let line = lines.shift(); if (line) extraLines.push(line); } if (!lines.length) throw emsg('Unable to read data, please try again'); //bring the first lines removed back into the data lines = [ ...lines, ...extraLines.filter(d => d) ]; //first line { //will be only text after "pokemon unite:" let line = lines[0].split(':').slice(1).join(':').trim(); let regex = uniteApiRegex.ogdescription; if (regex[0].test(line)) { //is master/has elo let regexData = line.match(regex[0]); if (!regexData || regexData.length < 4) throw emsg('Unable to read data, please try again'); foundData.level = regexData[1]; foundData.rank = regexData[2]; foundData.elo = regexData[3]; } else { //is not master/has a class let regexData = line.match(regex[1]); if (!regexData || regexData.length < 4) throw emsg('Unable to read data, please try again'); foundData.level = regexData[1]; foundData.rank = regexData[2]; foundData.class = regexData[3]; } } lines.shift(); //rest of lines lines.forEach(line => { let split = line.split(':').map(l => l.trim()), key = split[0].toLowerCase().replace(/[^\w]/g, ''), value = split[1]; switch(key) { case 'battles': foundData.battles = value; break; case 'wins': foundData.wins = value; break; case 'winrate': foundData.winrate = value; break; } }); } }); foundData.avatar = $('.player-card-image img').attr('src') || ""; foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/'); return foundData; } /** * verifies the data * @param data uniteApi data * @returns boolean, valid or invalid */ function verifyData(data: uniteApiData): boolean { if (data.id.length) return true; return false; } /** * gets player data from uniteApi * @param name name of player * @returns player data */ export async function getPlayer(name: string): Promise { let html = await getHTML(name); let data = readHTML(html); if (verifyData(data)) return data; return null; } async function sendPlayerEmbed(interaction: CommandInteraction, data: uniteApiData) { let embed = new MessageEmbed() .setTitle(`${data.name} (${data.id})`) .setURL(`https://uniteapi.dev/p/${encodeURIComponent(data.name)}`) .setTimestamp() .setThumbnail(data.avatar) .setDescription(`Level ${data.level} ${data.rank} ${data.elo ? `(${data.elo})` : `Class ${data.class}`} **Battles** ${data.battles} **Wins** ${data.wins} **Win Rate** ${data.winrate}`); await interaction.editReply({embeds: [embed]}); } /** * calls getPlayer() with the name from the interaction * @param interaction discord interaction * @throws errorMessage class if the user cannot be found */ export async function getPlayerInteraction(interaction: CommandInteraction) { let username = interaction.options.getString('username', true); await interaction.deferReply(); let data = await getPlayer(username); if (data === null) throw emsg('api.noUser'); else sendPlayerEmbed(interaction, data); }