Compare commits

...

4 Commits

  1. 95
      package-lock.json
  2. 3
      package.json
  3. 187
      src/account.ts
  4. 93
      src/bot.ts
  5. 19
      src/commands/add.ts
  6. 29
      src/commands/list.ts
  7. 7
      src/commands/ping.ts
  8. 19
      src/commands/remove.ts
  9. 9
      src/commands/update.ts
  10. 98
      src/data.ts
  11. 1
      src/index.ts
  12. 144
      src/player.ts
  13. 13
      src/util.ts

95
package-lock.json generated

@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"@fightmegg/riot-api": "^0.0.18", "@fightmegg/riot-api": "^0.0.18",
"discord.js": "^14.14.1", "discord.js": "^14.14.1",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"mysql2": "^3.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.6", "@types/node": "^20.10.6",
@ -355,6 +356,25 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ioredis": { "node_modules/ioredis": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
@ -378,6 +398,11 @@
"url": "https://opencollective.com/ioredis" "url": "https://opencollective.com/ioredis"
} }
}, },
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -398,6 +423,19 @@
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
}, },
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=16.14"
}
},
"node_modules/magic-bytes.js": { "node_modules/magic-bytes.js": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
@ -429,6 +467,43 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/mysql2": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz",
"integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
"dependencies": {
"lru-cache": "^7.14.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/named-placeholders/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"engines": {
"node": ">=12"
}
},
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -472,6 +547,24 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/standard-as-callback": { "node_modules/standard-as-callback": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",

@ -21,6 +21,7 @@
"dependencies": { "dependencies": {
"@fightmegg/riot-api": "^0.0.18", "@fightmegg/riot-api": "^0.0.18",
"discord.js": "^14.14.1", "discord.js": "^14.14.1",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"mysql2": "^3.10.0"
} }
} }

