diff --git a/dist/api.js b/dist/api.js index 935f4fe..98f16eb 100644 --- a/dist/api.js +++ b/dist/api.js @@ -24,7 +24,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.getPlayerInteraction = exports.getPlayer = void 0; const cheerio = __importStar(require("cheerio")); +const discord_js_1 = require("discord.js"); const https_1 = __importDefault(require("https")); +const util_1 = require("./util"); const uniteApiRegex = { //$1 = name, $2 = id ogtitle: /unite api - (.+) \((.*)\)/i, @@ -68,11 +70,14 @@ function getHTML(name) { * 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) { - let metaElems = cheerio.load(html)('meta').toArray(), foundData = { + let $ = cheerio.load(html); + let metaElems = $('meta').toArray(), foundData = { name: "", id: "", + avatar: "", level: "", rank: "", elo: null, @@ -102,7 +107,7 @@ function readHTML(html) { extraLines.push(line); } if (!lines.length) - throw 'Unable to read data, please try again'; + throw (0, util_1.emsg)('Unable to read data, please try again'); //bring the first lines removed back into the data lines = [ ...lines, @@ -116,7 +121,7 @@ function readHTML(html) { if (regex[0].test(line)) { //is master/has elo let regexData = line.match(regex[0]); if (!regexData || regexData.length < 4) - throw 'Unable to read data, please try again'; + throw (0, util_1.emsg)('Unable to read data, please try again'); foundData.level = regexData[1]; foundData.rank = regexData[2]; foundData.elo = regexData[3]; @@ -124,7 +129,7 @@ function readHTML(html) { else { //is not master/has a class let regexData = line.match(regex[1]); if (!regexData || regexData.length < 4) - throw 'Unable to read data, please try again'; + throw (0, util_1.emsg)('Unable to read data, please try again'); foundData.level = regexData[1]; foundData.rank = regexData[2]; foundData.class = regexData[3]; @@ -148,6 +153,8 @@ function readHTML(html) { }); } }); + foundData.avatar = $('.player-card-image img').attr('src') || ""; + foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/'); return foundData; } /** @@ -173,17 +180,32 @@ async function getPlayer(name) { return null; } exports.getPlayer = getPlayer; +async function sendPlayerEmbed(interaction, data) { + let embed = new discord_js_1.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 */ async function getPlayerInteraction(interaction) { let username = interaction.options.getString('username', true); await interaction.deferReply(); let data = await getPlayer(username); if (data === null) - await interaction.editReply('Unable to find user'); + throw (0, util_1.emsg)('Unable to find user'); else - await interaction.editReply('```\n' + JSON.stringify(data, null, 2) + '\n```'); + sendPlayerEmbed(interaction, data); } exports.getPlayerInteraction = getPlayerInteraction; diff --git a/dist/discord.js b/dist/discord.js index 0e6f5c4..501b79c 100644 --- a/dist/discord.js +++ b/dist/discord.js @@ -35,13 +35,13 @@ const commands = [ description: 'cancels the current queue (must have the Manage Messages permission)' }, { - name: 'elo', - description: 'display elo information', + name: 'player', + description: 'display player information', options: [ { type: 3, name: 'username', - description: 'in game name', + description: 'in game name or UniteApi short link', required: true } ] diff --git a/dist/index.js b/dist/index.js index e873226..6d22b89 100644 --- a/dist/index.js +++ b/dist/index.js @@ -24,6 +24,7 @@ const fs = __importStar(require("fs")); const api_1 = require("./api"); const discord_1 = require("./discord"); const queue_1 = require("./queue"); +const util_1 = require("./util"); const CLIENT = new discord_js_1.Client({ intents: [discord_js_1.Intents.FLAGS.GUILDS] }); //init logs with a timestamp console.log(new Date().toISOString() + '\n\n'); @@ -53,17 +54,18 @@ CLIENT.on('interactionCreate', async (interaction) => { await (0, queue_1.readyQueue)(interaction); else if (interaction.commandName === 'cancel') await (0, queue_1.cancelQueue)(interaction); - else if (interaction.commandName === 'queueinfo') - await (0, queue_1.queueInfo)(interaction); - else if (interaction.commandName === 'elo') + else if (interaction.commandName === 'player') await (0, api_1.getPlayerInteraction)(interaction); } catch (e) { - if (typeof e === 'string') { + if (e instanceof util_1.errorMessage) { if (interaction.deferred || interaction.replied) - interaction.editReply(e); + interaction.editReply(e.msg); else - interaction.reply(e); + interaction.reply({ + content: e.msg, + ephemeral: e.ephemeral + }); } else console.error(e); diff --git a/dist/queue.js b/dist/queue.js index d9e250d..77c1bbd 100644 --- a/dist/queue.js +++ b/dist/queue.js @@ -1,186 +1,189 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.queueInfo = exports.cancelQueue = exports.readyQueue = exports.leaveQueue = exports.joinQueue = exports.createQueue = exports.queueContains = void 0; +exports.cancelQueue = exports.readyQueue = exports.leaveQueue = exports.joinQueue = exports.createQueue = exports.queueContains = exports.getAll = exports.getInfo = void 0; const discord_js_1 = require("discord.js"); const util_1 = require("./util"); //maps ChannelID to QueueInfo const QUEUE = new Map(); -/** - * creates the timeout for the queue - * @param interaction - * @returns time timeout identifier - */ -function setQueueTimeout(interaction) { - let channel = getChannel(interaction); - return setTimeout(() => { - QUEUE.delete(channel.id); - channel.send('Queue has been reset due to inactivity'); - }, 5 * 60 * 1000); //5 minutes -} -/** - * get the GuildMember of an interaction - * @param interaction - * @throws string message if it cannot be read - * @returns member - */ -function getMember(interaction) { - let member = interaction.member; - if (!(member instanceof discord_js_1.GuildMember)) - throw 'Unable to retrieve guild member information, please try again'; - return member; -} -/** - * get the TextChannel of an interaction - * @param interaction - * @throws string message if it cannot be read - * @returns member - */ -function getChannel(interaction) { - let channel = interaction.channel; - if (!(channel instanceof discord_js_1.TextChannel)) - throw 'Unable to retrieve text channel information, please try again'; - return channel; -} /** * get the queueInfo of an interaction * @param interaction - * @throws string message if it does not exist + * @throws errorMessage class if it does not exist * @returns queue info */ function getInfo(interaction) { let info = QUEUE.get(interaction.channelId); if (!info) - throw 'There is not an active queue in this channel, type `/queue` to create one'; + throw (0, util_1.emsg)('There is not an active queue in this channel, type `/queue` to create one'); return info; } +exports.getInfo = getInfo; /** * compiles all the get functions above * @param interaction - * @throws string message if it does not exist + * @throws if another get function throws * @returns object containing each */ const getAll = (interaction) => ({ - member: getMember(interaction), - channel: getChannel(interaction), + member: (0, util_1.getMember)(interaction), + channel: (0, util_1.getChannel)(interaction), info: getInfo(interaction) }); +exports.getAll = getAll; /** * checks if the interaction data is already in the queue * @param interaction * @returns boolean */ function queueContains(interaction) { - let { member, info } = getAll(interaction); + let { member, info } = (0, exports.getAll)(interaction); if (info.players.map(m => m.id).includes(member.id)) return true; return false; } exports.queueContains = queueContains; +/** + * creates the timeout for the queue + * @param interaction + * @returns time timeout identifier + */ +function setQueueTimeout(interaction) { + let channel = (0, util_1.getChannel)(interaction); + return setTimeout(() => { + clearQueue(interaction); + channel.send('Queue has been reset due to inactivity'); + }, 5 * 60 * 1000); //5 minutes +} +/** + * updates the rich embed for the current queue + * @param interaction + */ +async function sendQueueEmbed(interaction, closed = false) { + let info = getInfo(interaction), origInteraction = info.initiator.interaction; + let embed = new discord_js_1.MessageEmbed() + .setTitle('Queue') + .setAuthor({ + name: info.initiator.member.displayName, + iconURL: info.initiator.member.displayAvatarURL({ dynamic: true }) + }) + .addField('Team Size', info.teamsize.toString(), true) + .addField('Players Joined', info.players.length.toString(), true) + .setFooter({ text: closed ? 'queue is finished' : 'type /join' }); + if (origInteraction.deferred || origInteraction.replied) + await origInteraction.editReply({ embeds: [embed] }); + else + await origInteraction.reply({ embeds: [embed] }); +} +/** + * sends the list of teams + * @param interaction + */ +async function sendTeamsEmbed(interaction, teams) { + let embed = new discord_js_1.MessageEmbed() + .setTitle('Teams'); + teams.forEach((team, i) => { + team.map(m => m.user.tag); + embed.addField(`Title ${i + 1}`, team.join('\n')); + }); + interaction.reply({ embeds: [embed] }); +} +/** + * sends the list of teams + * @param interaction + */ +async function clearQueue(interaction) { + let info = getInfo(interaction); + sendQueueEmbed(interaction, true); + clearTimeout(info.timeout); + QUEUE.delete(interaction.channelId); +} /** * creates a queue from an interaction * @param interaction - * @throws string message if it cannot be created + * @throws errorMessage class if it cannot be created */ async function createQueue(interaction) { - let member = getMember(interaction), { channelId } = interaction, teamsize = interaction.options.getInteger('teamsize', true); + let member = (0, util_1.getMember)(interaction), { channelId } = interaction, teamsize = interaction.options.getInteger('teamsize', true); if (QUEUE.has(channelId)) - throw 'There is already an active queue in this channel, ' + (queueContains(interaction) ? 'and you are already in it' : 'type `/join` to join'); //and you are already in it + throw (0, util_1.emsg)('There is already an active queue in this channel, ' + (queueContains(interaction) ? 'and you are already in it' : 'type `/join` to join')); QUEUE.set(channelId, { players: [ member ], - initiator: member, + initiator: { + interaction, + member + }, teamsize: teamsize, timeout: setQueueTimeout(interaction) }); - await interaction.reply(`Queue for teams of ${teamsize} has been created, and you have joined`); + sendQueueEmbed(interaction); } exports.createQueue = createQueue; /** * joins a queue from an interaction * @param interaction - * @throws string message if it cannot be joined + * @throws errorMessage class if it cannot be joined */ async function joinQueue(interaction) { - let { member, info } = getAll(interaction); + let { member, info } = (0, exports.getAll)(interaction); if (queueContains(interaction)) - throw 'You are already in the active queue'; + throw (0, util_1.emsg)('You are already in the active queue'); info.players.push(member); clearTimeout(info.timeout); info.timeout = setQueueTimeout(interaction); QUEUE.set(interaction.channelId, info); + sendQueueEmbed(interaction); await interaction.reply('Joined the queue'); } exports.joinQueue = joinQueue; /** * leaves a queue from an interaction * @param interaction - * @throws string message if it cannot be left + * @throws errorMessage class if it cannot be left */ async function leaveQueue(interaction) { - let { member, info } = getAll(interaction); + let { member, info } = (0, exports.getAll)(interaction); if (!queueContains(interaction)) - throw 'You aren\'t in the active queue'; + throw (0, util_1.emsg)('You aren\'t in the active queue'); info.players.splice(info.players.indexOf(member), 1); clearTimeout(info.timeout); info.timeout = setQueueTimeout(interaction); QUEUE.set(interaction.channelId, info); + sendQueueEmbed(interaction); await interaction.reply('Left the queue'); } exports.leaveQueue = leaveQueue; /** * readys a queue from an interaction * @param interaction - * @throws string message if it cannot be readied + * @throws errorMessage class if it cannot be readied */ async function readyQueue(interaction) { - let { member, info } = getAll(interaction), { initiator } = info; - if (member.id !== initiator.id) - throw 'Only the queue initiator can ready the queue'; - //reset queue - QUEUE.delete(interaction.channelId); - if (info.players.filter(m => m.id !== initiator.id).length === 0) - throw 'Nobody signed up for the queue, the queue has been reset'; + let { member, info } = (0, exports.getAll)(interaction), { initiator } = info; + if (member.id !== initiator.member.id) + throw (0, util_1.emsg)('Only the queue initiator can ready the queue'); + clearQueue(interaction); + if (info.players.filter(m => m.id !== initiator.member.id).length === 0) + throw (0, util_1.emsg)('Nobody signed up for the queue, the queue has been reset'); //team data let playerlist = (0, util_1.shuffle)(info.players), teams = []; //fill team data for (let i = 0; i < playerlist.length; i += info.teamsize) teams.push(playerlist.slice(i, i + info.teamsize)); - //convert teams to strings - let teamsStr = []; - teams.forEach((team, i) => { - let str = [`Team ${i + 1}`]; - team.forEach(m => str.push(` ${m.user.tag}`)); - teamsStr.push(str.join('\n')); - }); - await interaction.reply('```\n' + teamsStr.join('\n\n') + '\n```'); + sendTeamsEmbed(interaction, teams); } exports.readyQueue = readyQueue; /** * readys a queue from an interaction * @param interaction - * @throws string message if it cannot be reset + * @throws errorMessage class if it cannot be reset */ async function cancelQueue(interaction) { - let { member, channel } = getAll(interaction); + let { info, member, channel } = (0, exports.getAll)(interaction); if (!member.permissionsIn(channel).has('MANAGE_MESSAGES')) - throw 'You do not have permission to run this command'; - //reset queue - QUEUE.delete(interaction.channelId); + throw (0, util_1.emsg)('You do not have permission to run this command'); + clearQueue(interaction); await interaction.reply('Queue has been reset'); } exports.cancelQueue = cancelQueue; -/** - * sends the queue information from an interaction - * @param interaction - * @throws string message if it cannot be read - */ -async function queueInfo(interaction) { - let info = getInfo(interaction); - await interaction.reply('```' + ` -players: ${info.players.map(p => p.user.tag).join('\n ')} -initiator: ${info.initiator.user.tag} -teamsize: ${info.teamsize} -` + '```'); -} -exports.queueInfo = queueInfo; diff --git a/dist/util.js b/dist/util.js index 9ccfa56..6c15f05 100644 --- a/dist/util.js +++ b/dist/util.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.shuffle = void 0; +exports.getChannel = exports.getMember = exports.emsg = exports.errorMessage = exports.shuffle = void 0; +const discord_js_1 = require("discord.js"); /** * shuffles an array * https://stackoverflow.com/a/2450976/2856416 @@ -22,3 +23,44 @@ function shuffle(array) { return array; } exports.shuffle = shuffle; +class errorMessage { + constructor(msg, ephemeral = true) { + this.msg = msg; + this.ephemeral = ephemeral; + } +} +exports.errorMessage = errorMessage; +/** + * a simple class to contain an error message and related data + * @param msg error message + * @param ephemeral (default=true) + * @returns new errorMessage + */ +const emsg = (msg, ephemeral = true) => new errorMessage(msg, ephemeral); +exports.emsg = emsg; +/** + * get the GuildMember of an interaction + * @param interaction + * @throws errorMessage class if it cannot be read + * @returns member + */ +function getMember(interaction) { + let member = interaction.member; + if (!(member instanceof discord_js_1.GuildMember)) + throw (0, exports.emsg)('Unable to retrieve guild member information, please try again'); + return member; +} +exports.getMember = getMember; +/** + * get the TextChannel of an interaction + * @param interaction + * @throws errorMessage class if it cannot be read + * @returns member + */ +function getChannel(interaction) { + let channel = interaction.channel; + if (!(channel instanceof discord_js_1.TextChannel)) + throw (0, exports.emsg)('Unable to retrieve text channel information, please try again'); + return channel; +} +exports.getChannel = getChannel; diff --git a/src/api.ts b/src/api.ts index 4e5d1e1..ae7cc96 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,8 @@ import * as cheerio from 'cheerio'; -import { CommandInteraction } from 'discord.js'; +import { CommandInteraction, MessageEmbed } from 'discord.js'; import { IncomingMessage } from 'http'; import http from 'https'; +import { emsg } from './util'; const uniteApiRegex = { //$1 = name, $2 = id @@ -16,6 +17,7 @@ const uniteApiRegex = { type uniteApiData = { name: string, id: string, + avatar: string, level: string, rank: string, @@ -71,12 +73,15 @@ function getHTML(name: string): Promise { * 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 metaElems = cheerio.load(html)('meta').toArray(), + let $ = cheerio.load(html) + let metaElems = $('meta').toArray(), foundData: uniteApiData = { name: "", id: "", + avatar: "", level: "", rank: "", @@ -113,7 +118,7 @@ function readHTML(html: string): uniteApiData { } if (!lines.length) - throw 'Unable to read data, please try again'; + throw emsg('Unable to read data, please try again'); //bring the first lines removed back into the data lines = [ @@ -133,7 +138,7 @@ function readHTML(html: string): uniteApiData { let regexData = line.match(regex[0]); if (!regexData || regexData.length < 4) - throw 'Unable to read data, please try again'; + throw emsg('Unable to read data, please try again'); foundData.level = regexData[1]; foundData.rank = regexData[2]; @@ -144,7 +149,7 @@ function readHTML(html: string): uniteApiData { let regexData = line.match(regex[1]); if (!regexData || regexData.length < 4) - throw 'Unable to read data, please try again'; + throw emsg('Unable to read data, please try again'); foundData.level = regexData[1]; foundData.rank = regexData[2]; @@ -179,7 +184,10 @@ function readHTML(html: string): uniteApiData { } - }) + }); + + foundData.avatar = $('.player-card-image img').attr('src') || ""; + foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/'); return foundData; @@ -209,9 +217,27 @@ export async function getPlayer(name: string): Promise { 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); @@ -219,7 +245,7 @@ export async function getPlayerInteraction(interaction: CommandInteraction) { let data = await getPlayer(username); if (data === null) - await interaction.editReply('Unable to find user'); + throw emsg('Unable to find user'); else - await interaction.editReply('```\n'+JSON.stringify(data, null, 2)+'\n```'); + sendPlayerEmbed(interaction, data); } diff --git a/src/discord.ts b/src/discord.ts index 0fd3178..d11abaf 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -33,13 +33,13 @@ const commands = [ description: 'cancels the current queue (must have the Manage Messages permission)' }, { - name: 'elo', - description: 'display elo information', + name: 'player', + description: 'display player information', options: [ { type: 3, //STRING name: 'username', - description: 'in game name', + description: 'in game name or UniteApi short link', required: true } ] diff --git a/src/index.ts b/src/index.ts index 042bc7b..786fc50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,8 @@ import { Client, Intents } from 'discord.js'; import * as fs from 'fs'; import { getPlayerInteraction } from './api'; import { registerCommands } from './discord'; -import { cancelQueue, createQueue, joinQueue, leaveQueue, queueInfo, readyQueue } from './queue'; +import { cancelQueue, createQueue, joinQueue, leaveQueue, readyQueue } from './queue'; +import { errorMessage } from './util'; const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] }); //init logs with a timestamp @@ -39,18 +40,21 @@ CLIENT.on('interactionCreate', async interaction => { await readyQueue(interaction); else if (interaction.commandName === 'cancel') await cancelQueue(interaction); - else if (interaction.commandName === 'queueinfo') - await queueInfo(interaction); - else if (interaction.commandName === 'elo') + else if (interaction.commandName === 'player') await getPlayerInteraction(interaction); } catch (e) { - if (typeof e === 'string') { + if (e instanceof errorMessage) { + if (interaction.deferred || interaction.replied) - interaction.editReply(e); + interaction.editReply(e.msg); else - interaction.reply(e); + interaction.reply({ + content: e.msg, + ephemeral: e.ephemeral + }); + } else console.error(e); } diff --git a/src/queue.ts b/src/queue.ts index 601362c..0ebd120 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -1,70 +1,20 @@ -import { CommandInteraction, GuildMember, TextChannel } from "discord.js"; -import { shuffle } from "./util"; - -type queueInfo = { - players: GuildMember[], - initiator: GuildMember, - teamsize: number, - timeout: NodeJS.Timeout -} +import { CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; +import { emsg, getChannel, getMember, queueInfo, shuffle } from "./util"; //maps ChannelID to QueueInfo const QUEUE = new Map(); -/** - * creates the timeout for the queue - * @param interaction - * @returns time timeout identifier - */ -function setQueueTimeout(interaction: CommandInteraction) { - let channel = getChannel(interaction); - return setTimeout(() => { - QUEUE.delete(channel.id); - channel.send('Queue has been reset due to inactivity'); - }, 5*60*1000) //5 minutes -} - -/** - * get the GuildMember of an interaction - * @param interaction - * @throws string message if it cannot be read - * @returns member - */ -function getMember(interaction: CommandInteraction): GuildMember { - let member = interaction.member; - - if (!(member instanceof GuildMember)) - throw 'Unable to retrieve guild member information, please try again'; - - return member; -} - -/** - * get the TextChannel of an interaction - * @param interaction - * @throws string message if it cannot be read - * @returns member - */ -function getChannel(interaction: CommandInteraction): TextChannel { - let channel = interaction.channel; - - if (!(channel instanceof TextChannel)) - throw 'Unable to retrieve text channel information, please try again'; - - return channel; -} - /** * get the queueInfo of an interaction * @param interaction - * @throws string message if it does not exist + * @throws errorMessage class if it does not exist * @returns queue info */ -function getInfo(interaction: CommandInteraction): queueInfo { +export function getInfo(interaction: CommandInteraction): queueInfo { let info = QUEUE.get(interaction.channelId); if (!info) - throw 'There is not an active queue in this channel, type `/queue` to create one'; + throw emsg('There is not an active queue in this channel, type `/queue` to create one'); return info; } @@ -72,10 +22,10 @@ function getInfo(interaction: CommandInteraction): queueInfo { /** * compiles all the get functions above * @param interaction - * @throws string message if it does not exist + * @throws if another get function throws * @returns object containing each */ -const getAll = (interaction: CommandInteraction) => ({ +export const getAll = (interaction: CommandInteraction) => ({ member: getMember(interaction), channel: getChannel(interaction), info: getInfo(interaction) @@ -97,10 +47,74 @@ export function queueContains(interaction: CommandInteraction): boolean { } +/** + * creates the timeout for the queue + * @param interaction + * @returns time timeout identifier + */ +function setQueueTimeout(interaction: CommandInteraction) { + let channel = getChannel(interaction); + return setTimeout(() => { + clearQueue(interaction); + channel.send('Queue has been reset due to inactivity'); + }, 5*60*1000) //5 minutes +} + +/** + * updates the rich embed for the current queue + * @param interaction + */ +async function sendQueueEmbed(interaction: CommandInteraction, closed: boolean = false) { + let info = getInfo(interaction), + origInteraction = info.initiator.interaction; + + let embed = new MessageEmbed() + .setTitle('Queue') + .setAuthor({ + name: info.initiator.member.displayName, + iconURL: info.initiator.member.displayAvatarURL({dynamic: true}) + }) + .addField('Team Size', info.teamsize.toString(), true) + .addField('Players Joined', info.players.length.toString(), true) + .setFooter({text: closed ? 'queue is finished' : 'type /join'}); + + if (origInteraction.deferred || origInteraction.replied) + await origInteraction.editReply({embeds: [embed]}); + else + await origInteraction.reply({embeds: [embed]}); +} + +/** + * sends the list of teams + * @param interaction + */ +async function sendTeamsEmbed(interaction: CommandInteraction, teams: GuildMember[][]) { + let embed = new MessageEmbed() + .setTitle('Teams'); + + teams.forEach((team, i) => { + team.map(m => m.user.tag); + embed.addField(`Title ${i+1}`, team.join('\n')) + }); + + interaction.reply({embeds: [embed]}); +} + +/** + * sends the list of teams + * @param interaction + */ +async function clearQueue(interaction: CommandInteraction) { + let info = getInfo(interaction); + sendQueueEmbed(interaction, true); + clearTimeout(info.timeout); + QUEUE.delete(interaction.channelId); +} + /** * creates a queue from an interaction * @param interaction - * @throws string message if it cannot be created + * @throws errorMessage class if it cannot be created */ export async function createQueue(interaction: CommandInteraction) { @@ -109,32 +123,35 @@ export async function createQueue(interaction: CommandInteraction) { teamsize = interaction.options.getInteger('teamsize', true); if (QUEUE.has(channelId)) - throw 'There is already an active queue in this channel, ' + (queueContains(interaction) ? 'and you are already in it' : 'type `/join` to join'); //and you are already in it + throw emsg('There is already an active queue in this channel, ' + (queueContains(interaction) ? 'and you are already in it' : 'type `/join` to join')); QUEUE.set(channelId, { players: [ member ], - initiator: member, + initiator: { + interaction, + member + }, teamsize: teamsize, timeout: setQueueTimeout(interaction) }); - await interaction.reply(`Queue for teams of ${teamsize} has been created, and you have joined`); + sendQueueEmbed(interaction); } /** * joins a queue from an interaction * @param interaction - * @throws string message if it cannot be joined + * @throws errorMessage class if it cannot be joined */ export async function joinQueue(interaction: CommandInteraction) { let {member, info} = getAll(interaction); if (queueContains(interaction)) - throw 'You are already in the active queue'; + throw emsg('You are already in the active queue'); info.players.push(member); clearTimeout(info.timeout); @@ -142,6 +159,7 @@ export async function joinQueue(interaction: CommandInteraction) { QUEUE.set(interaction.channelId, info); + sendQueueEmbed(interaction); await interaction.reply('Joined the queue'); } @@ -149,14 +167,14 @@ export async function joinQueue(interaction: CommandInteraction) { /** * leaves a queue from an interaction * @param interaction - * @throws string message if it cannot be left + * @throws errorMessage class if it cannot be left */ export async function leaveQueue(interaction: CommandInteraction) { let {member, info} = getAll(interaction); if (!queueContains(interaction)) - throw 'You aren\'t in the active queue'; + throw emsg('You aren\'t in the active queue'); info.players.splice(info.players.indexOf(member), 1); clearTimeout(info.timeout); @@ -164,6 +182,7 @@ export async function leaveQueue(interaction: CommandInteraction) { QUEUE.set(interaction.channelId, info); + sendQueueEmbed(interaction); await interaction.reply('Left the queue'); } @@ -171,21 +190,20 @@ export async function leaveQueue(interaction: CommandInteraction) { /** * readys a queue from an interaction * @param interaction - * @throws string message if it cannot be readied + * @throws errorMessage class if it cannot be readied */ export async function readyQueue(interaction: CommandInteraction) { let {member, info} = getAll(interaction), {initiator} = info; - if (member.id !== initiator.id) - throw 'Only the queue initiator can ready the queue'; + if (member.id !== initiator.member.id) + throw emsg('Only the queue initiator can ready the queue'); - //reset queue - QUEUE.delete(interaction.channelId); + clearQueue(interaction); - if (info.players.filter(m => m.id !== initiator.id).length === 0) - throw 'Nobody signed up for the queue, the queue has been reset'; + if (info.players.filter(m => m.id !== initiator.member.id).length === 0) + throw emsg('Nobody signed up for the queue, the queue has been reset'); //team data let playerlist: GuildMember[] = shuffle(info.players), @@ -195,52 +213,24 @@ export async function readyQueue(interaction: CommandInteraction) { for (let i = 0; i < playerlist.length; i+= info.teamsize) teams.push(playerlist.slice(i, i+info.teamsize)); - //convert teams to strings - let teamsStr: string[] = []; - teams.forEach((team, i) => { - let str = [`Team ${i+1}`]; - - team.forEach(m => str.push(` ${m.user.tag}`)); - - teamsStr.push(str.join('\n')); - }); - - await interaction.reply('```\n'+teamsStr.join('\n\n')+'\n```'); + sendTeamsEmbed(interaction, teams); } /** * readys a queue from an interaction * @param interaction - * @throws string message if it cannot be reset + * @throws errorMessage class if it cannot be reset */ export async function cancelQueue(interaction: CommandInteraction) { - let {member, channel} = getAll(interaction); + let {info, member, channel} = getAll(interaction); if (!member.permissionsIn(channel).has('MANAGE_MESSAGES')) - throw 'You do not have permission to run this command'; + throw emsg('You do not have permission to run this command'); - //reset queue - QUEUE.delete(interaction.channelId); + clearQueue(interaction); await interaction.reply('Queue has been reset'); } - -/** - * sends the queue information from an interaction - * @param interaction - * @throws string message if it cannot be read - */ -export async function queueInfo(interaction: CommandInteraction) { - - let info = getInfo(interaction); - - await interaction.reply('```'+` -players: ${info.players.map(p => p.user.tag).join('\n ')} -initiator: ${info.initiator.user.tag} -teamsize: ${info.teamsize} -`+'```'); - -} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index b31d19f..f047c28 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import { CommandInteraction, GuildMember, TextChannel } from "discord.js"; /** * shuffles an array @@ -22,3 +23,68 @@ export function shuffle(array: any[]) { return array; } + + + +export class errorMessage { + public msg: string; + public ephemeral: boolean; + + constructor(msg: string, ephemeral: boolean = true) { + this.msg = msg; + this.ephemeral = ephemeral; + } +} + +/** + * a simple class to contain an error message and related data + * @param msg error message + * @param ephemeral (default=true) + * @returns new errorMessage + */ +export const emsg = (msg: string, ephemeral: boolean = true) => new errorMessage(msg, ephemeral); + + + + + + +export type queueInfo = { + players: GuildMember[], + initiator: { + interaction: CommandInteraction, + member: GuildMember + }, + teamsize: number, + timeout: NodeJS.Timeout +} + +/** + * get the GuildMember of an interaction + * @param interaction + * @throws errorMessage class if it cannot be read + * @returns member + */ +export function getMember(interaction: CommandInteraction): GuildMember { + let member = interaction.member; + + if (!(member instanceof GuildMember)) + throw emsg('Unable to retrieve guild member information, please try again'); + + return member; +} + +/** + * get the TextChannel of an interaction + * @param interaction + * @throws errorMessage class if it cannot be read + * @returns member + */ +export function getChannel(interaction: CommandInteraction): TextChannel { + let channel = interaction.channel; + + if (!(channel instanceof TextChannel)) + throw emsg('Unable to retrieve text channel information, please try again'); + + return channel; +} \ No newline at end of file