Compare commits

...

8 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
23 changed files with 667 additions and 451 deletions

View File

@@ -5,7 +5,7 @@
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./tsconfig.json"] }, "parserOptions": { "project": ["./tsconfig.eslint.json"] },
"plugins": [ "plugins": [
"@typescript-eslint" "@typescript-eslint"
], ],
@@ -17,10 +17,7 @@
"allowNumber" : false "allowNumber" : false
} }
], ],
/* dev */
"no-debugger": "off",
/* important */ /* important */
"prefer-const": "error", "prefer-const": "error",
"quotes": ["error", "single"], "quotes": ["error", "single"],

View File

@@ -2,10 +2,6 @@
# 1800queue # 1800queue
## Invite info
needs: create commands/send messages
## Prerequirements ## Prerequirements
Download and install [Node.js](https://nodejs.org/en/) Download and install [Node.js](https://nodejs.org/en/)
@@ -48,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

10
dist/api.js vendored
View File

@@ -26,7 +26,7 @@ exports.getPlayerInteraction = exports.getPlayer = void 0;
/*eslint prefer-const: "error"*/ /*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 util_1 = require("./util"); const main_1 = require("./util/main");
const Lang = __importStar(require("./lang")); const Lang = __importStar(require("./lang"));
const uniteApiRegex = { const uniteApiRegex = {
//$1 = name, $2 = id //$1 = name, $2 = id
@@ -109,7 +109,7 @@ function readHTML(html) {
extraLines.push(line); extraLines.push(line);
} }
if (lines.length === 0) if (lines.length === 0)
throw (0, util_1.emsg)('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,
@@ -122,7 +122,7 @@ function readHTML(html) {
if (regex[0].test(line)) { //is master/has elo if (regex[0].test(line)) { //is master/has elo
const regexData = line.match(regex[0]); const regexData = line.match(regex[0]);
if (!regexData || regexData.length < 4) if (!regexData || regexData.length < 4)
throw (0, util_1.emsg)('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];
@@ -130,7 +130,7 @@ function readHTML(html) {
else { //is not master/has a class else { //is not master/has a class
const regexData = line.match(regex[1]); const regexData = line.match(regex[1]);
if (!regexData || regexData.length < 4) if (!regexData || regexData.length < 4)
throw (0, util_1.emsg)('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];
@@ -210,7 +210,7 @@ async function getPlayerInteraction(interaction) {
await interaction.deferReply(); await interaction.deferReply();
const data = await getPlayer(username); const data = await getPlayer(username);
if (data === null) if (data === null)
throw (0, util_1.emsg)('api.noUser'); throw (0, main_1.emsg)('api.noUser');
else else
sendPlayerEmbed(interaction, data); sendPlayerEmbed(interaction, data);
} }

104
dist/lang.js vendored
View File

@@ -1,100 +1,7 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getEmbed = exports.get = exports.setLang = void 0; exports.getEmbed = exports.get = exports.setLang = void 0;
const discord_js_1 = require("discord.js"); const lang_1 = require("./util/lang");
/* UTIL FUNCS */
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;
});
}
function bigString(str) {
if (typeof str === 'object')
return str.join('\n');
return str;
}
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;
}
function embedObjStr(embedData, fallback = '') {
if (embedData.content !== undefined)
return bigString(embedData.content);
if (embedData.description !== undefined)
return bigString(embedData.description);
return fallback;
}
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;
}
/* LANG */
const LANG = { const LANG = {
en: { en: {
main: { main: {
@@ -190,9 +97,9 @@ function get(id, args = {}) {
if (key in finding) { if (key in finding) {
const found = finding[key]; const found = finding[key];
if (typeof found === 'string') if (typeof found === 'string')
return template(found, args); return (0, lang_1.template)(found, args);
if (found.embed === true) if (found.embed === true)
return embedObjStr(found, id); return (0, lang_1.embedObjStr)(found, args, id);
finding = found; finding = found;
} }
else else
@@ -219,11 +126,11 @@ function getEmbed(id, args = {}, otherOptions = {}) {
if (key in finding) { if (key in finding) {
const found = finding[key]; const found = finding[key];
if (typeof found === 'string') { if (typeof found === 'string') {
embedData.content = template(found, args); embedData.content = (0, lang_1.template)(found, args);
break; break;
} }
if (found.embed === true) { if (found.embed === true) {
const embedObj = found, { content } = embedObj, embed = embedObjEmbed(embedObj, args); const embedObj = found, { content } = embedObj, embed = (0, lang_1.embedObjEmbed)(embedObj, args);
embedData.embeds.push(embed); embedData.embeds.push(embed);
if (content !== undefined) if (content !== undefined)
embedData.content = content; embedData.content = content;
@@ -237,4 +144,3 @@ function getEmbed(id, args = {}, otherOptions = {}) {
return embedData; return embedData;
} }
exports.getEmbed = getEmbed; exports.getEmbed = getEmbed;
debugger;

View File

@@ -25,7 +25,7 @@ const api_1 = require("./api");
const discord_1 = require("./discord"); const discord_1 = require("./discord");
const Lang = __importStar(require("./lang")); const Lang = __importStar(require("./lang"));
const queue_1 = require("./queue"); const queue_1 = require("./queue");
const util_1 = require("./util"); 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');
@@ -76,7 +76,7 @@ CLIENT.on('interactionCreate', async (interaction) => {
} }
} }
catch (e) { catch (e) {
if (e instanceof util_1.errorMessage) { if (e instanceof main_1.errorMessage) {
if (interaction.deferred || interaction.replied) if (interaction.deferred || interaction.replied)
interaction.editReply(e.msg); interaction.editReply(e.msg);
else else

21
dist/queue.js vendored
View File

@@ -26,9 +26,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueCommands = exports.queueContains = exports.discordInit = exports.queueRemove = exports.queueCreate = 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 fs = __importStar(require("fs")); const fs = __importStar(require("fs"));
const util_1 = require("./util");
const Lang = __importStar(require("./lang")); const Lang = __importStar(require("./lang"));
//load queues from file const discord_1 = require("./util/discord");
const main_1 = require("./util/main");
//load queues from file`
if (!fs.existsSync('./queues.json')) if (!fs.existsSync('./queues.json'))
fs.writeFileSync('./queues.json', '{}'); fs.writeFileSync('./queues.json', '{}');
const _QUEUE = fs.readFileSync('./queues.json').toString(), QUEUE = new Map(); const _QUEUE = fs.readFileSync('./queues.json').toString(), QUEUE = new Map();
@@ -101,7 +102,7 @@ exports.discordInit = discordInit;
function getInfo(interaction) { function getInfo(interaction) {
const info = QUEUE.get(interaction.channelId); const info = QUEUE.get(interaction.channelId);
if (!info) if (!info)
throw (0, util_1.emsg)('discord.noQueue'); throw (0, main_1.emsg)('discord.noQueue');
return info; return info;
} }
/** /**
@@ -111,8 +112,8 @@ function getInfo(interaction) {
* @returns object containing each * @returns object containing each
*/ */
const getAll = (interaction) => ({ const getAll = (interaction) => ({
member: (0, util_1.getMember)(interaction), member: (0, discord_1.getMember)(interaction),
channel: (0, util_1.getChannel)(interaction), channel: (0, discord_1.getChannel)(interaction),
info: getInfo(interaction) info: getInfo(interaction)
}); });
/** /**
@@ -133,11 +134,11 @@ exports.queueContains = queueContains;
* @throws errorMessage class if it cannot be left * @throws errorMessage class if it cannot be left
*/ */
function open(interaction) { function open(interaction) {
(0, util_1.memberIsModThrow)(interaction); (0, discord_1.memberIsModThrow)(interaction);
const { channelId } = interaction, teamsize = interaction.options.getInteger('teamsize', true); const { channelId } = interaction, teamsize = interaction.options.getInteger('teamsize', true);
const existing = QUEUE.get(channelId); const existing = QUEUE.get(channelId);
if (existing) if (existing)
throw (0, util_1.emsg)(Lang.get('error.discord.noCreate', { throw (0, main_1.emsg)(Lang.get('error.discord.noCreate', {
teamsize: existing.teamsize.toString() teamsize: existing.teamsize.toString()
})); }));
queueCreate(channelId, teamsize); queueCreate(channelId, teamsize);
@@ -151,7 +152,7 @@ function open(interaction) {
* @throws errorMessage class if it cannot be joined * @throws errorMessage class if it cannot be joined
*/ */
async function close(interaction) { async function close(interaction) {
(0, util_1.memberIsModThrow)(interaction); (0, discord_1.memberIsModThrow)(interaction);
QUEUE.delete(interaction.channelId); QUEUE.delete(interaction.channelId);
await interaction.reply(Lang.get('discord.close')); await interaction.reply(Lang.get('discord.close'));
} }
@@ -177,7 +178,7 @@ async function queue(interaction) {
async function join(interaction) { async function join(interaction) {
const { member, info, channel } = getAll(interaction); const { member, info, channel } = getAll(interaction);
if (queueContains(interaction)) if (queueContains(interaction))
throw (0, util_1.emsg)('discord.inQueue'); 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(Lang.get('discord.join')); await interaction.reply(Lang.get('discord.join'));
@@ -191,7 +192,7 @@ async function join(interaction) {
async function leave(interaction) { async function leave(interaction) {
const { member, info } = getAll(interaction); const { member, info } = getAll(interaction);
if (!queueContains(interaction)) if (!queueContains(interaction))
throw (0, util_1.emsg)('discord.notInQueue'); 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(Lang.get('discord.leave')); await interaction.reply(Lang.get('discord.leave'));

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;

View File

@@ -2,7 +2,7 @@
"name": "1800queue", "name": "1800queue",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "node .", "start": "node .",

View File

@@ -1,9 +1,8 @@
/*eslint prefer-const: "error"*/ /*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 { emsg } from './util';
import * as Lang from './lang'; import * as Lang from './lang';
const uniteApiRegex = { const uniteApiRegex = {
@@ -16,20 +15,6 @@ const uniteApiRegex = {
] ]
}; };
type uniteApiData = {
name: string,
id: string,
avatar: string,
level: string,
rank: string,
class: string|null,
elo: string|null,
battles: string,
wins: string,
winrate: string
}
/** /**
* gets the html of the uniteApi page for the player * gets the html of the uniteApi page for the player
* @param name name of player * @param name name of player
@@ -41,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',
}; };
@@ -64,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();
}); });

View File

@@ -1,199 +1,4 @@
import { MessageEmbed } from 'discord.js'; import { embedObjEmbed, embedObjStr, template } from './util/lang';
/* TYPES */
//this is a generic type, and needs 'any'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type basicObject = {[keys: string]: any};
type basicObjectStr = {[keys: string]: string};
type URL = string;
type bigString = string | string[];
interface embedData {
content?: string,
embeds: MessageEmbed[]
}
interface embedField {
name: string,
value: bigString,
inline?: boolean
}
interface authorData {
name: string,
url?: string,
icon?: string
}
interface footerData {
text: string,
icon?: string
}
interface embedObj {
embed: true,
content?: string,
title?: string,
description?: bigString,
url?: URL,
color?: string,
footer?: string | footerData,
thumbnail?: URL,
image?: URL,
author?: string | authorData,
fields?: embedField[],
timestamp?: boolean | string | number
}
type LangObj = { [keys:string]: LangObj | embedObj | string }
type LangObjWhole = { [langid:string]: LangObj }
/* UTIL FUNCS */
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;
});
}
function bigString(str: bigString): string {
if (typeof str === 'object')
return str.join('\n');
return str;
}
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;
}
function embedObjStr(embedData: embedObj, fallback = ''): string {
if (embedData.content !== undefined)
return bigString(embedData.content);
if (embedData.description !== undefined)
return bigString(embedData.description);
return fallback;
}
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;
}
/* LANG */
const LANG: LangObjWhole = { const LANG: LangObjWhole = {
@@ -315,7 +120,7 @@ export function get(id: string, args: basicObjectStr = {}): string {
return template(found, args); return template(found, args);
if (found.embed === true) if (found.embed === true)
return embedObjStr(found as embedObj, id); return embedObjStr(found as embedObj, args, id);
finding = found as LangObj; finding = found as LangObj;
@@ -378,5 +183,3 @@ export function getEmbed(id: string, args: basicObjectStr = {}, otherOptions: ba
return embedData; return embedData;
} }
debugger;

View File

@@ -5,7 +5,7 @@ import { getPlayerInteraction } from './api';
import { registerCommands } from './discord'; import { registerCommands } from './discord';
import * as Lang from './lang'; import * as Lang from './lang';
import { discordInit, QueueCommands } from './queue'; import { discordInit, QueueCommands } from './queue';
import { errorMessage } from './util'; import { errorMessage } from './util/main';
const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] }); const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] });
//init logs with a timestamp //init logs with a timestamp

View File

@@ -1,14 +1,10 @@
/* TODO
join message should contain your current position in the queue, editing it to keep it current
*/
import { Client, CommandInteraction, TextChannel } from 'discord.js'; import { Client, CommandInteraction, TextChannel } from 'discord.js';
import * as fs from 'fs'; import * as fs from 'fs';
import { emsg, getChannel, getMember, memberIsModThrow, queueInfo, queueInfoBase } from './util';
import * as Lang from './lang'; import * as Lang from './lang';
import { getChannel, getMember, memberIsModThrow } from './util/discord';
import { emsg } from './util/main';
//load queues from file //load queues from file`
if (!fs.existsSync('./queues.json')) if (!fs.existsSync('./queues.json'))
fs.writeFileSync('./queues.json', '{}'); fs.writeFileSync('./queues.json', '{}');

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[]
}

View File

@@ -1,97 +0,0 @@
import { CommandInteraction, GuildMember, TextChannel } from 'discord.js';
import * as Lang from './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
export function shuffle(array: any[]) {
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;
}
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);
export interface queueInfoBase {
teamsize: number
}
export interface queueInfo extends queueInfoBase{
players: GuildMember[]
}
/**
* get the GuildMember of an interaction
* @param interaction
* @throws errorMessage class if it cannot be read
* @returns member
*/
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
* @param interaction
* @throws errorMessage class if it cannot be read
* @returns member
*/
export function getChannel(interaction: CommandInteraction): TextChannel {
const channel = interaction.channel;
if (!(channel instanceof TextChannel))
throw emsg('general.noChannel');
return channel;
}
export function memberIsMod(interaction: CommandInteraction): boolean {
const member = getMember(interaction);
return member.permissionsIn(interaction.channelId).has('MANAGE_MESSAGES');
}
export function memberIsModThrow(interaction: CommandInteraction) {
if (!memberIsMod(interaction))
throw emsg('discord.notMod');
}

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;
}

49
src/util/main.ts Normal file
View File

@@ -0,0 +1,49 @@
import * as Lang from '../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
export function shuffle(array: any[]) {
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;
}
/**
* 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

@@ -11,5 +11,11 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
} "typeRoots": [
"./src/types/"
]
},
"include": [
"src"
]
} }