@ -0,0 +1,187 @@
import {PlatformId, RiotAPI, RiotAPITypes} from "@fightmegg/riot-api"
import {RowDataPacket} from "mysql2/promise";
import {getMySQLConnection} from "./util";
import AccountDTO = RiotAPITypes.Account.AccountDTO;
const riotAPI = new RiotAPI(process.env["RIOT_API_KEY"] ?? "");
export class Account {
puuid: string
gameName?: string
tagLine?: string
currentElo?: Elo
startElo?: Elo
constructor(puuid: string) {
this.puuid = puuid;
}
getProgress(){
return eloToNumber(this.currentElo) - eloToNumber(this.startElo);
}
async isAdded() {
const conn = await getMySQLConnection();
const [rows] = await conn.execute<RowDataPacket[]>(`
SELECT 1
FROM accounts
WHERE puuid = ?
LIMIT 1
`, [this.puuid]);
return rows.length == 1;
}
async addToDB(){
const conn = await getMySQLConnection();
await conn.execute(`
INSERT INTO accounts
(puuid, gameName, tagLine)
VALUES (?, ?, ?)
`, [this.puuid, this.gameName, this.tagLine]);
}
async removeFromDB(){
const conn = await getMySQLConnection();
await conn.execute(`
DELETE FROM accounts
WHERE puuid = ?
`, [this.puuid]);
}
async update(){
const conn = await getMySQLConnection();
const fetchedAccount = await riotAPI.account.getByPUUID({
region: PlatformId.EUROPE,
puuid: this.puuid
});
this.gameName = fetchedAccount.gameName;
this.tagLine = fetchedAccount.tagLine;
await conn.execute(`
UPDATE accounts
SET gameName = ?, tagLine = ?
WHERE puuid = ?
`, [this.gameName, this.tagLine, this.puuid]);
const fetchedElo = await this.fetchCurrentElo();
if (fetchedElo && (!this.currentElo ||
this.currentElo.tier !== fetchedElo.tier ||
this.currentElo.rank !== fetchedElo.rank ||
this.currentElo.points !== fetchedElo.points
)){
this.currentElo = fetchedElo;
const date = new Date(Date.now()).toISOString().slice(0, 19).replace('T', ' ');
const [[row]] = await conn.execute<RowDataPacket[]>(`
SELECT id
FROM accounts
WHERE puuid = ?
`, [this.puuid]);
const sqlAccountId = row["id"];
await conn.execute(`
INSERT INTO elo_entries
(accountId, date, tier, \`rank\`, points)
VALUES (?, ?, ?, ?, ?)
`, [sqlAccountId, date, this.currentElo.tier, this.currentElo.rank, this.currentElo.points]);
console.log(`Added new elo entry for ${this}.`);
}
}
async fetchCurrentElo() {
const tryRegion = async (region: RiotAPITypes.LoLRegion) => {
return await riotAPI.league.getEntriesBySummonerId({
region: region,
summonerId: (await riotAPI.summoner.getByPUUID({
region: region,
puuid: this.puuid
})).id
});
};
let entries;
try {
entries = await tryRegion(PlatformId.EUW1);
} catch (error){
try {
entries = await tryRegion(PlatformId.EUNE1);
} catch (error){
let response = error as Response;
console.error(await response.json());
return;
}
}
let soloQ = entries.find(e => e.queueType == "RANKED_SOLO_5x5");
if (soloQ && ((soloQ.wins + soloQ.losses) >= 5)){
return {
tier: soloQ.tier,
rank: soloQ.rank,
points: soloQ.leaguePoints
} as Elo;
}
}
static async fetchFromRiotID(gameName: string, tagLine: string){
let fetchedAccount: AccountDTO;
try {
fetchedAccount = await riotAPI.account.getByRiotId({
region: PlatformId.EUROPE,
gameName: gameName,
tagLine: tagLine
});
} catch (error) {
console.error(`Riot ID not found: ${gameName}#${tagLine}!`);
return;
}
let account = new Account(fetchedAccount.puuid);
account.gameName = fetchedAccount.gameName;
account.tagLine = fetchedAccount.tagLine;
return account;
}
toString() {
return `${this.gameName}#${this.tagLine}`;
}
}
interface Elo {
tier: string,
rank: string,
points: number
}
export function eloToNumber(elo?: Elo){
if (!elo)
return 0;
let tiers: Record<string, number> = {
"CHALLENGER": 2800,
"GRANDMASTER": 2800,
"MASTER": 2800,
"DIAMOND": 2400,
"EMERALD": 2000,
"PLATINUM": 1600,
"GOLD": 1200,
"SILVER": 800,
"BRONZE": 400,
"IRON": 0,
};
let ranks: Record<string, number> = {
"I": 300,
"II": 200,
"III": 100,
"IV": 0,
};
let tier = elo.tier;
if (tier === "MASTER" || tier === "GRANDMASTER" || tier === "CHALLENGER")
return tiers[tier] + elo.points;
return tiers[tier] + ranks[elo.rank] + elo.points;
}

