Compare commits

..

44 Commits

Author SHA1 Message Date
54709a898b updated api 2022-03-06 11:20:36 -06:00
d6d75abd87 removed extra comment 2022-02-24 10:41:15 -06:00
0ff3a57747 updated readme 2022-02-15 16:05:16 -06:00
9fb3bf6941 renamed index to main lol 2022-02-15 16:01:08 -06:00
2956e24684 comments 2022-02-15 15:50:36 -06:00
c2447b180e fixed type definition errors 2022-02-15 15:24:45 -06:00
a3103f73c3 removed debugger 2022-02-15 15:09:27 -06:00
5348ab487b separated type definitions and util functions 2022-02-15 15:06:34 -06:00
513a9d1582 added more strings 2022-02-14 14:56:47 -06:00
458d3f3d76 added embed support to lang 2022-02-14 14:37:10 -06:00
28fb9ed3c8 deleted empty files 2022-02-14 12:13:50 -06:00
8503f274bd forgot to build 2022-02-13 21:42:04 -06:00
f5ab2b4297 eslint 2022-02-13 21:39:29 -06:00
983f742f0d useless change 2022-02-13 21:04:40 -06:00
84482be886 updated tsconfig 2022-02-13 20:11:37 -06:00
3d69a9375e i thought i already updated this 2022-02-13 19:56:53 -06:00
39deb13f5a typo 2022-02-13 19:53:41 -06:00
ab76d7edbc updated lang 2022-02-13 19:51:30 -06:00
343ed640ae updated lang strings 2022-02-13 19:35:16 -06:00
b1d01b414e register commands for new guilds 2022-02-13 19:29:10 -06:00
17643dc730 updated lang strings 2022-02-13 19:23:28 -06:00
f225bf924a updated lang strings 2022-02-13 19:15:55 -06:00
9da9650f92 created lang file 2022-02-13 18:56:12 -06:00
a1a387880c updated gitignore 2022-02-13 18:55:47 -06:00
943512d354 updated packages 2022-02-13 18:50:24 -06:00
edb786ec04 finished(?) renaming commands 2022-02-11 13:51:16 -06:00
525904de5a updated command name 2022-02-11 13:50:45 -06:00
4fdb233bd6 yet another dumb mistake 2022-02-11 13:44:00 -06:00
00c3fa8219 info for later 2022-02-11 13:29:32 -06:00
610ca41195 more command renaming 2022-02-11 13:29:23 -06:00
827dafbb99 more dumb issues 2022-02-11 13:29:17 -06:00
3ed56b7927 renamed/moved commands 2022-02-11 13:18:17 -06:00
e444167f07 dumb mistake 2022-02-11 13:08:27 -06:00
fcdf8dea06 shifted dependency 2022-02-07 14:44:54 -06:00
148d93aaab updated file names 2022-02-07 14:39:05 -06:00
81ea71328a updated builds 2022-02-07 14:38:55 -06:00
cf453957e9 remade queue logic 2022-02-01 13:47:52 -06:00
dc0cc6685d typo 2022-01-31 21:20:12 -06:00
c04cd2ef12 i hate windows 2022-01-31 21:18:43 -06:00
763892443d there were some extra newlines 2022-01-31 19:46:09 -06:00
9646d9f941 updated version number and dependencies 2022-01-31 19:35:28 -06:00
fe866050d5 improved messages and general cleanup 2022-01-31 19:33:39 -06:00
00d04c787d added inactivity timeout 2022-01-31 17:47:43 -06:00
f0283145dc added cancel command 2022-01-31 17:39:06 -06:00
30 changed files with 4202 additions and 483 deletions

56
.eslintrc.json Normal file
View File

@@ -0,0 +1,56 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./tsconfig.eslint.json"] },
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/strict-boolean-expressions": [
2,
{
"allowString" : false,
"allowNumber" : false
}
],
/* important */
"prefer-const": "error",
"quotes": ["error", "single"],
"block-scoped-var": "error",
"camelcase": "error",
"consistent-this": ["error", "that"],
"no-else-return": "error",
"no-eq-null": "error",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implied-eval": "error",
"no-invalid-this": "error",
"require-await": "error",
"yoda": "error",
"semi": ["error", "always"],
"semi-style": ["error", "last"],
/* less important */
"no-unreachable-loop": "error",
"no-unused-private-class-members": "error",
"no-use-before-define": "error",
"no-unmodified-loop-condition": "error",
"no-duplicate-imports": "error",
"no-promise-executor-return": "error",
"no-self-compare": "error",
"no-constructor-return": "error",
"no-template-curly-in-string": "error",
"array-callback-return": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error"
},
"ignorePatterns": ["src/**/*.test.ts", "src/frontend/generated/*"]
}

8
.gitignore vendored
View File

@@ -1,4 +1,6 @@
node_modules
token
.DS_Store .DS_Store
log .vscode
log
node_modules
queues.json
token.txt

View File

