Compare commits

...

83 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
50c297bfea removed dev comments 2022-01-31 17:07:59 -06:00
c2c59e53ca updated comments 2022-01-31 17:06:40 -06:00
85d0e833dd cleaned up queue code 2022-01-31 17:06:34 -06:00
1e990abdd5 updated comments 2022-01-31 16:56:47 -06:00
5ce68086b8 updated to es2020 2022-01-31 15:53:23 -06:00
5ee97cfd08 fixed special character bug 2022-01-31 15:50:25 -06:00
5167a25626 added comments 2022-01-31 14:24:29 -06:00
a3a7acf9a8 updated readme 2022-01-31 14:08:33 -06:00
7ae2cb8db0 updated install script 2022-01-31 13:56:39 -06:00
d7adc4afde updated version number 2022-01-31 13:45:40 -06:00
ae7093ae1d cleaned up queue file 2022-01-31 13:18:58 -06:00
e719eaef95 updated readme 2022-01-31 12:52:33 -06:00
c3855c31bc updated readme 2022-01-31 12:51:07 -06:00
f43c6ab4a6 changed version number 2022-01-31 12:49:48 -06:00
aa0fffdc94 functional queue 2022-01-31 12:46:44 -06:00
588365e558 minor windows script uudate 2022-01-27 22:00:15 -06:00
fcfb3e290f minor update to bash script 2022-01-27 21:58:23 -06:00
1bf5e9d64f updated windows scripts 2022-01-27 21:58:13 -06:00
219d3e55e7 added screen/tmux support 2022-01-27 21:04:48 -06:00
4f696e8b21 updated start scripts 2022-01-27 20:43:51 -06:00
f6adafe735 updated readme 2022-01-27 20:35:30 -06:00
c9f9ba366a updated readme 2022-01-27 20:34:10 -06:00
9067a67bd5 updated unix scripts 2022-01-27 20:05:27 -06:00
8d2ec0ce02 updated bash scripts 2022-01-27 19:57:59 -06:00
92f2409768 added unix scripts 2022-01-27 19:42:26 -06:00
66818a40be Merge branch 'main' of git.zomo.dev:zomo/1800queue 2022-01-27 19:40:54 -06:00
53db0f47b9 update license 2022-01-27 19:40:52 -06:00
adc15f0555 renamed unix scripts folder 2022-01-27 19:40:39 -06:00
e2d3097829 added unix scripts 2022-01-27 19:40:28 -06:00
72ddf229ec added unix installation scripts 2022-01-27 19:39:14 -06:00
93b7aea301 added install/run instructions 2022-01-27 19:29:09 -06:00
ce7efb08fd added install/run instructions 2022-01-27 19:28:18 -06:00
428de6a74e added install/run instructions 2022-01-27 19:26:04 -06:00
f4d14b2507 update license 2022-01-27 19:20:34 -06:00
4b14e5e050 queue create/join 2022-01-27 18:49:52 -06:00
2a3e1e3efa link commands 2022-01-27 18:49:08 -06:00
83d1af3bef add commands 2022-01-27 18:48:51 -06:00
a461fad0c1 function to receive interaction 2022-01-27 18:48:37 -06:00
0a9918a508 dev packages 2022-01-27 18:46:54 -06:00
37 changed files with 8217 additions and 3424 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/*"]
}

6
.gitignore vendored
View File

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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Ashley Rosch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
<!-- markdownlint-disable MD033 -->
# 1800queue
## Prerequirements
Download and install [Node.js](https://nodejs.org/en/)
<details>
<summary><b>Your bot's token</b></summary>
<br>
<p>Your bot's token can be found in your <a href="https://discord.com/developers/applications">Discord dev portal</a></p>
<ol>
<li>
Create or choose your application
</li>
<li>
Go to the <code>Bot</code> section
<ul>
<li>
click <code>Add Bot</code> if you haven't already
</li>
</ul>
</li>
<li>
Copy your token
</li>
</ol>
</details>
## Automatic Installation/Updating
1. Download and extract this repository
2. Open the `scripts` folder
3. Open the folder that corresponds with your operating system
4. Run the `install` file
## Manual Installation/Updating
1. Download and extract this repository
2. Open the directory that contains `package.json` in your command prompt or terminal
3. Run the command `npm install`
4. Create a file called `token` in the same directory and enter your bot client's token
## Starting
**Important**: the bot's invite needs permission to `Create Commands` and `Send Messages`
1. Open the `scripts` folder
2. Open the folder that corresponds with your operating system
3. Run the `start` file
## Other Start Scripts
<details>
<summary><b>Mac/Linux Screen</b></summary>
<br>
<p>Screen allows the bot to run in the background without a terminal present</p>
<p><a href="https://linuxize.com/post/how-to-use-linux-screen/">Tutorial for usage</a>, check if it's installed already before installing</p>
<p><b>Installing</b></p>
<ul>
<li>
<p>Mac</p>
<ol>
<li>Install <a href="https://formulae.brew.sh/"><code>Homebrew</code></a></li>
<li>Open the terminal and run the command <code>brew install screen</code></li>
</ol>
</li>
<li>
<p>Linux</p>
<ul>
<li>Use your distribution's package manager to find <code>screen</code></li>
</ul>
</li>
</ul>
</details>
<details>
<summary><b>Mac/Linux Tmux</b></summary>
<br>
<p>Tmux allows the bot to run in the background without a terminal present</p>
<p><a href="https://linuxize.com/post/getting-started-with-tmux/">Tutorial for usage</a>, check if it's installed already before installing</p>
<p><b>Installing</b></p>
<ul>
<li>
<p>Mac</p>
<ol>
<li>Install <a href="https://formulae.brew.sh/"><code>Homebrew</code></a></li>
<li>Open the terminal and run the command <code>brew install tmux</code></li>
</ol>
</li>
<li>
<p>Linux</p>
<ul>
<li>Use your distribution's package manager to find <code>tmux</code></li>
</ul>
</li>
</ul>
</details>

183
dist/api.js vendored
View File

@@ -18,47 +18,32 @@ var __importStar = (this && this.__importStar) || function (mod) {
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPlayer = void 0;
exports.getPlayerInteraction = exports.getPlayer = void 0;
/*eslint prefer-const: "error"*/
const cheerio = __importStar(require("cheerio"));
const https_1 = __importDefault(require("https"));
//making long regex rather than splitting the string at ":" because regex would be easier to debug than logic in the case the website changes
//while names cant have spaces, the name slot in ogtitle could be shown as "No Player Name" which has spaces
const main_1 = require("./util/main");
const Lang = __importStar(require("./lang"));
const uniteApiRegex = {
//$1 = name, $2 = id
ogtitle: /Unite API - ([\w\d ]+) \((.*)\)/,
//$1 = level, $2 = rank, $3 = elo, $4 = battles, $5 = wins, $6 = win rate
ogdescription: /Pokémon Unite : Lv.(\d+) (\w+) \((\d+)\)\n Battles : (\d+)\n Wins : (\d+)\n Win Rate : (\d+)%/,
ogtitle: /unite api - (.+) \((.*)\)/i,
//$1 = level, $2 = rank, $3 = elo/class (rest is found by splitting each line)
ogdescription: [
/lv\.(\d+) (\w+) \((\d+)\)/i,
/lv\.(\d+) (\w+): class (\d+)/i //other
]
};
/*
og:title
Unite API - IanWhysp (X188GF7)
og:description
Pokémon Unite : Lv.40 Master (1741)
Battles : 1089
Wins : 576
Win Rate : 52%
*/
/**
* gets the html of the uniteApi page for the player
* @param name name of player
* @returns the html of the page through a promise (rejects if the page status is not 200)
*/
function getHTML(name) {
name = name.replace(/[^\w\d]/g, '');
//name = name.replace(/[^\w\d]/g, '');
return new Promise((resolve, reject) => {
const init = {
host: 'uniteapi.dev',
@@ -71,11 +56,10 @@ function getHTML(name) {
return;
}
let result = Buffer.alloc(0);
response.on('data', function (chunk) {
response.on('data', chunk => {
result = Buffer.concat([result, chunk]);
});
response.on('end', function () {
// result has response body buffer
response.on('end', () => {
resolve(result.toString());
});
};
@@ -87,41 +71,92 @@ function getHTML(name) {
* interprets the html from getHTML()
* @param name name of player
* @returns player data from site
* @throws errorMessage class if the request fails
*/
function readHTML(html) {
let metaElems = cheerio.load(html)('meta').toArray(), foundData = {
name: "",
id: "",
level: "",
rank: "",
elo: "",
battles: "",
wins: "",
winrate: ""
const $ = cheerio.load(html), foundData = {
name: '',
id: '',
avatar: '',
level: '',
rank: '',
elo: null,
class: null,
battles: '',
wins: '',
winrate: ''
};
let metaElems = $('meta').toArray();
//filter down to just ones named "og:..."
metaElems = metaElems.filter(el => { var _a; return (_a = el.attribs.property) === null || _a === void 0 ? void 0 : _a.startsWith('og:'); });
metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:'));
metaElems.forEach(el => {
let attr = el.attribs;
const attr = el.attribs;
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) {
foundData.name = data[1];
foundData.id = data[2];
}
}
else if (attr.property === 'og:description') {
let data = uniteApiRegex.ogdescription.exec(attr.content);
if (data !== null && data.length >= 7) {
foundData.level = data[1];
foundData.rank = data[2];
foundData.elo = data[3];
foundData.battles = data[4];
foundData.wins = data[5];
foundData.winrate = data[6];
//all lines
let lines = attr.content.split('\n').map(l => l.trim());
const extraLines = [];
//ensure first line is correct
while ((lines.length > 0) && !/pok.mon unite/i.test(lines[0])) {
const line = lines.shift();
if (line !== undefined)
extraLines.push(line);
}
if (lines.length === 0)
throw (0, main_1.emsg)('Unable to read data, please try again');
//bring the first lines removed back into the data
lines = [
...lines,
...extraLines.filter(d => d)
];
//first line
{
//will be only text after "pokemon unite:"
const line = lines[0].split(':').slice(1).join(':').trim(), regex = uniteApiRegex.ogdescription;
if (regex[0].test(line)) { //is master/has elo
const regexData = line.match(regex[0]);
if (!regexData || regexData.length < 4)
throw (0, main_1.emsg)('Unable to read data, please try again');
foundData.level = regexData[1];
foundData.rank = regexData[2];
foundData.elo = regexData[3];
}
else { //is not master/has a class
const regexData = line.match(regex[1]);
if (!regexData || regexData.length < 4)
throw (0, main_1.emsg)('Unable to read data, please try again');
foundData.level = regexData[1];
foundData.rank = regexData[2];
foundData.class = regexData[3];
}
}
lines.shift();
//rest of lines
lines.forEach(line => {
const split = line.split(':').map(l => l.trim()), key = split[0].toLowerCase().replace(/[^\w]/g, ''), value = split[1];
switch (key) {
case 'battles':
foundData.battles = value;
break;
case 'wins':
foundData.wins = value;
break;
case 'winrate':
foundData.winrate = value;
break;
}
});
}
});
const imgSrc = $('.player-card-image img').attr('src');
foundData.avatar = imgSrc !== undefined ? imgSrc : '';
foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/');
return foundData;
}
/**
@@ -130,7 +165,7 @@ function readHTML(html) {
* @returns boolean, valid or invalid
*/
function verifyData(data) {
if (data.id.length)
if (data.id.length > 0)
return true;
return false;
}
@@ -139,14 +174,44 @@ function verifyData(data) {
* @param name name of player
* @returns player data
*/
function getPlayer(name) {
return __awaiter(this, void 0, void 0, function* () {
let html = yield getHTML(name);
let data = readHTML(html);
if (verifyData(data))
return data;
return null;
});
async function getPlayer(name) {
const html = await getHTML(name), data = readHTML(html);
if (verifyData(data))
return data;
return null;
}
exports.getPlayer = getPlayer;
//await getPlayer('IanWhysp')
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
* @param interaction discord interaction
* @throws errorMessage class if the user cannot be found
*/
async function getPlayerInteraction(interaction) {
const username = interaction.options.getString('username', true);
await interaction.deferReply();
const data = await getPlayer(username);
if (data === null)
throw (0, main_1.emsg)('api.noUser');
else
sendPlayerEmbed(interaction, data);
}
exports.getPlayerInteraction = getPlayerInteraction;

82
dist/discord.js vendored
View File

@@ -1,41 +1,71 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerCommands = void 0;
/* eslint-disable camelcase */
const rest_1 = require("@discordjs/rest");
const v9_1 = require("discord-api-types/v9");
const commands = [{
// list of commands to register with discord
const commands = [
{
name: 'open',
description: 'open a queue for this channel',
options: [
{
type: 4,
name: 'teamsize',
description: 'size of each team',
min_value: 1,
required: true
}
]
},
{
name: 'close',
description: 'close the queue for this channel'
},
{
name: 'queue',
description: 'creates a queue'
}];
description: 'view queue info'
},
{
name: 'join',
description: 'join the active queue'
},
{
name: 'leave',
description: 'leave the active queue'
},
{
name: 'player',
description: 'display player information',
options: [
{
type: 3,
name: 'username',
description: 'in game name or UniteApi short link',
required: true
}
]
}
];
/*commandNames = commands.map(c => c.name);*/
/**
* register/reload commands on guild(s)
* @param token discord bot token
* @param clientId discord bot client id
* @param guildIds discord guild id(s)
*/
function registerCommands(token, clientId, guildIds) {
return __awaiter(this, void 0, void 0, function* () {
const rest = new rest_1.REST({ version: '9' }).setToken(token);
if (typeof guildIds === 'string')
guildIds = [guildIds];
for (let i = 0; i < guildIds.length; i++) {
try {
yield rest.put(v9_1.Routes.applicationGuildCommands(clientId, guildIds[i]), { body: commands });
console.log(`[${guildIds[i]}] registered command`);
}
catch (error) {
console.error(error);
}
async function registerCommands(token, clientId, guildIds) {
const rest = new rest_1.REST({ version: '9' }).setToken(token);
if (typeof guildIds === 'string')
guildIds = [guildIds];
for (let i = 0; i < guildIds.length; i++) {
try {
await rest.put(v9_1.Routes.applicationGuildCommands(clientId, guildIds[i]), { body: commands });
}
});
catch (error) {
console.error(error);
}
}
}
exports.registerCommands = registerCommands;

52
dist/index.js vendored
View File

@@ -1,52 +0,0 @@
"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;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const discord_js_1 = require("discord.js");
const fs = __importStar(require("fs"));
const discord_1 = require("./discord");
const queue_1 = require("./queue");
const CLIENT = new discord_js_1.Client({ intents: [discord_js_1.Intents.FLAGS.GUILDS] });
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();
CLIENT.on('ready', client => {
console.log(`Logged in as ${client.user.tag}`);
client.guilds.fetch().then(guilds => (0, discord_1.registerCommands)(TOKEN, client.user.id, guilds.map(g => g.id)));
});
CLIENT.on('interactionCreate', (interaction) => __awaiter(void 0, void 0, void 0, function* () {
if (!interaction.isCommand())
return;
if (interaction.commandName === 'queue')
(0, queue_1.createQueue)(interaction);
}));
CLIENT.login(TOKEN);

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;

92
dist/main.js vendored Normal file
View File

@@ -0,0 +1,92 @@
"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 });
const discord_js_1 = require("discord.js");
const fs = __importStar(require("fs"));
const api_1 = require("./api");
const discord_1 = require("./discord");
const Lang = __importStar(require("./lang"));
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] });
//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 => (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) => {
if (!interaction.isCommand())
return;
try {
switch (interaction.commandName) {
//mod commands
case 'open':
await queue_1.QueueCommands.open(interaction);
break;
case 'close':
await queue_1.QueueCommands.close(interaction);
break;
//general commands
case 'queue':
await queue_1.QueueCommands.queue(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) {
if (e instanceof main_1.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);

206
dist/queue.js vendored
View File

@@ -1,6 +1,206 @@
"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 });
exports.createQueue = void 0;
function createQueue(interaction) {
exports.QueueCommands = exports.queueContains = exports.discordInit = exports.queueRemove = exports.queueCreate = void 0;
const discord_js_1 = require("discord.js");
const fs = __importStar(require("fs"));
const Lang = __importStar(require("./lang"));
const discord_1 = require("./util/discord");
const main_1 = require("./util/main");
//load queues from file`
if (!fs.existsSync('./queues.json'))
fs.writeFileSync('./queues.json', '{}');
const _QUEUE = fs.readFileSync('./queues.json').toString(), QUEUE = new Map();
try {
const queueJson = JSON.parse(_QUEUE);
for (const channelId in queueJson) {
const { teamsize } = queueJson[channelId];
if (teamsize !== 0)
QUEUE.set(channelId, { teamsize, players: [] });
}
}
exports.createQueue = createQueue;
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
* @param interaction
* @throws errorMessage class if it does not exist
* @returns queue info
*/
function getInfo(interaction) {
const info = QUEUE.get(interaction.channelId);
if (!info)
throw (0, main_1.emsg)('discord.noQueue');
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
* @param interaction
* @returns boolean
*/
function queueContains(interaction) {
const { member, info } = getAll(interaction);
if (info.players.map(m => m.id).includes(member.id))
return true;
return false;
}
exports.queueContains = queueContains;
/**
* opens a queue
* @param interaction
* @throws errorMessage class if it cannot be left
*/
function open(interaction) {
(0, discord_1.memberIsModThrow)(interaction);
const { channelId } = interaction, teamsize = interaction.options.getInteger('teamsize', true);
const existing = QUEUE.get(channelId);
if (existing)
throw (0, main_1.emsg)(Lang.get('error.discord.noCreate', {
teamsize: existing.teamsize.toString()
}));
queueCreate(channelId, teamsize);
interaction.reply(Lang.get('discord.create', {
teamsize: teamsize.toString()
}));
}
/**
* closes a queue
* @param interaction
* @throws errorMessage class if it cannot be joined
*/
async function close(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))
throw (0, main_1.emsg)('discord.inQueue');
info.players.push(member);
QUEUE.set(interaction.channelId, info);
await interaction.reply(Lang.get('discord.join'));
checkQueue(channel);
}
/**
* leaves a queue
* @param interaction
* @throws errorMessage class if it cannot be reset
*/
async function leave(interaction) {
const { member, info } = getAll(interaction);
if (!queueContains(interaction))
throw (0, main_1.emsg)('discord.notInQueue');
info.players.splice(info.players.indexOf(member), 1);
QUEUE.set(interaction.channelId, info);
await interaction.reply(Lang.get('discord.leave'));
}
exports.QueueCommands = {
open,
close,
queue,
join,
leave
};

97
dist/util.js vendored Normal file
View File

@@ -0,0 +1,97 @@
"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.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
* 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;
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;

8941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,45 @@
{
"name": "1800queue",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node .",
"watch": "npm-watch",
"dev": "npm-watch"
},
"watch": {
"build": {
"patterns": [
"src"
],
"extensions": "ts",
"legacyWatch": true
"name": "1800queue",
"version": "1.0.0",
"description": "",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node .",
"watch": "npm-watch",
"dev": "npm-watch",
"getname": "echo $npm_package_name"
},
"start": {
"patterns": [
"dist"
],
"extensions": "js",
"legacyWatch": true,
"runOnChangeOnly": true
"watch": {
"build": {
"patterns": [
"src"
],
"extensions": "ts",
"legacyWatch": true
},
"start": {
"patterns": [
"dist"
],
"extensions": "js",
"legacyWatch": true,
"runOnChangeOnly": true
}
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^17.0.17",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"discord-api-types": "^0.26.1",
"npm-watch": "^0.11.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.5"
},
"dependencies": {
"@discordjs/rest": "^0.3.0",
"cheerio": "^1.0.0-rc.10",
"discord.js": "^13.6.0"
}
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^17.0.13",
"npm-watch": "^0.11.0",
"typescript": "^4.5.5"
},
"dependencies": {
"@discordjs/rest": "^0.3.0",
"cheerio": "^1.0.0-rc.10",
"discord-api-types": "^0.26.1",
"discord.js": "^13.6.0"
}
}

View File

@@ -1 +0,0 @@
# 1800queue

18
scripts/mac-linux/install Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
#change to ccurrent directory
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
cd ../..
#run
npm install
#discord token
[ ! -f "token.txt" ] && touch "token.txt"
if [ ! -s "token.txt" ]
then
printf "Enter your discord bot token: "
read token
printf $token > "token.txt"
fi

42
scripts/mac-linux/start Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
#change to current directory
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
cd ../..
#colors
C1='\033[1;37m'
C2='\033[1;33m'
NC='\033[0m'
#make log folder
if [[ ! -d log ]]
then
mkdir log
fi
#current file number
num=0
if [[ -f log/number ]]
then
num=$(cat log/number)
fi
echo $num > log/number
#watchdog
while true
do
#increment log file number
num=$(( num + 1 ))
echo $num > log/number
#run
npm start 2> log/$num.err | tee log/$num.out
#give chance to close program
printf "\n\n\n${C1}press ${C2}Control+C${C1} to close${NC}\n\n\n"
sleep 5
done

12
scripts/mac-linux/start_screen Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
#change to ccurrent directory
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
#start
if [[ $(screen -ls | grep $(npm run getname -s)) ]]; then
echo "Session already exists"
else
screen -S $(npm run getname -s) -d -m ./start
echo "Created session"
fi

12
scripts/mac-linux/start_tmux Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
#change to ccurrent directory
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
#start
if [[ $(tmux ls | grep $(npm run getname -s)) ]]; then
echo "Session already exists"
else
tmux new-session -d -s $(npm run getname -s) ./start
echo "Created session"
fi

View File

@@ -0,0 +1,16 @@
@echo off
@REM change to ccurrent directory
cd %~f0\..\..\..\
@REM discord token
if not exist "token.txt" copy NUL "token.txt"
for /f %%i in ("token.txt") do set size=%%~zi
if %size% equ 0 (
set /p token="Enter your discord bot token: "
echo | set /p=%id% > "token.txt"
)
@REM run
npm install

28
scripts/windows/start.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
@Rem change to current directory
cd %~f0\..\..\..\
@Rem make log folder
if not exist log mkdir log
@Rem current file number
set /A num=0
if exist log\number set /p num=<log\number
echo %num > log\number
@Rem watchdog
:watchdog
@Rem increment log file number
set /A num=num+1
echo %num > log\number
@Rem run
powershell "npm start 2> log\%num%.err | tee log\%num%.out"
@Rem give chance to close program
timeout /t 5 /nobreak
goto watchdog

View File

@@ -1,37 +1,19 @@
/*eslint prefer-const: "error"*/
import * as cheerio from 'cheerio';
import { IncomingMessage } from 'http';
import http from 'https';
import { CommandInteraction } from 'discord.js';
import http, { IncomingMessage } from 'http';
import { emsg } from './util/main';
import * as Lang from './lang';
//making long regex rather than splitting the string at ":" because regex would be easier to debug than logic in the case the website changes
//while names cant have spaces, the name slot in ogtitle could be shown as "No Player Name" which has spaces
const uniteApiRegex = {
//$1 = name, $2 = id
ogtitle: /Unite API - ([\w\d ]+) \((.*)\)/,
//$1 = level, $2 = rank, $3 = elo, $4 = battles, $5 = wins, $6 = win rate
ogdescription: /Pokémon Unite : Lv.(\d+) (\w+) \((\d+)\)\n Battles : (\d+)\n Wins : (\d+)\n Win Rate : (\d+)%/,
}
type uniteApiData = {
name: string,
id: string,
level: string,
rank: string,
elo: string,
battles: string,
wins: string,
winrate: string
}
/*
og:title
Unite API - IanWhysp (X188GF7)
og:description
Pokémon Unite : Lv.40 Master (1741)
Battles : 1089
Wins : 576
Win Rate : 52%
*/
ogtitle: /unite api - (.+) \((.*)\)/i,
//$1 = level, $2 = rank, $3 = elo/class (rest is found by splitting each line)
ogdescription: [ //just the first line
/lv\.(\d+) (\w+) \((\d+)\)/i, //master
/lv\.(\d+) (\w+): class (\d+)/i //other
]
};
/**
* gets the html of the uniteApi page for the player
@@ -40,12 +22,12 @@ Pokémon Unite : Lv.40 Master (1741)
*/
function getHTML(name: string): Promise<string> {
name = name.replace(/[^\w\d]/g, '');
//name = name.replace(/[^\w\d]/g, '');
return new Promise((resolve, reject) => {
const init = {
host: 'uniteapi.dev',
const opts = {
host: '147.182.215.216',
path: `/p/${encodeURIComponent(name)}`,
method: 'GET',
};
@@ -58,17 +40,16 @@ function getHTML(name: string): Promise<string> {
}
let result = Buffer.alloc(0);
response.on('data', function(chunk) {
response.on('data', chunk => {
result = Buffer.concat([result, chunk]);
});
response.on('end', function() {
// result has response body buffer
response.on('end', () => {
resolve(result.toString());
});
};
const req = http.request(init, callback);
const req = http.request(opts, callback);
req.end();
});
@@ -79,46 +60,123 @@ function getHTML(name: string): Promise<string> {
* interprets the html from getHTML()
* @param name name of player
* @returns player data from site
* @throws errorMessage class if the request fails
*/
function readHTML(html: string): uniteApiData {
let metaElems = cheerio.load(html)('meta').toArray(),
const $ = cheerio.load(html),
foundData: uniteApiData = {
name: "",
id: "",
name: '',
id: '',
avatar: '',
level: "",
rank: "",
elo: "",
battles: "",
wins: "",
winrate: ""
level: '',
rank: '',
elo: null,
class: null,
battles: '',
wins: '',
winrate: ''
};
let metaElems = $('meta').toArray();
//filter down to just ones named "og:..."
metaElems = metaElems.filter(el => el.attribs.property?.startsWith('og:'));
metaElems.forEach(el => {
let attr = el.attribs;
const attr = el.attribs;
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) {
foundData.name = data[1];
foundData.id = data[2];
}
} else if (attr.property === 'og:description') {
let data = uniteApiRegex.ogdescription.exec(attr.content);
if (data !== null && data.length >= 7) {
foundData.level = data[1];
foundData.rank = data[2];
foundData.elo = data[3];
foundData.battles = data[4];
foundData.wins = data[5];
foundData.winrate = data[6];
//all lines
let lines = attr.content.split('\n').map(l => l.trim());
const extraLines: string[] = [];
//ensure first line is correct
while ((lines.length > 0) && !/pok.mon unite/i.test(lines[0])) {
const line = lines.shift();
if (line !== undefined)
extraLines.push(line);
}
if (lines.length === 0)
throw emsg('Unable to read data, please try again');
//bring the first lines removed back into the data
lines = [
...lines,
...extraLines.filter(d => d)
];
//first line
{
//will be only text after "pokemon unite:"
const line = lines[0].split(':').slice(1).join(':').trim(),
regex = uniteApiRegex.ogdescription;
if (regex[0].test(line)) { //is master/has elo
const regexData = line.match(regex[0]);
if (!regexData || regexData.length < 4)
throw emsg('Unable to read data, please try again');
foundData.level = regexData[1];
foundData.rank = regexData[2];
foundData.elo = regexData[3];
} else { //is not master/has a class
const regexData = line.match(regex[1]);
if (!regexData || regexData.length < 4)
throw emsg('Unable to read data, please try again');
foundData.level = regexData[1];
foundData.rank = regexData[2];
foundData.class = regexData[3];
}
}
lines.shift();
//rest of lines
lines.forEach(line => {
const split = line.split(':').map(l => l.trim()),
key = split[0].toLowerCase().replace(/[^\w]/g, ''),
value = split[1];
switch(key) {
case 'battles':
foundData.battles = value;
break;
case 'wins':
foundData.wins = value;
break;
case 'winrate':
foundData.winrate = value;
break;
}
});
}
})
});
const imgSrc = $('.player-card-image img').attr('src');
foundData.avatar = imgSrc !== undefined ? imgSrc : '';
foundData.avatar = foundData.avatar.replace('../', 'https://uniteapi.dev/');
return foundData;
@@ -130,7 +188,7 @@ function readHTML(html: string): uniteApiData {
* @returns boolean, valid or invalid
*/
function verifyData(data: uniteApiData): boolean {
if (data.id.length)
if (data.id.length > 0)
return true;
return false;
}
@@ -141,11 +199,47 @@ function verifyData(data: uniteApiData): boolean {
* @returns player data
*/
export async function getPlayer(name: string): Promise<uniteApiData|null> {
let html = await getHTML(name);
let data = readHTML(html);
const html = await getHTML(name),
data = readHTML(html);
if (verifyData(data))
return data;
return null;
}
//await getPlayer('IanWhysp')
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
* @param interaction discord interaction
* @throws errorMessage class if the user cannot be found
*/
export async function getPlayerInteraction(interaction: CommandInteraction) {
const username = interaction.options.getString('username', true);
await interaction.deferReply();
const data = await getPlayer(username);
if (data === null)
throw emsg('api.noUser');
else
sendPlayerEmbed(interaction, data);
}

View File

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

View File

@@ -1,28 +0,0 @@
import { Client, Intents } from 'discord.js';
import * as fs from 'fs';
import { registerCommands } from './discord';
import { createQueue } from './queue';
const CLIENT = new Client({ intents: [Intents.FLAGS.GUILDS] });
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();
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;
if (interaction.commandName === 'queue')
createQueue(interaction);
});
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,5 +1,249 @@
import { CommandInteraction } from "discord.js";
import { Client, CommandInteraction, TextChannel } from 'discord.js';
import * as fs from 'fs';
import * as Lang from './lang';
import { getChannel, getMember, memberIsModThrow } from './util/discord';
import { emsg } from './util/main';
export function createQueue(interaction: CommandInteraction) {
//load queues from file`
if (!fs.existsSync('./queues.json'))
fs.writeFileSync('./queues.json', '{}');
}
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
}
function SaveQueue() {
const queueJson = Object.fromEntries(QUEUE),
queueFileJson: {[keys: string]: queueInfoBase} = {};
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: 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
* @param interaction
* @throws errorMessage class if it does not exist
* @returns queue info
*/
function getInfo(interaction: CommandInteraction): queueInfo {
const info = QUEUE.get(interaction.channelId);
if (!info)
throw emsg('discord.noQueue');
return info;
}
/**
* compiles all the get functions above
* @param interaction
* @throws if another get function throws
* @returns object containing each
*/
const getAll = (interaction: CommandInteraction) => ({
member: getMember(interaction),
channel: getChannel(interaction),
info: getInfo(interaction)
});
/**
* checks if the interaction data is already in the queue
* @param interaction
* @returns boolean
*/
export function queueContains(interaction: CommandInteraction): boolean {
const {member, info} = getAll(interaction);
if (info.players.map(m=>m.id).includes(member.id))
return true;
return false;
}
/**
* opens a queue
* @param interaction
* @throws errorMessage class if it cannot be left
*/
function open(interaction: CommandInteraction) {
memberIsModThrow(interaction);
const {channelId} = interaction,
teamsize = interaction.options.getInteger('teamsize', true);
const existing = QUEUE.get(channelId);
if (existing)
throw emsg(Lang.get('error.discord.noCreate', {
teamsize: existing.teamsize.toString()
}));
queueCreate(channelId, teamsize);
interaction.reply(Lang.get('discord.create', {
teamsize: teamsize.toString()
}));
}
/**
* closes a queue
* @param interaction
* @throws errorMessage class if it cannot be joined
*/
async function close(interaction: CommandInteraction) {
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: 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))
throw emsg('discord.inQueue');
info.players.push(member);
QUEUE.set(interaction.channelId, info);
await interaction.reply(Lang.get('discord.join'));
checkQueue(channel);
}
/**
* leaves a queue
* @param interaction
* @throws errorMessage class if it cannot be reset
*/
async function leave(interaction: CommandInteraction) {
const {member, info} = getAll(interaction);
if (!queueContains(interaction))
throw emsg('discord.notInQueue');
info.players.splice(info.players.indexOf(member), 1);
QUEUE.set(interaction.channelId, info);
await interaction.reply(Lang.get('discord.leave'));
}
export const QueueCommands = {
open,
close,
queue,
join,
leave
};

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

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

@@ -1,12 +1,21 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true,
"skipLibCheck": true
}
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"typeRoots": [
"./src/types/"
]
},
"include": [
"src"
]
}