@ -1,115 +1,34 @@
import {Client, Collection, Events, GatewayIntentBits, Interaction} from "discord.js" import {Client, Collection, Events, GatewayIntentBits, Interaction} from "discord.js"
import {Player} from "./player";
import fs from "node:fs";
import {Command, loadCommands} from "./command"; import {Command, loadCommands} from "./command";
import {DataManager} from "./data";
require("./util.js"); require("./util.js");
export class UEMEloBot extends Client { export class UEMEloBot extends Client {
servers = new Collection<string, Player[]>();
fileName = "players.json"
commands = new Collection<string, Command>(); commands = new Collection<string, Command>();
data = new DataManager();
constructor() { constructor() {
super({intents: [GatewayIntentBits.Guilds]}); super({intents: [GatewayIntentBits.Guilds]});
this.once(Events.ClientReady, this.onReady); this.once(Events.ClientReady, this.onReady);
this.on(Events.InteractionCreate, this.onInteractionCreate); this.on(Events.InteractionCreate, this.onInteractionCreate);
this.parsePlayersFromFile();
loadCommands().then(async commands => { loadCommands().then(async commands => {
this.commands = commands; this.commands = commands;
const token = await this.login(); const token = await this.login();
console.log(`Discord Token: ${token}`); console.log(`Discord Token: ${token}`);
setInterval(this.updatePlayers.bind(this), 20 * 60 * 1000);
await this.updatePlayers();
}); });
} }
async onInteractionCreate(interaction: Interaction) { async onInteractionCreate(interaction: Interaction) {
if (!interaction.isChatInputCommand()) if (!interaction.isChatInputCommand())
return; return;
let commandName = interaction.commandName;
let command = this.commands.get(commandName) as Command;
await command.execute(interaction);
}
parsePlayersFromFile(){
// TODO get players from sql instead
if (fs.existsSync(this.fileName)){
let fileContent = fs.readFileSync(this.fileName).toString();
try {
this.servers.clear();
for (let [serverID, players] of Object.entries(JSON.parse(fileContent))){
console.log(`Server: ${serverID}`);
let playersList: Player[] = [];
for (let obj of (players as Object[])){
let p = Player.Load(obj);
playersList.push(p);
console.log(`Parsed player: ${p}`);
}
this.servers.set(serverID, playersList);
}
} catch (error) {
console.error(`Failed to parse players: ${error}`);
}
}
}
savePlayersToFile(){
fs.writeFileSync(this.fileName, JSON.stringify(Object.fromEntries(this.servers), null, 4));
}
async updatePlayers(){
for (let [_, players] of this.servers){
for (let p of players){
await p.updateFullName();
await p.updateCurrentElo();
if (!p.startElo && p.currentElo){
p.startElo = p.currentElo;
console.log(`Updated start elo for ${p}`);
}
}
}
this.savePlayersToFile();
}
async addPlayer(gameName: string, tagLine: string, serverID: string){
let player = await Player.TryCreateFrom(gameName, tagLine);
if (!player)
return;
if (this.servers.ensure(serverID, () => []).find(p => p.puuid === player.puuid)) {
console.error(`${player} already registered!`);
return;
}
this.servers.ensure(serverID, () => []).push(player);
console.log(`Added ${player} on Server: ${serverID}!`);
this.savePlayersToFile();
return player;
}
async removePlayer(gameName: string, tagLine: string, serverID: string){
let playerCopy = await Player.TryCreateFrom(gameName, tagLine);
if (!playerCopy){ if (interaction.guildId !== process.env["GUILD_ID"])
console.error(`Tried to remove non-existent Riot ID ${gameName}#${tagLine}`)
return; return;
}
let serverPlayers = this.servers.ensure(serverID, () => []); let commandName = interaction.commandName;
let command = this.commands.get(commandName) as Command;
let player = serverPlayers.find(p => p.puuid === playerCopy.puuid); await command.execute(interaction);
if (!player){
console.error(`Tried to remove non-added player ${playerCopy}!`);
return;
}
serverPlayers.splice(serverPlayers.indexOf(player), 1);
this.servers.set(serverID, serverPlayers);
console.log(`Removed ${player}!`);
this.savePlayersToFile();
return player;
} }
async onReady(readyClient: Client<true>){ async onReady(readyClient: Client<true>){

@ -1,33 +1,26 @@
import {Interaction, PermissionsBitField} from "discord.js"; import {ChatInputCommandInteraction, PermissionsBitField} from "discord.js";
import {Command} from "../command"; import {Command} from "../command";
import {UEMEloBot} from "../bot"; import {UEMEloBot} from "../bot";
import {checkServer} from "../util";
class Add extends Command { class Add extends Command {
constructor() { constructor() {
super("add", "Spieler hinzufügen"); super("add", "Spieler hinzufügen");
this.data.addStringOption(option => { this.data.addStringOption(option => {
return option.setName("riot-id").setDescription("Riot ID: 'Name#Tag'").setRequired(true) return option.setName("riot-id").setDescription("Riot ID: 'Name#Tag'").setRequired(true);
}).setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); }).setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator);
} }
async execute(interaction: Interaction) { async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand())
return;
console.log("Add command used"); console.log("Add command used");
let client = interaction.client as UEMEloBot; let client = interaction.client as UEMEloBot;
const riotId = interaction.options.getString("riot-id", true); const riotId = interaction.options.getString("riot-id", true);
const [gameName, tagLine] = riotId.split("#"); const [gameName, tagLine] = riotId.split("#");
const serverID = await checkServer(interaction);
if (!serverID)
return;
const player = await client.addPlayer(gameName, tagLine, serverID); const account = await client.data.addAccount(gameName, tagLine);
if (player) if (account)
await interaction.reply({content: `${player} hinzugefügt!`, ephemeral: true}); await interaction.reply({content: `${account} hinzugefügt!`, ephemeral: true});
else else
await interaction.reply({content: "Fehler!", ephemeral: true}); await interaction.reply({content: "Fehler!", ephemeral: true});
} }

@ -1,41 +1,34 @@
import {Interaction, EmbedBuilder, APIEmbedField, codeBlock} from "discord.js"; import {ChatInputCommandInteraction, EmbedBuilder, APIEmbedField, codeBlock} from "discord.js";
import {Command} from "../command"; import {Command} from "../command";
import {UEMEloBot} from "../bot"; import {UEMEloBot} from "../bot";
import {eloToNumber, Player} from "../player"; import {eloToNumber, Account} from "../account";
import {checkServer} from "../util"; import {checkServer} from "../util";
class List extends Command { class List extends Command {
constructor() { constructor() {
super("list", "Spieler auflisten"); super("list", "Spieler auflisten");
} }
async execute(interaction: Interaction) { async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand())
return;
console.log("List command used"); console.log("List command used");
const serverID = await checkServer(interaction); let accounts = await (interaction.client as UEMEloBot).data.getProgressedAccountList();
if (!serverID) if (accounts.length === 0){
return;
let players = (interaction.client as UEMEloBot).servers.ensure(serverID, () => []);
if (players.length === 0){
await interaction.reply("0 Spieler registriert!"); await interaction.reply("0 Spieler registriert!");
return; return;
} }
const maxNameLength = players.reduce((max: number, next: Player) => Math.max(max, next.toString().length), 0); const maxNameLength = accounts.reduce((max: number, next: Account) => Math.max(max, next.toString().length), 0);
const maxProgressDigits = players.reduce((max, next) => Math.max(max, Math.abs(next.getProgress()).toString().length), 0); const maxProgressDigits = accounts.reduce((max, next) => Math.max(max, Math.abs(next.getProgress()).toString().length), 0);
// sort by progress, then current elo, descending // sort by progress, then current elo, descending
players.sort((a, b) => { accounts.sort((a, b) => {
const diff = b.getProgress() - a.getProgress(); const diff = b.getProgress() - a.getProgress();
if (diff != 0) if (diff != 0)
return diff; return diff;
return eloToNumber(b.currentElo) - eloToNumber(a.currentElo); return eloToNumber(b.currentElo) - eloToNumber(a.currentElo);
}); });
let ranking = players.reduce((before, player, index) => { let ranking = accounts.reduce((before, player, index) => {
const placement = `${index + 1})`.padStart(3, '0'); const placement = `${index + 1})`.padStart(3, '0');
const fill = "-".repeat(maxNameLength - `${player}`.length + 2); const fill = "-".repeat(maxNameLength - `${player}`.length + 2);
const progress = Intl.NumberFormat(undefined, { const progress = Intl.NumberFormat(undefined, {
@ -47,13 +40,13 @@ class List extends Command {
ranking = ranking.trimEnd(); ranking = ranking.trimEnd();
const baseURL = "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-shared-components/global/default/"; const baseURL = "https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-shared-components/global/default/";
const fileName = (players[0].currentElo?.tier.toLowerCase() ?? "unranked") + ".png"; const fileName = (accounts[0].currentElo?.tier.toLowerCase() ?? "unranked") + ".png";
const iconURL = `${baseURL}${fileName}`; const iconURL = `${baseURL}${fileName}`;
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("UEM-Elo-Challenge Ranking") .setTitle("UEM-Elo-Challenge Ranking")
.setFields([ .setFields([
{name: "Leader:", value: players[0].toString()}, {name: "Leader:", value: accounts[0].toString()},
{name: " ", value: codeBlock(ranking)} {name: " ", value: codeBlock(ranking)}
]) ])
.setThumbnail(iconURL); .setThumbnail(iconURL);

@ -1,14 +1,11 @@
import {Interaction} from "discord.js"; import {ChatInputCommandInteraction} from "discord.js";
import {Command} from "../command"; import {Command} from "../command";
class Ping extends Command { class Ping extends Command {
constructor() { constructor() {
super("ping", "Pong"); super("ping", "Pong");
} }
async execute(interaction: Interaction) { async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand())
return;
console.log("Ping command used"); console.log("Ping command used");
await interaction.reply({content: "Pong", ephemeral: true}); await interaction.reply({content: "Pong", ephemeral: true});

@ -1,33 +1,26 @@
import {Interaction, PermissionsBitField} from "discord.js"; import {ChatInputCommandInteraction, PermissionsBitField} from "discord.js";
import {Command} from "../command"; import {Command} from "../command";
import {UEMEloBot} from "../bot"; import {UEMEloBot} from "../bot";
import {checkServer} from "../util";
class Remove extends Command { class Remove extends Command {
constructor() { constructor() {
super("remove", "Spieler entfernen"); super("remove", "Spieler entfernen");
this.data.addStringOption(option => { this.data.addStringOption(option => {
return option.setName("riot-id").setDescription("Riot ID: 'Name#Tag'").setRequired(true) return option.setName("riot-id").setDescription("Riot ID: 'Name#Tag'").setRequired(true);
}).setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); }).setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator);
} }
async execute(interaction: Interaction) { async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand())
return;
console.log("Remove command used"); console.log("Remove command used");
let client = interaction.client as UEMEloBot; let client = interaction.client as UEMEloBot;
const riotId = interaction.options.getString("riot-id", true); const riotId = interaction.options.getString("riot-id", true);
const [gameName, tagLine] = riotId.split("#"); const [gameName, tagLine] = riotId.split("#");
const serverID = await checkServer(interaction);
if (!serverID)
return;
const player = await client.removePlayer(gameName, tagLine, serverID); const account = await client.data.removeAccount(gameName, tagLine);
if (player) if (account)
await interaction.reply({content: `${player} entfernt!`, ephemeral: true}); await interaction.reply({content: `${account} entfernt!`, ephemeral: true});
else else
await interaction.reply({content: "Fehler!", ephemeral: true}); await interaction.reply({content: "Fehler!", ephemeral: true});
} }

@ -1,4 +1,4 @@
import {Interaction} from "discord.js"; import {ChatInputCommandInteraction} from "discord.js";
import {Command} from "../command"; import {Command} from "../command";
import {UEMEloBot} from "../bot"; import {UEMEloBot} from "../bot";
@ -6,16 +6,13 @@ class Update extends Command {
constructor() { constructor() {
super("update", "Ränge jetzt aktualisieren (passiert automatisch alle 20min)"); super("update", "Ränge jetzt aktualisieren (passiert automatisch alle 20min)");
} }
async execute(interaction: Interaction) { async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand())
return;
console.log("Update command used"); console.log("Update command used");
await interaction.deferReply({ephemeral: true}); await interaction.deferReply({ephemeral: true});
let client = interaction.client as UEMEloBot; let client = interaction.client as UEMEloBot;
await client.updatePlayers(); await client.data.updateAllAccounts();
await interaction.editReply({content: "Aktualisiert!"}); await interaction.editReply({content: "Aktualisiert!"});
} }

@ -0,0 +1,98 @@
import {Account} from "./account";
import {getMySQLConnection} from "./util";
import {RowDataPacket} from "mysql2/promise";
import {Collection} from "discord.js";
export class DataManager {
async updateAllAccounts(){
let accounts = await this.getProgressedAccountList();
for (let account of accounts){
await account.update();
}
}
async addAccount(gameName: string, tagLine: string) {
let account = await Account.fetchFromRiotID(gameName, tagLine);
if (!account)
return;
if (!await account.isAdded()){
await account.addToDB();
await account.update();
return account;
} else {
console.error(`Account ${account} already added.`);
return;
}
}
async removeAccount(gameName: string, tagLine: string) {
let account = await Account.fetchFromRiotID(gameName, tagLine);
if (!account)
return;
if (await account.isAdded()){
await account.removeFromDB();
return account;
} else {
console.error(`Account ${account} was not added.`);
return;
}
}
async getAccountMap(){
const conn = await getMySQLConnection();
const [sqlAccounts] = await conn.query<RowDataPacket[]>(`SELECT * FROM accounts`);
let accounts = new Collection<string, Account>();
for (const sqlAccount of sqlAccounts){
let account = new Account(sqlAccount["puuid"]);
account.gameName = sqlAccount["gameName"];
account.tagLine = sqlAccount["tagLine"];
accounts.set(account.puuid, account);
}
return accounts;
}
async getProgressedAccountList() {
const conn = await getMySQLConnection();
const [[, starts, ends]] = await conn.query<RowDataPacket[][]>(`
CREATE TEMPORARY TABLE limits AS
SELECT MIN(date) AS start, MAX(date) AS end, accountId, gameName, tagLine, puuid
FROM accounts JOIN elo_entries ON id = accountId
GROUP BY accountId, gameName, tagLine, puuid;
SELECT tier, \`rank\`, points, gameName, tagLine, puuid
FROM elo_entries JOIN limits ON elo_entries.date = start AND limits.accountId = elo_entries.accountId;
SELECT tier, \`rank\`, points, gameName, tagLine, puuid
FROM elo_entries JOIN limits ON elo_entries.date = end AND limits.accountId = elo_entries.accountId;
`);
let accounts = await this.getAccountMap();
for (const start of starts){
let account = accounts.get(start["puuid"]) as Account;
account.startElo = {
rank: start["rank"],
tier: start["tier"],
points: start["points"]
};
}
for (const end of ends){
let account = accounts.get(end["puuid"]) as Account;
account.currentElo = {
rank: end["rank"],
tier: end["tier"],
points: end["points"]
};
}
return Array.from(accounts.values());
}
}

@ -10,4 +10,5 @@ readline.createInterface({input: process.stdin}).on('line', async line => {
const client = new UEMEloBot(); const client = new UEMEloBot();
process.on("SIGINT", () => client.stop()); process.on("SIGINT", () => client.stop());

@ -1,144 +0,0 @@
import {PlatformId, RiotAPI, RiotAPITypes} from "@fightmegg/riot-api"
import AccountDTO = RiotAPITypes.Account.AccountDTO;
const riotAPI = new RiotAPI(process.env["RIOT_API_KEY"] as string);
export class Player {
puuid: string
gameName?: string
tagLine?: string
startElo?: Elo
currentElo?: Elo
constructor(puuid: string) {
this.puuid = puuid;
}
static Load(raw: any): Player {
let p = new Player(raw.puuid);
p.startElo = raw.startElo;
p.gameName = raw.gameName;
p.tagLine = raw.tagLine;
return p;
}
static async TryCreateFrom(gameName: string, tagLine: string) {
let account: AccountDTO;
try {
account = await riotAPI.account.getByRiotId({
region: PlatformId.EUROPE,
gameName: gameName,
tagLine: tagLine
});
} catch (error) {
console.error(`Riot ID not found: ${gameName}#${tagLine}!`);
return;
}
let p = new Player(account.puuid);
p.gameName = account.gameName;
p.tagLine = account.tagLine;
await p.updateCurrentElo();
p.startElo = p.currentElo;
return p;
}
async updateFullName(){
const account = await riotAPI.account.getByPUUID({
region: PlatformId.EUROPE,
puuid: this.puuid
});
this.gameName = account.gameName;
this.tagLine = account.tagLine;
}
async updateCurrentElo() {
const tryRegion = async (region: RiotAPITypes.LoLRegion) => {
return await riotAPI.league.getEntriesBySummonerId({
region: region,
summonerId: (await riotAPI.summoner.getByPUUID({
region: region,
puuid: this.puuid
})).id
});
};
let entries;
try {
entries = await tryRegion(PlatformId.EUW1);
} catch (error){
try {
entries = await tryRegion(PlatformId.EUNE1);
} catch (error){
let response = error as Response;
console.error(await response.json());
return;
}
}
let soloQ = entries.find(e => e.queueType == "RANKED_SOLO_5x5");
if (soloQ && ((soloQ.wins + soloQ.losses) >= 5)){
this.currentElo = {
tier: soloQ.tier,
rank: soloQ.rank,
points: soloQ.leaguePoints
};
}
}
getProgress(){
return eloToNumber(this.currentElo) - eloToNumber(this.startElo);
}
toString() {
return `${this.gameName}#${this.tagLine}`;
}
// noinspection JSUnusedGlobalSymbols
toJSON(){
return {
puuid: this.puuid,
gameName: this.gameName,
tagLine: this.tagLine,
startElo: this.startElo
}
}
}
interface Elo {
tier: string,
rank: string,
points: number
}
export function eloToNumber(elo: Elo | undefined){
if (!elo)
return 0;
let tiers: Record<string, number> = {
"CHALLENGER": 2800,
"GRANDMASTER": 2800,
"MASTER": 2800,
"DIAMOND": 2400,
"EMERALD": 2000,
"PLATINUM": 1600,
"GOLD": 1200,
"SILVER": 800,
"BRONZE": 400,
"IRON": 0,
};
let ranks: Record<string, number> = {
"I": 300,
"II": 200,
"III": 100,
"IV": 0,
};
let tier = elo.tier;
if (tier === "MASTER" || tier === "GRANDMASTER" || tier === "CHALLENGER")
return tiers[tier] + elo.points;
return tiers[tier] + ranks[elo.rank] + elo.points;
}

@ -1,4 +1,5 @@
import {ChatInputCommandInteraction} from "discord.js"; import {ChatInputCommandInteraction} from "discord.js";
import mysql from "mysql2/promise";
export async function checkServer(interaction: ChatInputCommandInteraction){ export async function checkServer(interaction: ChatInputCommandInteraction){
const serverID = interaction.guild?.id; const serverID = interaction.guild?.id;
@ -22,4 +23,14 @@ console.log = function(...args) {
console.error = function(...args) { console.error = function(...args) {
originalError(`[${getTimestamp()}]`, ...args); originalError(`[${getTimestamp()}]`, ...args);
}; };
export async function getMySQLConnection(){
return await mysql.createConnection({
port: parseInt(process.env["MYSQL_PORT"] ?? "3306"),
user: process.env["MYSQL_USER"],
password: process.env["MYSQL_PASSWORD"],
database: process.env["MYSQL_DATABASE"],
multipleStatements: true
});
}
Loading…
Cancel
Save