diff --git a/.gitignore b/.gitignore index 17aef5d..9f19878 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules -token +token.txt +queues.json .DS_Store log \ No newline at end of file diff --git a/scripts/mac-linux/install b/scripts/mac-linux/install index 37996f3..834a135 100755 --- a/scripts/mac-linux/install +++ b/scripts/mac-linux/install @@ -8,11 +8,11 @@ cd ../.. npm install #discord token -[ ! -f "token" ] && touch "token" +[ ! -f "token.txt" ] && touch "token.txt" -if [ ! -s "token" ] +if [ ! -s "token.txt" ] then printf "Enter your discord bot token: " read token - printf $token > "token" + printf $token > "token.txt" fi \ No newline at end of file diff --git a/scripts/windows/install.bat b/scripts/windows/install.bat index 0e98f1e..e10a36b 100644 --- a/scripts/windows/install.bat +++ b/scripts/windows/install.bat @@ -4,12 +4,12 @@ cd %~f0\..\..\..\ @REM discord token -if not exist "token" copy NUL "token" +if not exist "token.txt" copy NUL "token.txt" -for /f %%i in ("token") do set size=%%~zi +for /f %%i in ("token.txt") do set size=%%~zi if %size% equ 0 ( set /p token="Enter your discord bot token: " - echo | set /p=%id% > "token" + echo | set /p=%id% > "token.txt" ) @REM run diff --git a/src/discord.ts b/src/discord.ts index d11abaf..3f3c316 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -5,13 +5,12 @@ import { Routes } from 'discord-api-types/v9'; const commands = [ { name: 'queue', - description: 'create a queue', + description: 'get queue info or initialize a queue for this channel', options: [ { type: 4, //INTEGER name: 'teamsize', description: 'size of each team', - required: true, min_value: 1 } ] @@ -25,12 +24,8 @@ const commands = [ description: 'leave the active queue' }, { - name: 'ready', - description: 'ready the queue and display team info' - }, - { - name: 'cancel', - description: 'cancels the current queue (must have the Manage Messages permission)' + name: 'stop', + description: 'stops the current queue (must have the Manage Messages permission)' }, { name: 'player', diff --git a/src/index.ts b/src/index.ts index 786fc50..7a17465 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { Client, Intents } from 'discord.js'; import * as fs from 'fs'; import { getPlayerInteraction } from './api'; import { registerCommands } from './discord'; -import { cancelQueue, createQueue, joinQueue, leaveQueue, readyQueue } from './queue'; +import { discordInit, QueueCommands } from './queue'; import { errorMessage } from './util'; const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] }); @@ -11,8 +11,8 @@ const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] }); console.log(new Date().toISOString()+'\n\n'); //get token -if (!fs.existsSync('./token')) { - fs.writeFileSync('./token', ''); +if (!fs.existsSync('./token.txt')) { + fs.writeFileSync('./token.txt', ''); console.error('Missing Discord Token, please enter the bot token into the token file'); process.exit(1); } @@ -23,6 +23,7 @@ CLIENT.on('ready', client => { console.log(`Logged in as ${client.user.tag}`); client.guilds.fetch().then(guilds => registerCommands(TOKEN, client.user.id, guilds.map(g => g.id))); + discordInit(client); }); CLIENT.on('interactionCreate', async interaction => { @@ -31,15 +32,13 @@ CLIENT.on('interactionCreate', async interaction => { try { if (interaction.commandName === 'queue') - await createQueue(interaction); + await QueueCommands.queue(interaction); else if (interaction.commandName === 'join') - await joinQueue(interaction); + await QueueCommands.join(interaction); else if (interaction.commandName === 'leave') - await leaveQueue(interaction); - else if (interaction.commandName === 'ready') - await readyQueue(interaction); - else if (interaction.commandName === 'cancel') - await cancelQueue(interaction); + await QueueCommands.leave(interaction); + else if (interaction.commandName === 'stop') + await QueueCommands.stop(interaction); else if (interaction.commandName === 'player') await getPlayerInteraction(interaction); diff --git a/src/queue.ts b/src/queue.ts index a02e7c4..8b550f2 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -1,236 +1,262 @@ -import { CommandInteraction, GuildMember, MessageEmbed } from "discord.js"; -import { emsg, getChannel, getMember, queueInfo, shuffle } from "./util"; +/* TODO -//maps ChannelID to QueueInfo -const QUEUE = new Map(); +join message should contain your current position in the queue, editing it to keep it current +*/ -/** - * get the queueInfo of an interaction - * @param interaction - * @throws errorMessage class if it does not exist - * @returns queue info - */ -export function getInfo(interaction: CommandInteraction): queueInfo { - let info = QUEUE.get(interaction.channelId); +import { Client, CommandInteraction, GuildMember, MessageEmbed, TextChannel } from "discord.js"; +import * as fs from 'fs'; +import { emsg, getChannel, getMember, memberIsModThrow, queueInfo, queueInfoBase } from "./util"; + +//load queues from file +if (!fs.existsSync('./queues')) + fs.writeFileSync('./queues', '{}'); + +const _QUEUE = fs.readFileSync('./queues').toString(), + QUEUE = new Map(); + +try { + + let queueJson = JSON.parse(_QUEUE); + + for (let channelId of queueJson) { + let {teamsize} = queueJson[channelId] as queueInfoBase; + if (teamsize) + QUEUE.set(channelId, { teamsize, players: [] }) + } + +} catch(e) {} + +function SaveQueue() { + + let queueJson = Object.fromEntries(QUEUE), + queueFileJson: {[keys: string]: queueInfoBase} = {}; + + for (let channelId in QUEUE.keys()) + queueFileJson[channelId] = { teamsize: queueJson[channelId].teamsize }; + + fs.writeFileSync('./queues.json', JSON.stringify(queueFileJson, null, 2)); + +} + +async function checkQueue(channel: TextChannel) { + let info = QUEUE.get(channel.id); if (!info) - throw emsg('There is not an active queue in this channel, type `/queue` to create one'); - - return info; -} - -/** - * compiles all the get functions above - * @param interaction - * @throws if another get function throws - * @returns object containing each - */ -export const getAll = (interaction: CommandInteraction) => ({ - member: getMember(interaction), - channel: getChannel(interaction), - info: getInfo(interaction) - }); - -/** - * checks if the interaction data is already in the queue - * @param interaction - * @returns boolean - */ -export function queueContains(interaction: CommandInteraction): boolean { - - let {member, info} = getAll(interaction); - - if (info.players.map(m=>m.id).includes(member.id)) - return true; - - return false; - -} - -/** - * 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; + return; - 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 (info.players.length > info.teamsize) { - if (origInteraction.deferred || origInteraction.replied) - await origInteraction.editReply({embeds: [embed]}); - else - await origInteraction.reply({embeds: [embed]}); + let team = info.players.splice(0, info.teamsize).map(m => m.toString()); + + let embed = new MessageEmbed() + .setTitle('Team') + .setDescription(team.join('\n')); + + await channel.send({embeds: [embed]}); + + } + } +namespace Queue { -/** - * sends the list of teams - * @param interaction - */ -async function sendTeamsEmbed(interaction: CommandInteraction, teams: GuildMember[][]) { - let embed = new MessageEmbed() - .setTitle('Teams'); + export function create(channelId: string, teamsize: number) { + if (!QUEUE.has(channelId)) { + QUEUE.set(channelId, {teamsize, players: []}); + SaveQueue(); + } + } - teams.forEach((team, i) => { - team.map(m => m.user.tag); - embed.addField(`Team ${i+1}`, team.join('\n')) - }); + export function remove(channelId: string) { + if (QUEUE.has(channelId)) { + QUEUE.delete(channelId); + SaveQueue(); + } + } - interaction.reply({embeds: [embed]}); -} + export function addPlayer(channelId: string, member: GuildMember) { + if (QUEUE.has(channelId)) { + QUEUE.delete(channelId); + } + } -/** - * 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 errorMessage class if it cannot be created - */ -export async function createQueue(interaction: CommandInteraction) { - - let member = getMember(interaction), - {channelId} = interaction, - teamsize = interaction.options.getInteger('teamsize', true); - - if (QUEUE.has(channelId)) - 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: { - interaction, - member - }, - teamsize: teamsize, - timeout: setQueueTimeout(interaction) - }); - - sendQueueEmbed(interaction); + export function removePlayer(channelId: string, member: GuildMember) { + if (QUEUE.has(channelId)) { + QUEUE.delete(channelId); + } + } } -/** - * joins a queue from an interaction - * @param interaction - * @throws errorMessage class if it cannot be joined - */ -export async function joinQueue(interaction: CommandInteraction) { +SaveQueue(); - let {member, info} = getAll(interaction); +export async function discordInit(client: Client) { - if (queueContains(interaction)) - throw emsg('You are already in the active queue'); + for (let channelId in QUEUE.keys()) { - info.players.push(member); - clearTimeout(info.timeout); - info.timeout = setQueueTimeout(interaction) + let info = QUEUE.get(channelId), + channel = await client.channels.fetch(channelId); - QUEUE.set(interaction.channelId, info); + if (!info) { //no idea what could cause this but TS complains + Queue.remove(channelId); + continue; + } - sendQueueEmbed(interaction); - await interaction.reply('Joined the queue'); - -} - -/** - * leaves a queue from an interaction - * @param interaction - * @throws errorMessage class if it cannot be left - */ -export async function leaveQueue(interaction: CommandInteraction) { - - let {member, info} = getAll(interaction); - - if (!queueContains(interaction)) - throw 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'); - -} - -/** - * readys a queue from an interaction - * @param interaction - * @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.member.id) - throw emsg('Only the queue initiator can ready the queue'); - - clearQueue(interaction); - - 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), - teams: GuildMember[][] = []; - - //fill team data - for (let i = 0; i < playerlist.length; i+= info.teamsize) - teams.push(playerlist.slice(i, i+info.teamsize)); - - sendTeamsEmbed(interaction, teams); - -} - -/** - * readys a queue from an interaction - * @param interaction - * @throws errorMessage class if it cannot be reset - */ -export async function cancelQueue(interaction: CommandInteraction) { - - let {info, member, channel} = getAll(interaction); - - if (!member.permissionsIn(channel).has('MANAGE_MESSAGES')) - throw emsg('You do not have permission to run this command'); - - clearQueue(interaction); - - await interaction.reply('Queue has been reset'); + if (!channel || !(channel instanceof TextChannel)) { + console.error(`Unable to find channel ${channelId} for teams of ${info?.teamsize}`); + Queue.remove(channelId); + continue; + } + + channel.send('The bot has just restarted and anybody in the queues have been reset') + + } + +} +export namespace QueueCommands { + + /** + * get the queueInfo of an interaction + * @param interaction + * @throws errorMessage class if it does not exist + * @returns queue info + */ + function getInfo(interaction: CommandInteraction): queueInfo { + let info = QUEUE.get(interaction.channelId); + + if (!info) + throw emsg('There is not an active queue in this channel, type `/queue` to create one'); + + return info; + } + + /** + * compiles all the get functions above + * @param interaction + * @throws if another get function throws + * @returns object containing each + */ + const getAll = (interaction: CommandInteraction) => ({ + member: getMember(interaction), + channel: getChannel(interaction), + info: getInfo(interaction) + }); + + /** + * checks if the interaction data is already in the queue + * @param interaction + * @returns boolean + */ + export function queueContains(interaction: CommandInteraction): boolean { + + let {member, info} = getAll(interaction); + + if (info.players.map(m=>m.id).includes(member.id)) + return true; + + return false; + + } + + /** + * creates a queue from an interaction + * @param interaction + * @throws errorMessage class if it cannot be left + */ + export function queueCreate(interaction: CommandInteraction) { + memberIsModThrow(interaction); + + let {channelId} = interaction, + teamsize = interaction.options.getInteger('teamsize', true); + + if (QUEUE.has(channelId)) + throw emsg(`There is already an active queue in this channel for teams of ${QUEUE.get(channelId)?.teamsize}`); + + Queue.create(channelId, teamsize); + + interaction.reply(`A queue for teams of ${teamsize} has been started`) + + } + + /** + * creates a queue from an interaction + * @param interaction + * @throws errorMessage class if it cannot be left + */ + export async function queue(interaction: CommandInteraction) { + + let teamsize = interaction.options.getInteger('teamsize'); + + if (teamsize) { + queueCreate(interaction); + return; + } + + let info = getInfo(interaction); + + let embed = new MessageEmbed() + .setTitle('Active Queue') + .addField('Team Size', info.teamsize.toString(), true) + .addField('Players Joined', info.players.length.toString(), true) + .setFooter({text: 'type /join'}); + + await interaction.reply({embeds: [embed], ephemeral: true}); + + } + + /** + * stops a queue from an interaction + * @param interaction + * @throws errorMessage class if it cannot be joined + */ + export async function stop(interaction: CommandInteraction) { + memberIsModThrow(interaction); + + QUEUE.delete(interaction.channelId); + + await interaction.reply('Queue has been reset'); + + } + + /** + * joins a queue from an interaction + * @param interaction + * @throws errorMessage class if it cannot be readied + */ + export async function join(interaction: CommandInteraction) { + + let {member, info, channel} = getAll(interaction); + + if (queueContains(interaction)) + throw emsg('You are already in the queue'); + + info.players.push(member); + + QUEUE.set(interaction.channelId, info); + + await interaction.reply('Joined the queue'); + + checkQueue(channel); + + } + + /** + * leaves a queue from an interaction + * @param interaction + * @throws errorMessage class if it cannot be reset + */ + export async function leave(interaction: CommandInteraction) { + + let {member, info} = getAll(interaction); + + if (!queueContains(interaction)) + throw emsg('You aren\'t in the queue'); + + info.players.splice(info.players.indexOf(member), 1); + + QUEUE.set(interaction.channelId, info); + + await interaction.reply('Left the queue'); + + } } diff --git a/src/util.ts b/src/util.ts index 18f19fe..a9e324d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -46,14 +46,11 @@ export const emsg = (msg: string, ephemeral: boolean = true) => new errorMessage -export type queueInfo = { - players: GuildMember[], - initiator: { - interaction: CommandInteraction, - member: GuildMember - }, - teamsize: number, - timeout: NodeJS.Timeout +export interface queueInfoBase { + teamsize: number +} +export interface queueInfo extends queueInfoBase{ + players: GuildMember[] } /** @@ -84,4 +81,15 @@ export function getChannel(interaction: CommandInteraction): TextChannel { throw emsg('Unable to retrieve text channel information, please try again'); return channel; -} \ No newline at end of file +} + + +export function memberIsMod(interaction: CommandInteraction): boolean { + let member = getMember(interaction); + return member.permissionsIn(interaction.channelId).has('MANAGE_MESSAGES'); +} + +export function memberIsModThrow(interaction: CommandInteraction) { + if (!memberIsMod(interaction)) + throw emsg('Member is not a moderator'); +}