@@ -44,6 +44,8 @@ Download and install [Node.js](https://nodejs.org/en/)
## Starting ## Starting
**Important**: the bot's invite needs permission to `Create Commands` and `Send Messages`
1. Open the `scripts` folder 1. Open the `scripts` folder
2. Open the folder that corresponds with your operating system 2. Open the folder that corresponds with your operating system
3. Run the `start` file 3. Run the `start` file

88
dist/api.js vendored
View File

@@ -23,8 +23,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getPlayerInteraction = exports.getPlayer = void 0; exports.getPlayerInteraction = exports.getPlayer = void 0;
/*eslint prefer-const: "error"*/
const cheerio = __importStar(require("cheerio")); const cheerio = __importStar(require("cheerio"));
const https_1 = __importDefault(require("https")); const https_1 = __importDefault(require("https"));
const main_1 = require("./util/main");
const Lang = __importStar(require("./lang"));
const uniteApiRegex = { const uniteApiRegex = {
//$1 = name, $2 = id //$1 = name, $2 = id
ogtitle: /unite api - (.+) \((.*)\)/i, ogtitle: /unite api - (.+) \((.*)\)/i,
@@ -68,25 +71,28 @@ function getHTML(name) {
* interprets the html from getHTML() * interprets the html from getHTML()
* @param name name of player * @param name name of player
* @returns player data from site * @returns player data from site
* @throws errorMessage class if the request fails
*/ */
function readHTML(html) { function readHTML(html) {
let metaElems = cheerio.load(html)('meta').toArray(), foundData = { const $ = cheerio.load(html), foundData = {
name: "", name: '',
id: "", id: '',
level: "", avatar: '',
rank: "", level: '',
rank: '',
elo: null, elo: null,
class: null, class: null,
battles: "", battles: '',
wins: "", wins: '',
winrate: "" winrate: ''
}; };
let metaElems = $('meta').toArray();
//filter down to just ones named "og:..." //filter down to just ones named "og:..."
metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:')); metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:'));
metaElems.forEach(el => { metaElems.forEach(el => {
let attr = el.attribs; const attr = el.attribs;
if (attr.property === 'og:title') { if (attr.property === 'og:title') {
let data = uniteApiRegex.ogtitle.exec(attr.content); const data = uniteApiRegex.ogtitle.exec(attr.content);
if (data !== null && data.length >= 3) { if (data !== null && data.length >= 3) {
foundData.name = data[1]; foundData.name = data[1];
foundData.id = data[2]; foundData.id = data[2];
@@ -94,15 +100,16 @@ function readHTML(html) {
} }
else if (attr.property === 'og:description') { else if (attr.property === 'og:description') {
//all lines //all lines
let lines = attr.content.split('\n').map(l => l.trim()), extraLines = []; let lines = attr.content.split('\n').map(l => l.trim());
const extraLines = [];
//ensure first line is correct //ensure first line is correct
while (lines.length && !/pok.mon unite/i.test(lines[0])) { while ((lines.length > 0) && !/pok.mon unite/i.test(lines[0])) {
let line = lines.shift(); const line = lines.shift();
if (line) if (line !== undefined)
extraLines.push(line); extraLines.push(line);
} }
if (!lines.length) if (lines.length === 0)
throw 'Unable to read data, please try again'; throw (0, main_1.emsg)('Unable to read data, please try again');
//bring the first lines removed back into the data //bring the first lines removed back into the data
lines = [ lines = [
...lines, ...lines,
@@ -111,20 +118,19 @@ function readHTML(html) {
//first line //first line
{ {
//will be only text after "pokemon unite:" //will be only text after "pokemon unite:"
let line = lines[0].split(':').slice(1).join(':').trim(); const line = lines[0].split(':').slice(1).join(':').trim(), regex = uniteApiRegex.ogdescription;
let regex = uniteApiRegex.ogdescription;
if (regex[0].test(line)) { //is master/has elo if (regex[0].test(line)) { //is master/has elo
let regexData = line.match(regex[0]); const regexData = line.match(regex[0]);
if (!regexData || regexData.length < 4) if (!regexData || regexData.length < 4)
throw 'Unable to read data, please try again'; throw (0, main_1.emsg)('Unable to read data, please try again');
foundData.level = regexData[1]; foundData.level = regexData[1];
foundData.rank = regexData[2]; foundData.rank = regexData[2];
foundData.elo = regexData[3]; foundData.elo = regexData[3];
} }
else { //is not master/has a class else { //is not master/has a class
let regexData = line.match(regex[1]); const regexData = line.match(regex[1]);
if (!regexData || regexData.length < 4) if (!regexData || regexData.length < 4)
throw 'Unable to read data, please try again'; throw (0, main_1.emsg)('Unable to read data, please try again');
foundData.level = regexData[1]; foundData.level = regexData[1];
foundData.rank = regexData[2]; foundData.rank = regexData[2];
foundData.class = regexData[3]; foundData.class = regexData[3];
@@ -133,7 +139,7 @@ function readHTML(html) {
lines.shift(); lines.shift();
//rest of lines //rest of lines
lines.forEach(line => { lines.forEach(line => {
let split = line.split(':').map(l => l.trim()), key = split[0].toLowerCase().replace(/[^\w]/g, ''), value = split[1]; const split = line.split(':').map(l => l.trim()), key = split[0].toLowerCase().replace(/[^\w]/g, ''), value = split[1];
switch (key) { switch (key) {
case 'battles': case 'battles':
foundData.battles = value; foundData.battles = value;
@@ -148,6 +154,9 @@ function readHTML(html) {
}); });
} }
}); });
const imgSrc = $('.player-card-image img').attr('src');
foundData.avatar = imgSrc !== undefined ? imgSrc : '';
foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/');
return foundData; return foundData;
} }
/** /**
@@ -156,7 +165,7 @@ function readHTML(html) {
* @returns boolean, valid or invalid * @returns boolean, valid or invalid
*/ */
function verifyData(data) { function verifyData(data) {
if (data.id.length) if (data.id.length > 0)
return true; return true;
return false; return false;
} }
@@ -166,24 +175,43 @@ function verifyData(data) {
* @returns player data * @returns player data
*/ */
async function getPlayer(name) { async function getPlayer(name) {
let html = await getHTML(name); const html = await getHTML(name), data = readHTML(html);
let data = readHTML(html);
if (verifyData(data)) if (verifyData(data))
return data; return data;
return null; return null;
} }
exports.getPlayer = getPlayer; exports.getPlayer = getPlayer;
async function sendPlayerEmbed(interaction, data) {
let eloStr;
if (data.elo !== null)
eloStr = `(${data.elo})`;
else
eloStr = `Class ${data.class}`;
await interaction.editReply(Lang.getEmbed('api.player', {
name: data.name,
id: data.id,
nameEncoded: encodeURIComponent(data.name),
avatar: data.avatar,
level: data.level,
rank: data.rank,
elo: eloStr,
battles: data.battles,
wins: data.wins,
winrate: data.winrate
}));
}
/** /**
* calls getPlayer() with the name from the interaction * calls getPlayer() with the name from the interaction
* @param interaction discord interaction * @param interaction discord interaction
* @throws errorMessage class if the user cannot be found
*/ */
async function getPlayerInteraction(interaction) { async function getPlayerInteraction(interaction) {
let username = interaction.options.getString('username', true); const username = interaction.options.getString('username', true);
await interaction.deferReply(); await interaction.deferReply();
let data = await getPlayer(username); const data = await getPlayer(username);
if (data === null) if (data === null)
await interaction.editReply('Unable to find user'); throw (0, main_1.emsg)('api.noUser');
else else
await interaction.editReply('```\n' + JSON.stringify(data, null, 2) + '\n```'); sendPlayerEmbed(interaction, data);
} }
exports.getPlayerInteraction = getPlayerInteraction; exports.getPlayerInteraction = getPlayerInteraction;

30
dist/discord.js vendored
View File

@@ -1,22 +1,32 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.registerCommands = void 0; exports.registerCommands = void 0;
/* eslint-disable camelcase */
const rest_1 = require("@discordjs/rest"); const rest_1 = require("@discordjs/rest");
const v9_1 = require("discord-api-types/v9"); const v9_1 = require("discord-api-types/v9");
// list of commands to register with discord // list of commands to register with discord
const commands = [ const commands = [
{ {
name: 'queue', name: 'open',
description: 'create a queue', description: 'open a queue for this channel',
options: [ options: [
{ {
type: 4, type: 4,
name: 'teamsize', name: 'teamsize',
description: 'size of each team', description: 'size of each team',
min_value: 1,
required: true required: true
} }
] ]
}, },
{
name: 'close',
description: 'close the queue for this channel'
},
{
name: 'queue',
description: 'view queue info'
},
{ {
name: 'join', name: 'join',
description: 'join the active queue' description: 'join the active queue'
@@ -26,26 +36,19 @@ const commands = [
description: 'leave the active queue' description: 'leave the active queue'
}, },
{ {
name: 'ready', name: 'player',
description: 'ready the queue and display team info' description: 'display player information',
},
{
name: 'queueinfo',
description: 'get info of the current queue'
},
{
name: 'elo',
description: 'display elo information',
options: [ options: [
{ {
type: 3, type: 3,
name: 'username', name: 'username',
description: 'in game name', description: 'in game name or UniteApi short link',
required: true required: true
} }
] ]
} }
]; ];
/*commandNames = commands.map(c => c.name);*/
/** /**
* register/reload commands on guild(s) * register/reload commands on guild(s)
* @param token discord bot token * @param token discord bot token
@@ -59,7 +62,6 @@ async function registerCommands(token, clientId, guildIds) {
for (let i = 0; i < guildIds.length; i++) { for (let i = 0; i < guildIds.length; i++) {
try { try {
await rest.put(v9_1.Routes.applicationGuildCommands(clientId, guildIds[i]), { body: commands }); await rest.put(v9_1.Routes.applicationGuildCommands(clientId, guildIds[i]), { body: commands });
console.log(`[${guildIds[i]}] registered command`);
} }
catch (error) { catch (error) {
console.error(error); console.error(error);

146
dist/lang.js vendored Normal file
View File

@@ -0,0 +1,146 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEmbed = exports.get = exports.setLang = void 0;
const lang_1 = require("./util/lang");
const LANG = {
en: {
main: {
login: 'Logged in as {user}'
},
discord: {
botRestart: 'The bot has just restarted, anybody previously in the queue has been reset',
create: 'A queue for teams of {teamsize} has been created',
close: 'Queue has been closed',
join: 'Joined the queue',
leave: 'Left the queue',
team: {
embed: true,
title: 'Team',
description: '{team}'
},
queue: {
embed: true,
title: 'Active Queue',
fields: [
{
name: 'Team Size',
value: '{teamsize}'
},
{
name: 'Players Joined',
value: '{playercount}'
}
],
footer: 'type `/join`'
}
},
api: {
player: {
embed: true,
title: '{name} ({id})',
url: 'https://uniteapi.dev/p/{nameEncoded}',
timestamp: true,
thumbnail: '{avatar}',
description: [
'Level {level}',
'{rank} {elo}',
'',
'**Battles** {battles}',
'**Wins** {wins}',
'**Win Rate** {winrate}'
]
}
},
error: {
main: {
missingToken: 'Missing Discord Token, please enter the bot token into the token file'
},
discord: {
noQueue: 'There is not an active queue in this channel, type `/open` to create one',
noChannel: 'Unable to find channel {channelId} for teams of {teamsize}',
noCreate: 'There is already an active queue in this channel for teams of {teamsize}',
inQueue: 'You are already in the queue',
notInQueue: 'You aren\'t in the queue',
notMod: 'Member is not a moderator'
},
general: {
noMember: 'Unable to retrieve guild member information, please try again',
noChannel: 'Unable to retrieve text channel information, please try again'
},
api: {
noUser: 'Unable to find user'
}
}
}
};
/* MAIN */
let LANGID = 'en';
if (!(LANGID in LANG))
throw 'language id does not exist';
function setLang(langid) {
if (langid in LANG)
LANGID = langid;
else
throw 'language id does not exist';
}
exports.setLang = setLang;
/**
* reads language json (just strings)
* @param id ex: discord.error.noActiveQueue
* @param args list of key/value pairs to represent template values
* @returns language value, defaults to `id` parameter
*/
function get(id, args = {}) {
const keySpl = id.split('.').map(k => k.trim()).filter(k => k);
let finding = LANG[LANGID];
for (const key of keySpl) {
if (key in finding) {
const found = finding[key];
if (typeof found === 'string')
return (0, lang_1.template)(found, args);
if (found.embed === true)
return (0, lang_1.embedObjStr)(found, args, id);
finding = found;
}
else
break;
}
return id;
}
exports.get = get;
/**
* reads language json as an object (could be embed or just string)
* @param id ex: discord.error.noActiveQueue
* @param args list of key/value pairs to represent template values
* @param otherOptions values to be passed through to the return value
* @returns language value, defaults to `id` parameter
*/
function getEmbed(id, args = {}, otherOptions = {}) {
const embedData = {
...otherOptions,
embeds: []
};
const keySpl = id.split('.').map(k => k.trim()).filter(k => k);
let finding = LANG[LANGID];
for (const key of keySpl) {
if (key in finding) {
const found = finding[key];
if (typeof found === 'string') {
embedData.content = (0, lang_1.template)(found, args);
break;
}
if (found.embed === true) {
const embedObj = found, { content } = embedObj, embed = (0, lang_1.embedObjEmbed)(embedObj, args);
embedData.embeds.push(embed);
if (content !== undefined)
embedData.content = content;
return embedData;
}
finding = found;
}
else
break;
}
return embedData;
}
exports.getEmbed = getEmbed;

View File

@@ -23,45 +23,67 @@ const discord_js_1 = require("discord.js");
const fs = __importStar(require("fs")); const fs = __importStar(require("fs"));
const api_1 = require("./api"); const api_1 = require("./api");
const discord_1 = require("./discord"); const discord_1 = require("./discord");
const Lang = __importStar(require("./lang"));
const queue_1 = require("./queue"); const queue_1 = require("./queue");
const main_1 = require("./util/main");
const CLIENT = new discord_js_1.Client({ intents: [discord_js_1.Intents.FLAGS.GUILDS] }); const CLIENT = new discord_js_1.Client({ intents: [discord_js_1.Intents.FLAGS.GUILDS] });
//init logs with a timestamp //init logs with a timestamp
console.log(new Date().toISOString() + '\n\n'); console.log(new Date().toISOString() + '\n\n');
//get token //get token
if (!fs.existsSync('./token')) { if (!fs.existsSync('./token.txt')) {
fs.writeFileSync('./token', ''); fs.writeFileSync('./token.txt', '');
console.error('Missing Discord Token, please enter the bot token into the token file'); console.error(Lang.get('error.main.missingToken'));
process.exit(1); process.exit(1);
} }
const TOKEN = fs.readFileSync('./token').toString(); const TOKEN = fs.readFileSync('./token.txt').toString();
//discord connections //discord connections
CLIENT.on('ready', client => { CLIENT.on('ready', client => {
console.log(`Logged in as ${client.user.tag}`); console.log(Lang.get('main.login', {
user: client.user.tag
}));
client.guilds.fetch().then(guilds => (0, discord_1.registerCommands)(TOKEN, client.user.id, guilds.map(g => g.id))); client.guilds.fetch().then(guilds => (0, discord_1.registerCommands)(TOKEN, client.user.id, guilds.map(g => g.id)));
(0, queue_1.discordInit)(client);
});
CLIENT.on('guildCreate', guild => {
if (guild.client.user)
(0, discord_1.registerCommands)(TOKEN, guild.client.user.id, guild.id);
}); });
CLIENT.on('interactionCreate', async (interaction) => { CLIENT.on('interactionCreate', async (interaction) => {
if (!interaction.isCommand()) if (!interaction.isCommand())
return; return;
try { try {
if (interaction.commandName === 'queue') switch (interaction.commandName) {
await (0, queue_1.createQueue)(interaction); //mod commands
else if (interaction.commandName === 'join') case 'open':
await (0, queue_1.joinQueue)(interaction); await queue_1.QueueCommands.open(interaction);
else if (interaction.commandName === 'leave') break;
await (0, queue_1.leaveQueue)(interaction); case 'close':
else if (interaction.commandName === 'ready') await queue_1.QueueCommands.close(interaction);
await (0, queue_1.readyQueue)(interaction); break;
else if (interaction.commandName === 'queueinfo') //general commands
await (0, queue_1.queueInfo)(interaction); case 'queue':
else if (interaction.commandName === 'elo') await queue_1.QueueCommands.queue(interaction);
await (0, api_1.getPlayerInteraction)(interaction); break;
case 'join':
await queue_1.QueueCommands.join(interaction);
break;
case 'leave':
await queue_1.QueueCommands.leave(interaction);
break;
case 'player':
await (0, api_1.getPlayerInteraction)(interaction);
break;
}
} }
catch (e) { catch (e) {
if (typeof e === 'string') { if (e instanceof main_1.errorMessage) {
if (interaction.deferred || interaction.replied) if (interaction.deferred || interaction.replied)
interaction.editReply(e); interaction.editReply(e.msg);
else else
interaction.reply(e); interaction.reply({
content: e.msg,
ephemeral: e.ephemeral
});
} }
else else
console.error(e); console.error(e);

254
dist/queue.js vendored
View File

@@ -1,132 +1,206 @@
"use strict"; "use strict";
/* TODO
join message should contain your current position in the queue, editing it to keep it current
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.queueInfo = exports.readyQueue = exports.leaveQueue = exports.joinQueue = exports.createQueue = exports.queueContains = void 0; exports.QueueCommands = exports.queueContains = exports.discordInit = exports.queueRemove = exports.queueCreate = void 0;
const discord_js_1 = require("discord.js"); const discord_js_1 = require("discord.js");
const util_1 = require("./util"); const fs = __importStar(require("fs"));
//maps ChannelID to QueueInfo const Lang = __importStar(require("./lang"));
const QUEUE = new Map(); const discord_1 = require("./util/discord");
/** const main_1 = require("./util/main");
* get the GuildMember of an interaction //load queues from file`
* @param interaction if (!fs.existsSync('./queues.json'))
* @throws string message if it cannot be read fs.writeFileSync('./queues.json', '{}');
* @returns member const _QUEUE = fs.readFileSync('./queues.json').toString(), QUEUE = new Map();
*/ try {
function getMember(interaction) { const queueJson = JSON.parse(_QUEUE);
let member = interaction.member; for (const channelId in queueJson) {
if (!(member instanceof discord_js_1.GuildMember)) const { teamsize } = queueJson[channelId];
throw 'Unable to retrieve guild member information, please try again'; if (teamsize !== 0)
return member; QUEUE.set(channelId, { teamsize, players: [] });
}
} }
catch (e) {
//do nothing
}
function SaveQueue() {
const queueJson = Object.fromEntries(QUEUE), queueFileJson = {};
for (const channelId of QUEUE.keys())
queueFileJson[channelId] = { teamsize: queueJson[channelId].teamsize };
fs.writeFileSync('./queues.json', JSON.stringify(queueFileJson, null, 2));
}
async function checkQueue(channel) {
const info = QUEUE.get(channel.id);
if (!info)
return;
if (info.players.length >= info.teamsize) {
const team = info.players.splice(0, info.teamsize).map(m => m.toString());
await channel.send(Lang.getEmbed('discord.team', { team: team.join('\n') }));
}
}
function queueCreate(channelId, teamsize) {
if (!QUEUE.has(channelId)) {
QUEUE.set(channelId, { teamsize, players: [] });
SaveQueue();
}
}
exports.queueCreate = queueCreate;
function queueRemove(channelId) {
if (QUEUE.has(channelId)) {
QUEUE.delete(channelId);
SaveQueue();
}
}
exports.queueRemove = queueRemove;
SaveQueue();
async function discordInit(client) {
for (const channelId of QUEUE.keys()) {
const info = QUEUE.get(channelId), channel = await client.channels.fetch(channelId);
if (!info) {
queueRemove(channelId);
continue;
}
if (!channel || !(channel instanceof discord_js_1.TextChannel)) {
console.error(Lang.get('error.discord.noChannel'), {
channelId,
teamsize: info.teamsize
});
queueRemove(channelId);
continue;
}
channel.send(Lang.get('discord.botRestart'));
}
}
exports.discordInit = discordInit;
/** /**
* get the queueInfo of an interaction * get the queueInfo of an interaction
* @param interaction * @param interaction
* @throws string message if it does not exist * @throws errorMessage class if it does not exist
* @returns queue info * @returns queue info
*/ */
function getInfo(interaction) { function getInfo(interaction) {
let info = QUEUE.get(interaction.channelId); const info = QUEUE.get(interaction.channelId);
if (!info) if (!info)
throw 'There is not an active queue in this channel, type `/queue` to create one'; throw (0, main_1.emsg)('discord.noQueue');
return info; return info;
} }
/**
* compiles all the get functions above
* @param interaction
* @throws if another get function throws
* @returns object containing each
*/
const getAll = (interaction) => ({
member: (0, discord_1.getMember)(interaction),
channel: (0, discord_1.getChannel)(interaction),
info: getInfo(interaction)
});
/** /**
* checks if the interaction data is already in the queue * checks if the interaction data is already in the queue
* @param interaction * @param interaction
* @returns boolean * @returns boolean
*/ */
function queueContains(interaction) { function queueContains(interaction) {
let member = getMember(interaction), info = getInfo(interaction); const { member, info } = getAll(interaction);
if (info.players.map(m => m.id).includes(member.id)) if (info.players.map(m => m.id).includes(member.id))
return true; return true;
return false; return false;
} }
exports.queueContains = queueContains; exports.queueContains = queueContains;
/** /**
* creates a queue from an interaction * opens a queue
* @param interaction * @param interaction
* @throws string message if it cannot be created * @throws errorMessage class if it cannot be left
*/ */
async function createQueue(interaction) { function open(interaction) {
let { channelId } = interaction, member = getMember(interaction), teamsize = interaction.options.getInteger('teamsize', true); (0, discord_1.memberIsModThrow)(interaction);
if (QUEUE.has(channelId)) const { channelId } = interaction, teamsize = interaction.options.getInteger('teamsize', true);
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 const existing = QUEUE.get(channelId);
QUEUE.set(channelId, { if (existing)
players: [ throw (0, main_1.emsg)(Lang.get('error.discord.noCreate', {
member teamsize: existing.teamsize.toString()
], }));
initiator: member, queueCreate(channelId, teamsize);
teamsize: teamsize interaction.reply(Lang.get('discord.create', {
}); teamsize: teamsize.toString()
await interaction.reply(`Queue for teams of ${teamsize} has been created, and you have joined`); }));
} }
exports.createQueue = createQueue;
/** /**
* joins a queue from an interaction * closes a queue
* @param interaction * @param interaction
* @throws string message if it cannot be joined * @throws errorMessage class if it cannot be joined
*/ */
async function joinQueue(interaction) { async function close(interaction) {
let member = getMember(interaction), info = getInfo(interaction); (0, discord_1.memberIsModThrow)(interaction);
QUEUE.delete(interaction.channelId);
await interaction.reply(Lang.get('discord.close'));
}
/**
* gives info about the queue
* @param interaction
* @throws errorMessage class if it cannot be left
*/
async function queue(interaction) {
const info = getInfo(interaction);
await interaction.reply(Lang.getEmbed('discord.queue', {
teamsize: info.teamsize.toString(),
playercount: info.players.length.toString(),
}, {
ephemeral: true
}));
}
/**
* joins a queue
* @param interaction
* @throws errorMessage class if it cannot be readied
*/
async function join(interaction) {
const { member, info, channel } = getAll(interaction);
if (queueContains(interaction)) if (queueContains(interaction))
throw 'You are already in the active queue'; throw (0, main_1.emsg)('discord.inQueue');
info.players.push(member); info.players.push(member);
QUEUE.set(interaction.channelId, info); QUEUE.set(interaction.channelId, info);
await interaction.reply('Joined the queue'); await interaction.reply(Lang.get('discord.join'));
checkQueue(channel);
} }
exports.joinQueue = joinQueue;
/** /**
* leaves a queue from an interaction * leaves a queue
* @param interaction * @param interaction
* @throws string message if it cannot be left * @throws errorMessage class if it cannot be reset
*/ */
async function leaveQueue(interaction) { async function leave(interaction) {
let member = getMember(interaction), info = getInfo(interaction); const { member, info } = getAll(interaction);
if (!queueContains(interaction)) if (!queueContains(interaction))
throw 'You aren\'t in the active queue'; throw (0, main_1.emsg)('discord.notInQueue');
info.players.splice(info.players.indexOf(member), 1); info.players.splice(info.players.indexOf(member), 1);
QUEUE.set(interaction.channelId, info); QUEUE.set(interaction.channelId, info);
await interaction.reply('Left the queue'); await interaction.reply(Lang.get('discord.leave'));
} }
exports.leaveQueue = leaveQueue; exports.QueueCommands = {
/** open,
* readys a queue from an interaction close,
* @param interaction queue,
* @throws string message if it cannot be readied join,
*/ leave
async function readyQueue(interaction) { };
let member = getMember(interaction), info = getInfo(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';
//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```');
}
exports.readyQueue = readyQueue;
/**
* 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;

75
dist/util.js vendored
View File

@@ -1,12 +1,34 @@
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.shuffle = void 0; exports.memberIsModThrow = exports.memberIsMod = exports.getChannel = exports.getMember = exports.emsg = exports.errorMessage = exports.shuffle = void 0;
const discord_js_1 = require("discord.js");
const Lang = __importStar(require("./lang"));
/** /**
* shuffles an array * shuffles an array
* https://stackoverflow.com/a/2450976/2856416 * https://stackoverflow.com/a/2450976/2856416
* @param array an array * @param array an array
* @returns an array but shuffled * @returns an array but shuffled
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function shuffle(array) { function shuffle(array) {
let currentIndex = array.length, randomIndex; let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle... // While there remain elements to shuffle...
@@ -22,3 +44,54 @@ function shuffle(array) {
return array; return array;
} }
exports.shuffle = shuffle; 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(Lang.get(`error.${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) {
const member = interaction.member;
if (!(member instanceof discord_js_1.GuildMember))
throw (0, exports.emsg)('general.noMember');
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) {
const channel = interaction.channel;
if (!(channel instanceof discord_js_1.TextChannel))
throw (0, exports.emsg)('general.noChannel');
return channel;
}
exports.getChannel = getChannel;
function memberIsMod(interaction) {
const member = getMember(interaction);
return member.permissionsIn(interaction.channelId).has('MANAGE_MESSAGES');
}
exports.memberIsMod = memberIsMod;
function memberIsModThrow(interaction) {
if (!memberIsMod(interaction))
throw (0, exports.emsg)('discord.notMod');
}
exports.memberIsModThrow = memberIsModThrow;

45
dist/util/discord.js vendored Normal file
View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.memberIsModThrow = exports.memberIsMod = exports.getChannel = exports.getMember = void 0;
const discord_js_1 = require("discord.js");
const main_1 = require("./main");
/**
* get the GuildMember of an interaction
* @throws errorMessage class if it cannot be read
*/
function getMember(interaction) {
const member = interaction.member;
if (!(member instanceof discord_js_1.GuildMember))
throw (0, main_1.emsg)('general.noMember');
return member;
}
exports.getMember = getMember;
/**
* get the TextChannel of an interaction
* @throws errorMessage class if it cannot be read
*/
function getChannel(interaction) {
const channel = interaction.channel;
if (!(channel instanceof discord_js_1.TextChannel))
throw (0, main_1.emsg)('general.noChannel');
return channel;
}
exports.getChannel = getChannel;
/**
* get the TextChannel of an interaction
* @throws errorMessage class if the Member cannot be read
*/
function memberIsMod(interaction) {
const member = getMember(interaction);
return member.permissionsIn(interaction.channelId).has('MANAGE_MESSAGES');
}
exports.memberIsMod = memberIsMod;
/**
* get the TextChannel of an interaction
* @throws errorMessage class if the Member cannot be read or if Member is not a mod
*/
function memberIsModThrow(interaction) {
if (!memberIsMod(interaction))
throw (0, main_1.emsg)('discord.notMod');
}
exports.memberIsModThrow = memberIsModThrow;

119
dist/util/lang.js vendored Normal file
View File

@@ -0,0 +1,119 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.embedObjEmbed = exports.embedObjStr = exports.resolveColor = exports.bigString = exports.template = void 0;
const discord_js_1 = require("discord.js");
/**
*
* @param str
* @param args
* @returns
*/
function template(str, args) {
return str.replace(/{\w+}/g, str => {
const key = str.substring(1, str.length - 1);
if (key in args)
return args[key];
return key;
});
}
exports.template = template;
/**
* converts bigString to string
*/
function bigString(bigStr) {
if (Array.isArray(bigStr))
return bigStr.join('\n');
return bigStr;
}
exports.bigString = bigString;
/**
* converts Hex Color string to an RGB array
*/
function resolveColor(color) {
color = color.replace(/[^0-9a-f]/gi, '');
const colorNum = [0, 0, 0];
if (color.length === 3 || color.length === 6) {
const colorSpl = /([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/.exec(color);
if (!colorSpl)
return colorNum;
for (let i = 0; i < colorSpl.length && i < colorNum.length; i++)
colorNum[i] = parseInt(colorSpl[i], 16);
}
return colorNum;
}
exports.resolveColor = resolveColor;
/**
* converts embedObj to a string if applicable
* @param fallback the string to use if no valid strings can be found
*/
function embedObjStr(embedObj, args = {}, fallback = '') {
if (embedObj.content !== undefined)
return template(bigString(embedObj.content), args);
if (embedObj.description !== undefined)
return template(bigString(embedObj.description), args);
return fallback;
}
exports.embedObjStr = embedObjStr;
/**
* converts embedObj to Discord.MessageEmbed
*/
function embedObjEmbed(embedObj, args = {}) {
const embed = new discord_js_1.MessageEmbed(), { author, color, description, fields, footer, image, thumbnail, timestamp, title, url } = embedObj;
if (author !== undefined) {
let authorFix;
if (typeof author === 'string')
authorFix = {
name: template(author, args)
};
else {
const { name, icon, url } = author;
authorFix = {
name: template(name, args)
};
if (icon !== undefined)
authorFix.icon = template(icon, args);
if (url !== undefined)
authorFix.url = template(url, args);
}
embed.setAuthor(authorFix);
}
if (footer !== undefined) {
let footerFix;
if (typeof footer === 'string')
footerFix = {
text: template(footer, args)
};
else {
const { text, icon } = footer;
footerFix = {
text: template(text, args)
};
if (icon !== undefined)
footerFix.icon = template(icon, args);
}
embed.setFooter(footerFix);
}
if (color !== undefined)
embed.setColor(resolveColor(template(color, args)));
if (description !== undefined)
embed.setDescription(template(bigString(description), args));
if (image !== undefined)
embed.setImage(template(image, args));
if (thumbnail !== undefined)
embed.setThumbnail(template(thumbnail, args));
if (title !== undefined)
embed.setTitle(template(title, args));
if (url !== undefined)
embed.setURL(template(url, args));
if (timestamp === true)
embed.setTimestamp();
else if (typeof timestamp === 'string')
embed.setTimestamp(new Date(template(timestamp, args)));
else if (timestamp !== false)
embed.setTimestamp(timestamp);
fields?.forEach(field => {
embed.addField(template(field.name, args), template(bigString(field.value), args), field.inline);
});
return embed;
}
exports.embedObjEmbed = embedObjEmbed;

63
dist/util/main.js vendored Normal file
View File

@@ -0,0 +1,63 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.emsg = exports.errorMessage = exports.shuffle = void 0;
const Lang = __importStar(require("../lang"));
/**
* shuffles an array
* https://stackoverflow.com/a/2450976/2856416
* @param array an array
* @returns an array but shuffled
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function shuffle(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]
];
}
return array;
}
exports.shuffle = shuffle;
/**
* use the emsg() function instead
*/
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(Lang.get(`error.${msg}`), ephemeral);
exports.emsg = emsg;

2554
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{ {
"name": "1800queue", "name": "1800queue",
"version": "0.0.1", "version": "1.0.0",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "node .", "start": "node .",
@@ -30,7 +30,9 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.13", "@types/node": "^17.0.17",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"discord-api-types": "^0.26.1",
"npm-watch": "^0.11.0", "npm-watch": "^0.11.0",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.5" "typescript": "^4.5.5"
@@ -38,7 +40,6 @@
"dependencies": { "dependencies": {
"@discordjs/rest": "^0.3.0", "@discordjs/rest": "^0.3.0",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
"discord-api-types": "^0.26.1",
"discord.js": "^13.6.0" "discord.js": "^13.6.0"
} }
} }

View File

@@ -8,11 +8,11 @@ cd ../..
npm install npm install
#discord token #discord token
[ ! -f "token" ] && touch "token" [ ! -f "token.txt" ] && touch "token.txt"
if [ ! -s "token" ] if [ ! -s "token.txt" ]
then then
printf "Enter your discord bot token: " printf "Enter your discord bot token: "
read token read token
printf $token > "token" printf $token > "token.txt"
fi fi

View File

@@ -3,14 +3,14 @@
@REM change to ccurrent directory @REM change to ccurrent directory
cd %~f0\..\..\..\ cd %~f0\..\..\..\
@REM run
npm install
@REM discord token @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 ( if %size% equ 0 (
set /p token="Enter your discord bot token: " set /p token="Enter your discord bot token: "
echo | set /p=%id% > "token" echo | set /p=%id% > "token.txt"
) )
@REM run
npm install

View File

@@ -1,7 +1,9 @@
/*eslint prefer-const: "error"*/
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { IncomingMessage } from 'http'; import http, { IncomingMessage } from 'http';
import http from 'https'; import { emsg } from './util/main';
import * as Lang from './lang';
const uniteApiRegex = { const uniteApiRegex = {
//$1 = name, $2 = id //$1 = name, $2 = id
@@ -11,20 +13,7 @@ const uniteApiRegex = {
/lv\.(\d+) (\w+) \((\d+)\)/i, //master /lv\.(\d+) (\w+) \((\d+)\)/i, //master
/lv\.(\d+) (\w+): class (\d+)/i //other /lv\.(\d+) (\w+): class (\d+)/i //other
] ]
} };
type uniteApiData = {
name: string,
id: 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 * gets the html of the uniteApi page for the player
@@ -37,8 +26,8 @@ function getHTML(name: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const init = { const opts = {
host: 'uniteapi.dev', host: '147.182.215.216',
path: `/p/${encodeURIComponent(name)}`, path: `/p/${encodeURIComponent(name)}`,
method: 'GET', method: 'GET',
}; };
@@ -60,7 +49,7 @@ function getHTML(name: string): Promise<string> {
}); });
}; };
const req = http.request(init, callback); const req = http.request(opts, callback);
req.end(); req.end();
}); });
@@ -71,30 +60,34 @@ function getHTML(name: string): Promise<string> {
* interprets the html from getHTML() * interprets the html from getHTML()
* @param name name of player * @param name name of player
* @returns player data from site * @returns player data from site
* @throws errorMessage class if the request fails
*/ */
function readHTML(html: string): uniteApiData { function readHTML(html: string): uniteApiData {
let metaElems = cheerio.load(html)('meta').toArray(), const $ = cheerio.load(html),
foundData: uniteApiData = { foundData: uniteApiData = {
name: "", name: '',
id: "", id: '',
avatar: '',
level: "", level: '',
rank: "", rank: '',
elo: null, elo: null,
class: null, class: null,
battles: "", battles: '',
wins: "", wins: '',
winrate: "" winrate: ''
}; };
let metaElems = $('meta').toArray();
//filter down to just ones named "og:..." //filter down to just ones named "og:..."
metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:')); metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:'));
metaElems.forEach(el => { metaElems.forEach(el => {
let attr = el.attribs; const attr = el.attribs;
if (attr.property === 'og:title') { if (attr.property === 'og:title') {
let data = uniteApiRegex.ogtitle.exec(attr.content); const data = uniteApiRegex.ogtitle.exec(attr.content);
if (data !== null && data.length >= 3) { if (data !== null && data.length >= 3) {
foundData.name = data[1]; foundData.name = data[1];
foundData.id = data[2]; foundData.id = data[2];
@@ -102,18 +95,18 @@ function readHTML(html: string): uniteApiData {
} else if (attr.property === 'og:description') { } else if (attr.property === 'og:description') {
//all lines //all lines
let lines = attr.content.split('\n').map(l => l.trim()), let lines = attr.content.split('\n').map(l => l.trim());
extraLines: string[] = []; const extraLines: string[] = [];
//ensure first line is correct //ensure first line is correct
while (lines.length && !/pok.mon unite/i.test(lines[0])) { while ((lines.length > 0) && !/pok.mon unite/i.test(lines[0])) {
let line = lines.shift(); const line = lines.shift();
if (line) if (line !== undefined)
extraLines.push(line); extraLines.push(line);
} }
if (!lines.length) if (lines.length === 0)
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 //bring the first lines removed back into the data
lines = [ lines = [
@@ -124,16 +117,15 @@ function readHTML(html: string): uniteApiData {
//first line //first line
{ {
//will be only text after "pokemon unite:" //will be only text after "pokemon unite:"
let line = lines[0].split(':').slice(1).join(':').trim(); const line = lines[0].split(':').slice(1).join(':').trim(),
regex = uniteApiRegex.ogdescription;
let regex = uniteApiRegex.ogdescription;
if (regex[0].test(line)) { //is master/has elo if (regex[0].test(line)) { //is master/has elo
let regexData = line.match(regex[0]); const regexData = line.match(regex[0]);
if (!regexData || regexData.length < 4) 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.level = regexData[1];
foundData.rank = regexData[2]; foundData.rank = regexData[2];
@@ -141,10 +133,10 @@ function readHTML(html: string): uniteApiData {
} else { //is not master/has a class } else { //is not master/has a class
let regexData = line.match(regex[1]); const regexData = line.match(regex[1]);
if (!regexData || regexData.length < 4) 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.level = regexData[1];
foundData.rank = regexData[2]; foundData.rank = regexData[2];
@@ -159,7 +151,7 @@ function readHTML(html: string): uniteApiData {
//rest of lines //rest of lines
lines.forEach(line => { lines.forEach(line => {
let split = line.split(':').map(l => l.trim()), const split = line.split(':').map(l => l.trim()),
key = split[0].toLowerCase().replace(/[^\w]/g, ''), key = split[0].toLowerCase().replace(/[^\w]/g, ''),
value = split[1]; value = split[1];
@@ -179,7 +171,12 @@ function readHTML(html: string): uniteApiData {
} }
}) });
const imgSrc = $('.player-card-image img').attr('src');
foundData.avatar = imgSrc !== undefined ? imgSrc : '';
foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/');
return foundData; return foundData;
@@ -191,7 +188,7 @@ function readHTML(html: string): uniteApiData {
* @returns boolean, valid or invalid * @returns boolean, valid or invalid
*/ */
function verifyData(data: uniteApiData): boolean { function verifyData(data: uniteApiData): boolean {
if (data.id.length) if (data.id.length > 0)
return true; return true;
return false; return false;
} }
@@ -202,24 +199,47 @@ function verifyData(data: uniteApiData): boolean {
* @returns player data * @returns player data
*/ */
export async function getPlayer(name: string): Promise<uniteApiData|null> { export async function getPlayer(name: string): Promise<uniteApiData|null> {
let html = await getHTML(name); const html = await getHTML(name),
let data = readHTML(html); data = readHTML(html);
if (verifyData(data)) if (verifyData(data))
return data; return data;
return null; return null;
} }
async function sendPlayerEmbed(interaction: CommandInteraction, data: uniteApiData) {
let eloStr: string;
if (data.elo !== null)
eloStr = `(${data.elo})`;
else
eloStr = `Class ${data.class}`;
await interaction.editReply(Lang.getEmbed('api.player', {
name: data.name,
id: data.id,
nameEncoded: encodeURIComponent(data.name),
avatar: data.avatar,
level: data.level,
rank: data.rank,
elo: eloStr,
battles: data.battles,
wins: data.wins,
winrate: data.winrate
}));
}
/** /**
* calls getPlayer() with the name from the interaction * calls getPlayer() with the name from the interaction
* @param interaction discord interaction * @param interaction discord interaction
* @throws errorMessage class if the user cannot be found
*/ */
export async function getPlayerInteraction(interaction: CommandInteraction) { export async function getPlayerInteraction(interaction: CommandInteraction) {
let username = interaction.options.getString('username', true); const username = interaction.options.getString('username', true);
await interaction.deferReply(); await interaction.deferReply();
let data = await getPlayer(username); const data = await getPlayer(username);
if (data === null) if (data === null)
await interaction.editReply('Unable to find user'); throw emsg('api.noUser');
else else
await interaction.editReply('```\n'+JSON.stringify(data, null, 2)+'\n```'); sendPlayerEmbed(interaction, data);
} }

View File

@@ -1,20 +1,31 @@
/* eslint-disable camelcase */
import { REST } from '@discordjs/rest'; import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9'; import { Routes } from 'discord-api-types/v9';
// list of commands to register with discord // list of commands to register with discord
const commands = [ const commands = [
{ {
name: 'queue', name: 'open',
description: 'create a queue', description: 'open a queue for this channel',
options: [ options: [
{ {
type: 4, //INTEGER type: 4, //INTEGER
name: 'teamsize', name: 'teamsize',
description: 'size of each team', description: 'size of each team',
min_value: 1,
required: true required: true
} }
] ]
}, },
{
name: 'close',
description: 'close the queue for this channel'
},
{
name: 'queue',
description: 'view queue info'
},
{ {
name: 'join', name: 'join',
description: 'join the active queue' description: 'join the active queue'
@@ -24,26 +35,19 @@ const commands = [
description: 'leave the active queue' description: 'leave the active queue'
}, },
{ {
name: 'ready', name: 'player',
description: 'ready the queue and display team info' description: 'display player information',
},
{
name: 'queueinfo',
description: 'get info of the current queue'
},
{
name: 'elo',
description: 'display elo information',
options: [ options: [
{ {
type: 3, //STRING type: 3, //STRING
name: 'username', name: 'username',
description: 'in game name', description: 'in game name or UniteApi short link',
required: true required: true
} }
] ]
} }
]; ];
/*commandNames = commands.map(c => c.name);*/
/** /**
* register/reload commands on guild(s) * register/reload commands on guild(s)
@@ -64,7 +68,6 @@ export async function registerCommands(token: string, clientId: string, guildIds
Routes.applicationGuildCommands(clientId, guildIds[i]), Routes.applicationGuildCommands(clientId, guildIds[i]),
{ body: commands }, { body: commands },
); );
console.log(`[${guildIds[i]}] registered command`);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@@ -1,57 +0,0 @@
import { Client, Intents } from 'discord.js';
import * as fs from 'fs';
import { getPlayerInteraction } from './api';
import { registerCommands } from './discord';
import { createQueue, joinQueue, leaveQueue, queueInfo, readyQueue } from './queue';
const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] });
//init logs with a timestamp
console.log(new Date().toISOString()+'\n\n');
//get token
if (!fs.existsSync('./token')) {
fs.writeFileSync('./token', '');
console.error('Missing Discord Token, please enter the bot token into the token file');
process.exit(1);
}
const TOKEN = fs.readFileSync('./token').toString();
//discord connections
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)));
});
CLIENT.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
try {
if (interaction.commandName === 'queue')
await createQueue(interaction);
else if (interaction.commandName === 'join')
await joinQueue(interaction);
else if (interaction.commandName === 'leave')
await leaveQueue(interaction);
else if (interaction.commandName === 'ready')
await readyQueue(interaction);
else if (interaction.commandName === 'queueinfo')
await queueInfo(interaction);
else if (interaction.commandName === 'elo')
await getPlayerInteraction(interaction);
} catch (e) {
if (typeof e === 'string') {
if (interaction.deferred || interaction.replied)
interaction.editReply(e);
else
interaction.reply(e);
} else console.error(e);
}
});
CLIENT.login(TOKEN);

185
src/lang.ts Normal file
View File

@@ -0,0 +1,185 @@
import { embedObjEmbed, embedObjStr, template } from './util/lang';
const LANG: LangObjWhole = {
en: {
main: {
login: 'Logged in as {user}'
},
discord: {
botRestart: 'The bot has just restarted, anybody previously in the queue has been reset',
create: 'A queue for teams of {teamsize} has been created',
close: 'Queue has been closed',
join: 'Joined the queue',
leave: 'Left the queue',
team: {
embed: true,
title: 'Team',
description: '{team}'
},
queue: {
embed: true,
title: 'Active Queue',
fields: [
{
name: 'Team Size',
value: '{teamsize}'
},
{
name: 'Players Joined',
value: '{playercount}'
}
],
footer: 'type `/join`'
}
},
api: {
player: {
embed: true,
title: '{name} ({id})',
url: 'https://uniteapi.dev/p/{nameEncoded}',
timestamp: true,
thumbnail: '{avatar}',
description: [
'Level {level}',
'{rank} {elo}',
'',
'**Battles** {battles}',
'**Wins** {wins}',
'**Win Rate** {winrate}'
]
}
},
error: {
main: {
missingToken: 'Missing Discord Token, please enter the bot token into the token file'
},
discord: {
noQueue: 'There is not an active queue in this channel, type `/open` to create one',
noChannel: 'Unable to find channel {channelId} for teams of {teamsize}',
noCreate: 'There is already an active queue in this channel for teams of {teamsize}',
inQueue: 'You are already in the queue',
notInQueue: 'You aren\'t in the queue',
notMod: 'Member is not a moderator'
},
general: {
noMember: 'Unable to retrieve guild member information, please try again',
noChannel: 'Unable to retrieve text channel information, please try again'
},
api: {
noUser: 'Unable to find user'
}
}
}
};
/* MAIN */
let LANGID = 'en';
if (!(LANGID in LANG))
throw 'language id does not exist';
export function setLang(langid: string) {
if (langid in LANG)
LANGID = langid;
else
throw 'language id does not exist';
}
/**
* reads language json (just strings)
* @param id ex: discord.error.noActiveQueue
* @param args list of key/value pairs to represent template values
* @returns language value, defaults to `id` parameter
*/
export function get(id: string, args: basicObjectStr = {}): string {
const keySpl = id.split('.').map(k => k.trim()).filter(k => k);
let finding = LANG[LANGID];
for (const key of keySpl) {
if (key in finding) {
const found = finding[key];
if (typeof found === 'string')
return template(found, args);
if (found.embed === true)
return embedObjStr(found as embedObj, args, id);
finding = found as LangObj;
} else
break;
}
return id;
}
/**
* reads language json as an object (could be embed or just string)
* @param id ex: discord.error.noActiveQueue
* @param args list of key/value pairs to represent template values
* @param otherOptions values to be passed through to the return value
* @returns language value, defaults to `id` parameter
*/
export function getEmbed(id: string, args: basicObjectStr = {}, otherOptions: basicObject = {}): embedData {
const embedData: embedData = {
...otherOptions,
embeds: []
};
const keySpl = id.split('.').map(k => k.trim()).filter(k => k);
let finding = LANG[LANGID];
for (const key of keySpl) {
if (key in finding) {
const found = finding[key];
if (typeof found === 'string') {
embedData.content = template(found, args);
break;
}
if (found.embed === true) {
const embedObj = found as embedObj,
{content} = embedObj,
embed = embedObjEmbed(embedObj, args);
embedData.embeds.push(embed);
if (content !== undefined)
embedData.content = content;
return embedData;
}
finding = found as LangObj;
} else
break;
}
return embedData;
}

85
src/main.ts Normal file
View File

@@ -0,0 +1,85 @@
import { Client, Intents } from 'discord.js';
import * as fs from 'fs';
import { getPlayerInteraction } from './api';
import { registerCommands } from './discord';
import * as Lang from './lang';
import { discordInit, QueueCommands } from './queue';
import { errorMessage } from './util/main';
const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] });
//init logs with a timestamp
console.log(new Date().toISOString()+'\n\n');
//get token
if (!fs.existsSync('./token.txt')) {
fs.writeFileSync('./token.txt', '');
console.error(Lang.get('error.main.missingToken'));
process.exit(1);
}
const TOKEN = fs.readFileSync('./token.txt').toString();
//discord connections
CLIENT.on('ready', client => {
console.log(Lang.get('main.login', {
user: client.user.tag
}));
client.guilds.fetch().then(guilds =>
registerCommands(TOKEN, client.user.id, guilds.map(g => g.id)));
discordInit(client);
});
CLIENT.on('guildCreate', guild => {
if (guild.client.user)
registerCommands(TOKEN, guild.client.user.id, guild.id);
});
CLIENT.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
try {
switch (interaction.commandName) {
//mod commands
case 'open':
await QueueCommands.open(interaction);
break;
case 'close':
await QueueCommands.close(interaction);
break;
//general commands
case 'queue':
await QueueCommands.queue(interaction);
break;
case 'join':
await QueueCommands.join(interaction);
break;
case 'leave':
await QueueCommands.leave(interaction);
break;
case 'player':
await getPlayerInteraction(interaction);
break;
}
} catch (e) {
if (e instanceof errorMessage) {
if (interaction.deferred || interaction.replied)
interaction.editReply(e.msg);
else
interaction.reply({
content: e.msg,
ephemeral: e.ephemeral
});
} else console.error(e);
}
});
CLIENT.login(TOKEN);

View File

@@ -1,46 +1,129 @@
import { info } from "console"; import { Client, CommandInteraction, TextChannel } from 'discord.js';
import { CommandInteraction, GuildMember } from "discord.js"; import * as fs from 'fs';
import { shuffle } from "./util"; import * as Lang from './lang';
import { getChannel, getMember, memberIsModThrow } from './util/discord';
import { emsg } from './util/main';
type queueInfo = { //load queues from file`
players: GuildMember[], if (!fs.existsSync('./queues.json'))
initiator: GuildMember, fs.writeFileSync('./queues.json', '{}');
teamsize: number
const _QUEUE = fs.readFileSync('./queues.json').toString(),
QUEUE = new Map<string, queueInfo>();
try {
const queueJson = JSON.parse(_QUEUE);
for (const channelId in queueJson) {
const {teamsize} = queueJson[channelId] as queueInfoBase;
if (teamsize !== 0)
QUEUE.set(channelId, { teamsize, players: [] });
}
} catch(e) {
//do nothing
} }
//maps ChannelID to QueueInfo function SaveQueue() {
const QUEUE = new Map<string, queueInfo>();
/** const queueJson = Object.fromEntries(QUEUE),
* get the GuildMember of an interaction queueFileJson: {[keys: string]: queueInfoBase} = {};
* @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)) for (const channelId of QUEUE.keys())
throw 'Unable to retrieve guild member information, please try again'; queueFileJson[channelId] = { teamsize: queueJson[channelId].teamsize };
fs.writeFileSync('./queues.json', JSON.stringify(queueFileJson, null, 2));
return member; }
async function checkQueue(channel: TextChannel) {
const info = QUEUE.get(channel.id);
if (!info)
return;
if (info.players.length >= info.teamsize) {
const team = info.players.splice(0, info.teamsize).map(m => m.toString());
await channel.send(Lang.getEmbed('discord.team', { team: team.join('\n') }));
}
}
export function queueCreate(channelId: string, teamsize: number) {
if (!QUEUE.has(channelId)) {
QUEUE.set(channelId, {teamsize, players: []});
SaveQueue();
}
}
export function queueRemove(channelId: string) {
if (QUEUE.has(channelId)) {
QUEUE.delete(channelId);
SaveQueue();
}
}
SaveQueue();
export async function discordInit(client: Client) {
for (const channelId of QUEUE.keys()) {
const info = QUEUE.get(channelId),
channel = await client.channels.fetch(channelId);
if (!info) {
queueRemove(channelId);
continue;
}
if (!channel || !(channel instanceof TextChannel)) {
console.error(Lang.get('error.discord.noChannel'), {
channelId,
teamsize: info.teamsize
});
queueRemove(channelId);
continue;
}
channel.send(Lang.get('discord.botRestart'));
}
} }
/** /**
* get the queueInfo of an interaction * get the queueInfo of an interaction
* @param interaction * @param interaction
* @throws string message if it does not exist * @throws errorMessage class if it does not exist
* @returns queue info * @returns queue info
*/ */
function getInfo(interaction: CommandInteraction): queueInfo { function getInfo(interaction: CommandInteraction): queueInfo {
let info = QUEUE.get(interaction.channelId); const info = QUEUE.get(interaction.channelId);
if (!info) if (!info)
throw 'There is not an active queue in this channel, type `/queue` to create one'; throw emsg('discord.noQueue');
return info; 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 * checks if the interaction data is already in the queue
* @param interaction * @param interaction
@@ -48,8 +131,7 @@ function getInfo(interaction: CommandInteraction): queueInfo {
*/ */
export function queueContains(interaction: CommandInteraction): boolean { export function queueContains(interaction: CommandInteraction): boolean {
let member = getMember(interaction), const {member, info} = getAll(interaction);
info = getInfo(interaction);
if (info.players.map(m=>m.id).includes(member.id)) if (info.players.map(m=>m.id).includes(member.id))
return true; return true;
@@ -59,128 +141,109 @@ export function queueContains(interaction: CommandInteraction): boolean {
} }
/** /**
* creates a queue from an interaction * opens a queue
* @param interaction * @param interaction
* @throws string message if it cannot be created * @throws errorMessage class if it cannot be left
*/ */
export async function createQueue(interaction: CommandInteraction) { function open(interaction: CommandInteraction) {
let {channelId} = interaction, memberIsModThrow(interaction);
member = getMember(interaction),
const {channelId} = interaction,
teamsize = interaction.options.getInteger('teamsize', true); teamsize = interaction.options.getInteger('teamsize', true);
if (QUEUE.has(channelId)) const existing = QUEUE.get(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 if (existing)
throw emsg(Lang.get('error.discord.noCreate', {
teamsize: existing.teamsize.toString()
}));
QUEUE.set(channelId, { queueCreate(channelId, teamsize);
players: [
member
],
initiator: member,
teamsize: teamsize
});
await interaction.reply(`Queue for teams of ${teamsize} has been created, and you have joined`); interaction.reply(Lang.get('discord.create', {
teamsize: teamsize.toString()
}));
} }
/** /**
* joins a queue from an interaction * closes a queue
* @param 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) { async function close(interaction: CommandInteraction) {
memberIsModThrow(interaction);
let member = getMember(interaction), QUEUE.delete(interaction.channelId);
info = getInfo(interaction);
await interaction.reply(Lang.get('discord.close'));
}
/**
* gives info about the queue
* @param interaction
* @throws errorMessage class if it cannot be left
*/
async function queue(interaction: CommandInteraction) {
const info = getInfo(interaction);
await interaction.reply(Lang.getEmbed('discord.queue', {
teamsize: info.teamsize.toString(),
playercount: info.players.length.toString(),
}, {
ephemeral: true
}));
}
/**
* joins a queue
* @param interaction
* @throws errorMessage class if it cannot be readied
*/
async function join(interaction: CommandInteraction) {
const {member, info, channel} = getAll(interaction);
if (queueContains(interaction)) if (queueContains(interaction))
throw 'You are already in the active queue'; throw emsg('discord.inQueue');
info.players.push(member); info.players.push(member);
QUEUE.set(interaction.channelId, info); QUEUE.set(interaction.channelId, info);
await interaction.reply('Joined the queue'); await interaction.reply(Lang.get('discord.join'));
checkQueue(channel);
} }
/** /**
* leaves a queue from an interaction * leaves a queue
* @param interaction * @param interaction
* @throws string message if it cannot be left * @throws errorMessage class if it cannot be reset
*/ */
export async function leaveQueue(interaction: CommandInteraction) { async function leave(interaction: CommandInteraction) {
let member = getMember(interaction), const {member, info} = getAll(interaction);
info = getInfo(interaction);
if (!queueContains(interaction)) if (!queueContains(interaction))
throw 'You aren\'t in the active queue'; throw emsg('discord.notInQueue');
info.players.splice(info.players.indexOf(member), 1); info.players.splice(info.players.indexOf(member), 1);
QUEUE.set(interaction.channelId, info); QUEUE.set(interaction.channelId, info);
await interaction.reply('Left the queue'); await interaction.reply(Lang.get('discord.leave'));
} }
/** export const QueueCommands = {
* readys a queue from an interaction open,
* @param interaction close,
* @throws string message if it cannot be readied queue,
*/ join,
export async function readyQueue(interaction: CommandInteraction) { leave
};
let member = getMember(interaction),
info = getInfo(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';
//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));
//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```');
}
/**
* 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}
`+'```');
}

16
src/types/api.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
/**
* data taken from UniteAPI
*/
interface uniteApiData {
name: string,
id: string,
avatar: string,
level: string,
rank: string,
class: string|null,
elo: string|null,
battles: string,
wins: string,
winrate: string
}

104
src/types/lang.d.ts vendored Normal file
View File

@@ -0,0 +1,104 @@
/**
* any indexable object
*/
//this is a generic type, and needs 'any'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type basicObject = {[keys: string]: any};
/**
* any indexable object with string values
*/
type basicObjectStr = {[keys: string]: string};
/**
* an abstract version of strings
*/
type bigString = string | string[];
/**
* an object that contains embeds and can be passed directly to methods like `Discord.TextChannel.send()`
*/
interface embedData {
content?: string,
embeds: MessageEmbed[]
}
/**
* a representation of an author in the LANG object
*
* `LANG > Language > Embed > Field`
*/
interface embedField {
name: string,
value: bigString,
inline?: boolean
}
/**
* a representation of an author in the LANG object
*
* `LANG > Language > Embed > Author`
*/
interface authorData {
name: string,
url?: string,
icon?: string
}
/**
* a representation of a footer in the LANG object
*
* `LANG > Language > Embed > Footer`
*/
interface footerData {
text: string,
icon?: string
}
/**
* a representation of an embed in the LANG object
*
* `LANG > Language > Embed`
*/
interface embedObj {
embed: true,
content?: string,
title?: string,
description?: bigString,
/**
* URL
*/
url?: string,
/**
* #FFFFFF
*/
color?: string,
footer?: string | footerData,
thumbnail?: string,
/**
* URL
*/
image?: string,
/**
* URL
*/
author?: string | authorData,
fields?: embedField[],
timestamp?: boolean | string | number
}
/**
* a specific language in the LANG object
*
* `LANG > Language`
*/
type LangObj = { [keys:string]: LangObj | embedObj | string }
/**
* the entire LANG object
*
* `LANG`
*/
type LangObjWhole = { [langid:string]: LangObj }

6
src/types/queue.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface queueInfoBase {
teamsize: number
}
interface queueInfo extends queueInfoBase {
players: GuildMember[]
}

46
src/util/discord.ts Normal file
View File

@@ -0,0 +1,46 @@
import { CommandInteraction, GuildMember, TextChannel } from 'discord.js';
import { emsg } from './main';
/**
* get the GuildMember of an interaction
* @throws errorMessage class if it cannot be read
*/
export function getMember(interaction: CommandInteraction): GuildMember {
const member = interaction.member;
if (!(member instanceof GuildMember))
throw emsg('general.noMember');
return member;
}
/**
* get the TextChannel of an interaction
* @throws errorMessage class if it cannot be read
*/
export function getChannel(interaction: CommandInteraction): TextChannel {
const channel = interaction.channel;
if (!(channel instanceof TextChannel))
throw emsg('general.noChannel');
return channel;
}
/**
* get the TextChannel of an interaction
* @throws errorMessage class if the Member cannot be read
*/
export function memberIsMod(interaction: CommandInteraction): boolean {
const member = getMember(interaction);
return member.permissionsIn(interaction.channelId).has('MANAGE_MESSAGES');
}
/**
* get the TextChannel of an interaction
* @throws errorMessage class if the Member cannot be read or if Member is not a mod
*/
export function memberIsModThrow(interaction: CommandInteraction) {
if (!memberIsMod(interaction))
throw emsg('discord.notMod');
}

162
src/util/lang.ts Normal file
View File

@@ -0,0 +1,162 @@
import { MessageEmbed } from 'discord.js';
/**
*
* @param str
* @param args
* @returns
*/
export function template(str: string, args: basicObject): string {
return str.replace(/{\w+}/g, str => {
const key = str.substring(1, str.length-1);
if (key in args)
return args[key];
return key;
});
}
/**
* converts bigString to string
*/
export function bigString(bigStr: bigString): string {
if (Array.isArray(bigStr))
return bigStr.join('\n');
return bigStr;
}
/**
* converts Hex Color string to an RGB array
*/
export function resolveColor(color: string): [number, number, number] {
color = color.replace(/[^0-9a-f]/gi, '');
const colorNum: [number, number, number] = [0, 0, 0];
if (color.length === 3 || color.length === 6) {
const colorSpl = /([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/.exec(color);
if (!colorSpl)
return colorNum;
for (let i = 0; i < colorSpl.length && i < colorNum.length; i++)
colorNum[i] = parseInt(colorSpl[i], 16);
}
return colorNum;
}
/**
* converts embedObj to a string if applicable
* @param fallback the string to use if no valid strings can be found
*/
export function embedObjStr(embedObj: embedObj, args: basicObjectStr = {}, fallback = ''): string {
if (embedObj.content !== undefined)
return template(bigString(embedObj.content), args);
if (embedObj.description !== undefined)
return template(bigString(embedObj.description), args);
return fallback;
}
/**
* converts embedObj to Discord.MessageEmbed
*/
export function embedObjEmbed(embedObj: embedObj, args: basicObjectStr = {}): MessageEmbed {
const embed = new MessageEmbed(),
{ author, color, description, fields, footer, image, thumbnail, timestamp, title, url } = embedObj;
if (author !== undefined) {
let authorFix: authorData;
if (typeof author === 'string')
authorFix = {
name: template(author, args)
};
else {
const {name, icon, url} = author;
authorFix = {
name: template(name, args)
};
if (icon !== undefined)
authorFix.icon = template(icon, args);
if (url !== undefined)
authorFix.url = template(url, args);
}
embed.setAuthor(authorFix);
}
if (footer !== undefined) {
let footerFix: footerData;
if (typeof footer === 'string')
footerFix = {
text: template(footer, args)
};
else {
const {text, icon} = footer;
footerFix = {
text: template(text, args)
};
if (icon !== undefined)
footerFix.icon = template(icon, args);
}
embed.setFooter(footerFix);
}
if (color !== undefined)
embed.setColor(resolveColor(template(color, args)));
if (description !== undefined)
embed.setDescription(template(bigString(description), args));
if (image !== undefined)
embed.setImage(template(image, args));
if (thumbnail !== undefined)
embed.setThumbnail(template(thumbnail, args));
if (title !== undefined)
embed.setTitle(template(title, args));
if (url !== undefined)
embed.setURL(template(url, args));
if (timestamp === true)
embed.setTimestamp();
else if (typeof timestamp === 'string')
embed.setTimestamp(new Date(template(timestamp, args)));
else if (timestamp !== false)
embed.setTimestamp(timestamp);
fields?.forEach(field => {
embed.addField(template(field.name, args), template(bigString(field.value), args), field.inline);
});
return embed;
}

View File

@@ -1,3 +1,4 @@
import * as Lang from '../lang';
/** /**
* shuffles an array * shuffles an array
@@ -5,6 +6,7 @@
* @param array an array * @param array an array
* @returns an array but shuffled * @returns an array but shuffled
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function shuffle(array: any[]) { export function shuffle(array: any[]) {
let currentIndex = array.length, randomIndex; let currentIndex = array.length, randomIndex;
@@ -22,3 +24,26 @@ export function shuffle(array: any[]) {
return array; return array;
} }
/**
* use the emsg() function instead
*/
export class errorMessage {
public msg: string;
public ephemeral: boolean;
constructor(msg: string, ephemeral = 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 = true) => new errorMessage(Lang.get(`error.${msg}`), ephemeral);

11
tsconfig.eslint.json Normal file
View File

@@ -0,0 +1,11 @@
// Special typescript project file, used by eslint only.
{
"extends": "./tsconfig.json",
"include": [
// repeated from base config's "include" setting
"src",
// these are the eslint-only inclusions
".eslintrc.json",
]
}

View File

@@ -6,7 +6,16 @@
"outDir": "./dist", "outDir": "./dist",
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true, "strict": true,
"skipLibCheck": true "noImplicitReturns": true,
} "noUnusedLocals": true,
"noUnusedParameters": true,
"typeRoots": [
"./src/types/"
]
},
"include": [
"src"
]
} }