├── .eslintignore ├── migrations ├── add_subrole.js ├── make_locale_longer.js ├── add_thresh.js ├── add_mmr.js ├── botspam.js ├── add_announce.js ├── add_locale.js ├── 20180608-modify_users.js ├── subs.js ├── timeouts.js ├── fix_trivia.js ├── fixup_subs.js ├── add_trivia.js ├── doublecheck.js ├── migrate.js ├── transform_items.js └── users.js ├── commands ├── utility │ ├── pong.js │ ├── ping.js │ └── cleanup.js ├── meta │ ├── invite.js │ ├── botstats.js │ ├── info.js │ └── help.js ├── fun │ ├── flip.js │ ├── mike.js │ ├── arteezy.js │ ├── roll.js │ ├── courage.js │ └── trivia.js ├── owner │ ├── shardinfo.js │ ├── botcheck.js │ ├── restart.js │ ├── usage.js │ └── eval.js ├── static │ ├── talents.js │ ├── hero.js │ ├── patch.js │ ├── ability.js │ └── item.js ├── personal │ ├── unregister.js │ ├── nextmatch.js │ ├── badge.js │ ├── matchinfo.js │ ├── profile.js │ ├── register.js │ ├── lastmatch.js │ ├── matches.js │ ├── twenty.js │ └── history.js └── esports │ ├── twitch.js │ ├── unsub.js │ ├── prommr.js │ ├── sub.js │ └── live.js ├── json ├── heroes.js ├── items.js ├── keys.js ├── courage.json ├── abilities.js ├── feeds.json └── mike.json ├── util ├── transformConstants.js ├── checkDotaID.js ├── checkDiscordID.js ├── queryString.js ├── findHero.js ├── resolveVanityURL.js ├── getBuffer.js ├── eat.js ├── searchMembers.js └── genQuestions.js ├── contributors.json ├── embeds ├── allMmr.js ├── patch.js ├── talents.js ├── historyWith.js ├── streams.js ├── prommr.js ├── matches.js ├── ability.js ├── item.js ├── hero.js ├── singleMmr.js ├── liveMatch.js ├── playerinfo.js └── match.js ├── classes ├── usage.js ├── strings.js ├── reactionChooser.js ├── unwatcher.js ├── SteppedList.js ├── watcher.js ├── trivia.js └── LeagueUtils.js ├── apps.json ├── readme.md ├── templates ├── shardinfo.js └── stats.js ├── .eslintrc.json ├── dbots ├── post.js └── gen.js ├── start.js ├── package.json ├── helper.js ├── .gitignore └── services ├── feeds.js └── matches.js /.eslintignore: -------------------------------------------------------------------------------- 1 | migrations 2 | dbots 3 | -------------------------------------------------------------------------------- /migrations/add_subrole.js: -------------------------------------------------------------------------------- 1 | module.exports = (pg) => { 2 | return pg.query("alter table guilds add column subrole bigint default 0;"); 3 | } -------------------------------------------------------------------------------- /migrations/make_locale_longer.js: -------------------------------------------------------------------------------- 1 | module.exports = (pg) => { 2 | return pg.query("ALTER TABLE public.guilds ALTER COLUMN locale TYPE text;"); 3 | } -------------------------------------------------------------------------------- /migrations/add_thresh.js: -------------------------------------------------------------------------------- 1 | module.exports = (pg) => { 2 | return pg.query("ALTER TABLE guilds ADD COLUMN threshold INT; UPDATE guilds SET threshold = 5;"); 3 | }; 4 | -------------------------------------------------------------------------------- /migrations/add_mmr.js: -------------------------------------------------------------------------------- 1 | function add_mmr(pg) { 2 | return pg.query("ALTER TABLE users ADD scr INT, ADD cr INT, ADD sat TEXT;"); 3 | } 4 | 5 | module.exports = add_mmr; 6 | -------------------------------------------------------------------------------- /commands/utility/pong.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | return ctx.send(ctx.strings.get("pong", ctx.guild.shard.latency)); 3 | } 4 | 5 | module.exports = { 6 | name: "pong", 7 | category: "utility", 8 | exec 9 | }; 10 | -------------------------------------------------------------------------------- /json/heroes.js: -------------------------------------------------------------------------------- 1 | const hero_abilities = require("dotaconstants").hero_abilities; 2 | module.exports = Object.values(require("dotaconstants").heroes).map((hero) => { 3 | hero.abilities = hero_abilities[hero.name].abilities; 4 | return hero; 5 | }); 6 | -------------------------------------------------------------------------------- /migrations/botspam.js: -------------------------------------------------------------------------------- 1 | module.exports = (pg) => { 2 | pg.query("ALTER TABLE guilds ADD botspam BIGINT; UPDATE guilds SET botspam = 0;").then((res) => { 3 | console.log(res); 4 | }).catch((err) => { 5 | console.log(err) 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /migrations/add_announce.js: -------------------------------------------------------------------------------- 1 | module.exports = (client) => { 2 | client.pg.query("ALTER TABLE guilds ADD announce bigint; UPDATE guilds SET announce = 0;").then(res => { 3 | console.log(res); 4 | }).catch(err => { 5 | console.log(err); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /migrations/add_locale.js: -------------------------------------------------------------------------------- 1 | module.exports = (client) => { 2 | client.pg.query("ALTER TABLE guilds ADD locale varchar(2); UPDATE guilds SET locale = 'en';").then(res => { 3 | console.log(res); 4 | }).catch(err => { 5 | console.log(err); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /commands/utility/ping.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | let msg = await ctx.send(ctx.message.timestamp); 3 | return msg.edit(ctx.strings.get("ping_edit", msg.timestamp - ctx.message.timestamp)); 4 | } 5 | 6 | module.exports = { 7 | name: "ping", 8 | category: "utility", 9 | exec 10 | }; 11 | -------------------------------------------------------------------------------- /migrations/20180608-modify_users.js: -------------------------------------------------------------------------------- 1 | module.exports = (client) => { 2 | client.pg.query("ALTER TABLE users ALTER COLUMN steamid DROP NOT NULL; ALTER TABLE users ALTER COLUMN dotaid DROP NOT NULL;").then(res => { 3 | console.log(res); 4 | }).catch(err => { 5 | console.log(err); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /commands/meta/invite.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | return ctx.send(``); 3 | } 4 | 5 | module.exports = { 6 | name: "invite", 7 | category: "meta", 8 | exec 9 | }; 10 | -------------------------------------------------------------------------------- /commands/fun/flip.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | let coin = Math.floor(Math.random() * 2); 3 | 4 | return ctx.send(`**${ctx.member.nick || ctx.member.username}** flipped a coin: ***${coin === 0 ? "HEADS" : "TAILS"}***`); 5 | } 6 | 7 | module.exports = { 8 | name: "flip", 9 | category: "fun", 10 | exec 11 | } -------------------------------------------------------------------------------- /json/items.js: -------------------------------------------------------------------------------- 1 | var items = require("dotaconstants").items; 2 | for (item in items) { 3 | if (item.startsWith("recipe")) continue; 4 | if (items[`recipe_${item}`] && items[item].created) items[item].components.push(`recipe_${item}`); 5 | } 6 | 7 | items = require("../util/transformConstants")(items); 8 | 9 | module.exports = items; 10 | -------------------------------------------------------------------------------- /util/transformConstants.js: -------------------------------------------------------------------------------- 1 | function transformConstants(constant) { 2 | let keys = Object.keys(constant); 3 | let values = Object.values(constant); 4 | 5 | return keys.map((key, index) => { 6 | let value = values[index]; 7 | value.name = key; 8 | return value; 9 | }); 10 | } 11 | 12 | module.exports = transformConstants; 13 | -------------------------------------------------------------------------------- /migrations/subs.js: -------------------------------------------------------------------------------- 1 | module.exports = (pg) => { 2 | let q = [ 3 | "CREATE TABLE subs (", 4 | "mess TEXT NOT NULL,", 5 | "owner BIGINT NOT NULL,", 6 | "channel BIGINT NOT NULL,", 7 | "type TEXT NOT NULL,", 8 | "value TEXT NOT NULL,", 9 | "PRIMARY KEY (mess)", 10 | ");" 11 | ].join(" "); 12 | 13 | return pg.query(q); 14 | }; 15 | -------------------------------------------------------------------------------- /json/keys.js: -------------------------------------------------------------------------------- 1 | const hero_abilities = require("dotaconstants").hero_abilities; 2 | const letters = ["q", "w", "e", "d", "f", "r"]; 3 | 4 | let keys = {}; 5 | 6 | for (let hero_name of Object.keys(hero_abilities)) { 7 | let hero = hero_abilities[hero_name]; 8 | for (let index in hero.abilities) { 9 | keys[hero.abilities[index]] = letters[index]; 10 | } 11 | } 12 | 13 | module.exports = keys; 14 | 15 | -------------------------------------------------------------------------------- /contributors.json: -------------------------------------------------------------------------------- 1 | { 2 | "info_alpha_testers": [ 3 | "Laura Dickinson", 4 | "MT5A Server" 5 | ], 6 | "info_translators": [ 7 | "Illia K.", 8 | "Mateusz Brawański", 9 | "Zerodrink", 10 | "Snickeor", 11 | "Mustafa Mert Kızılırmak", 12 | "Hayden P.", 13 | "Gabriel Quezada", 14 | "Ruben Dahl", 15 | "Adrian Hassa" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /commands/owner/shardinfo.js: -------------------------------------------------------------------------------- 1 | const template = require("../../templates/shardinfo"); 2 | 3 | async function exec(ctx) { 4 | return ctx.send({ 5 | content: `I am shard ${ctx.guild.shard.id + 1} of ${ctx.client.shards.size}.`, 6 | embed: { 7 | description: template(ctx.client) 8 | } 9 | }); 10 | } 11 | 12 | module.exports = { 13 | name: "shardinfo", 14 | category: "owner", 15 | exec 16 | } -------------------------------------------------------------------------------- /embeds/allMmr.js: -------------------------------------------------------------------------------- 1 | function allMmr(list, members, name) { 2 | let earliest = list.slice().sort((a, b) => a.sat - b.sat)[0].sat; 3 | return { 4 | "author": { 5 | "name": this.get("mmr_all_title", name) 6 | }, 7 | "timestamp": new Date(parseInt(earliest)), 8 | "footer": { 9 | "text": this.get("prommr_last_updated") 10 | } 11 | }; 12 | } 13 | 14 | module.exports = allMmr; 15 | -------------------------------------------------------------------------------- /commands/fun/mike.js: -------------------------------------------------------------------------------- 1 | const mike = require("../../json/mike.json"); 2 | 3 | async function exec(ctx) { 4 | return ctx.send(mike[Math.floor(Math.random() * mike.length)]).then((msg) => { 5 | if (msg.channel.guild.id == "137589613312081920") msg.addReaction("ixmikeW:256896118380691466"); 6 | }); 7 | } 8 | 9 | module.exports = { 10 | name: "mike", 11 | category: "fun", 12 | aliases: ["ixmike", "miek"], 13 | exec 14 | }; 15 | -------------------------------------------------------------------------------- /commands/fun/arteezy.js: -------------------------------------------------------------------------------- 1 | const arteezy = require("../../json/arteezy.json"); 2 | 3 | async function exec(ctx) { 4 | return ctx.send(arteezy[Math.floor(Math.random() * arteezy.length)]).then((msg) => { 5 | if (msg.channel.guild.id == "137589613312081920") msg.addReaction("rtzW:302222677991620608"); 6 | }); 7 | } 8 | 9 | module.exports = { 10 | name: "arteezy", 11 | category: "fun", 12 | aliases: ["rtz"], 13 | exec 14 | }; 15 | -------------------------------------------------------------------------------- /commands/meta/botstats.js: -------------------------------------------------------------------------------- 1 | const statsEmbed = require("../../templates/stats"); 2 | 3 | async function exec(ctx) { 4 | try { 5 | let embed = await statsEmbed(ctx.client); 6 | return ctx.embed(embed); 7 | } catch (err) { 8 | ctx.error(err); 9 | return ctx.send("Something went horribly wrong."); 10 | } 11 | } 12 | 13 | module.exports = { 14 | name: "botstats", 15 | category: "meta", 16 | exec 17 | }; 18 | -------------------------------------------------------------------------------- /classes/usage.js: -------------------------------------------------------------------------------- 1 | class Usage { 2 | constructor(stats) { 3 | if (stats) { 4 | this._stats = stats; 5 | } else { 6 | this._stats = { "all": 0 }; 7 | } 8 | } 9 | 10 | increment(field) { 11 | this._stats[field] ? this._stats[field] += 1 : this._stats[field] = 1; 12 | this._stats.all += 1; 13 | } 14 | 15 | get stats() { 16 | return this._stats; 17 | } 18 | } 19 | 20 | module.exports = Usage; 21 | -------------------------------------------------------------------------------- /util/checkDotaID.js: -------------------------------------------------------------------------------- 1 | async function checkDotaID(pg, id) { 2 | try { 3 | let res = await pg.query({ 4 | "text": "SELECT * FROM public.users WHERE dotaid = $1", 5 | "values": [id] 6 | }); 7 | 8 | if (res.rows.length) { 9 | return Promise.resolve(res.rows[0].id); 10 | } else { 11 | return Promise.resolve(null); 12 | } 13 | } catch (err) { 14 | return Promise.reject(err); 15 | } 16 | } 17 | 18 | module.exports = checkDotaID; 19 | -------------------------------------------------------------------------------- /util/checkDiscordID.js: -------------------------------------------------------------------------------- 1 | async function checkDiscordID(pg, id) { 2 | try { 3 | let res = await pg.query({ 4 | "text": "SELECT * FROM public.users WHERE id = $1", 5 | "values": [id] 6 | }); 7 | 8 | if (res.rows.length && res.rows[0].dotaid) { 9 | return Promise.resolve(res.rows[0].dotaid); 10 | } else { 11 | return Promise.resolve(null); 12 | } 13 | } catch (err) { 14 | return Promise.reject(err); 15 | } 16 | } 17 | 18 | module.exports = checkDiscordID; 19 | -------------------------------------------------------------------------------- /migrations/timeouts.js: -------------------------------------------------------------------------------- 1 | module.exports = (client) => { 2 | client.pg.query("SELECT * FROM guilds;").then(res => { 3 | res.rows.forEach(row => { 4 | if (row.climit != 0 || row.mlimit != 0) { 5 | client.pg.query({ 6 | "text": "UPDATE guilds SET climit = $1, mlimit = $2 WHERE id = $3;", 7 | "values": [row.climit / 1000, row.mlimit / 1000, row.id] 8 | }).then(res => { 9 | console.log(row.id); 10 | }) 11 | } 12 | }) 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /embeds/patch.js: -------------------------------------------------------------------------------- 1 | function patchEmbed(changes, hero, version) { 2 | return { 3 | "author": { 4 | "name": hero.local, 5 | "url": `http://dota2.gamepedia.com/${hero.local.replace(/ /g, "_")}`, 6 | "icon_url": `http://cdn.dota2.com/apps/dota2/images/heroes/${hero.name}_vert.jpg` 7 | }, 8 | "footer": { 9 | "text": `Changes from ${version}` 10 | }, 11 | "fields": [{ 12 | "name": "Changes", 13 | "value": changes.join("\n") 14 | }] 15 | }; 16 | } 17 | 18 | module.exports = patchEmbed; 19 | -------------------------------------------------------------------------------- /util/queryString.js: -------------------------------------------------------------------------------- 1 | function queryString(options) { 2 | let str = []; 3 | for (let p in options) { 4 | if (options.hasOwnProperty(p)) { 5 | if (Array.isArray(options[p])) { 6 | for (let index of options[p]) { 7 | str.push(`${encodeURIComponent(p)}=${encodeURIComponent(index)}`); 8 | } 9 | } else { 10 | str.push(`${encodeURIComponent(p)}=${encodeURIComponent(options[p])}`); 11 | } 12 | } 13 | } 14 | return `?${str.join("&")}`; 15 | } 16 | 17 | module.exports = queryString; 18 | -------------------------------------------------------------------------------- /util/findHero.js: -------------------------------------------------------------------------------- 1 | const FuzzySet = require("fuzzyset.js"); 2 | const aliases = require("../json/aliases.json"); 3 | 4 | const fuzzy = FuzzySet([].concat(...aliases.map((hero) => { 5 | return [hero.name].concat(hero.aliases); 6 | }))); 7 | 8 | function findHero(string) { 9 | let exact = aliases.find((hero) => ~hero.aliases.indexOf(string)); 10 | if (exact) return exact; 11 | 12 | let match = fuzzy.get(string); 13 | if (match && match[0] && match[0][0] >= 0.5) return aliases.find((hero) => ~hero.aliases.indexOf(match[0][1])); 14 | 15 | return false; 16 | } 17 | 18 | module.exports = findHero; 19 | -------------------------------------------------------------------------------- /apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": [{ 3 | "name": "feeds", 4 | "script": "services/feeds.js", 5 | "instances": 1, 6 | "exec_mode": "fork" 7 | }, { 8 | "name": "matches", 9 | "script": "services/matches.js", 10 | "instances": 1, 11 | "exec_mode": "fork" 12 | }, { 13 | "name": "steam", 14 | "script": "services/steam.js", 15 | "instances": 1, 16 | "exec_mode": "fork" 17 | }], 18 | "bot": { 19 | "name": "listen", 20 | "script": "./bot.js", 21 | "instances": 1, 22 | "exec_mode": "fork" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /migrations/fix_trivia.js: -------------------------------------------------------------------------------- 1 | module.exports = (pg) => { 2 | return pg.query([ 3 | "ALTER TABLE guilds ALTER COLUMN trivia SET DEFAULT 0;", 4 | "ALTER TABLE guilds ALTER COLUMN prefix SET DEFAULT '--';", 5 | "ALTER TABLE guilds ALTER COLUMN climit SET DEFAULT 0;", 6 | "ALTER TABLE guilds ALTER COLUMN mlimit SET DEFAULT 0;", 7 | "ALTER TABLE guilds ALTER COLUMN locale SET DEFAULT 'en';", 8 | "ALTER TABLE guilds ALTER COLUMN botspam SET DEFAULT 0;", 9 | "UPDATE guilds SET trivia = 0 WHERE trivia IS NULL;", 10 | "UPDATE guilds SET botspam = 0 WHERE botspam IS NULL;" 11 | ].join(" ")); 12 | } 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # listen-bot 2 | 3 | A bot with various Dota 2 related commands. 4 | 5 | [Invite the bot to your server](https://discordapp.com/oauth2/authorize?permissions=19456&scope=bot&client_id=240209888771309568) 6 | 7 | [Join our help server](https://discord.gg/qRE5aWh) 8 | 9 | `--help` responds with help text for all the commands. 10 | 11 | `--patch `: 12 | 13 | ![patch spirit breaker](http://i.imgur.com/CtKLLAt.gif) 14 | 15 | You can also pass in a version number between 6.79 and the latest version to get the patch notes for that version. 16 | 17 | `--talents alchemist`: 18 | 19 | ![talents alchemist](http://i.imgur.com/L9qWPgy.gif) 20 | -------------------------------------------------------------------------------- /templates/shardinfo.js: -------------------------------------------------------------------------------- 1 | module.exports = (client) => { 2 | let shard_info_list = [`\`\`\`groovy\nShards: ${client.shards.size}`]; 3 | let users = new Array(client.shards.size).fill(0); 4 | let guilds = new Array(client.shards.size).fill(0); 5 | client.guilds.forEach(guild => { 6 | users[guild.shard.id] += guild.members.size; 7 | guilds[guild.shard.id] += 1; 8 | }); 9 | client.shards.forEach(shard => { 10 | shard_info_list.push(`Shard ${shard.id}: ${guilds[shard.id]} guilds, ${users[shard.id]} users, ${shard.latency} ms`); 11 | }); 12 | shard_info_list.push("```"); 13 | return shard_info_list.join("\n"); 14 | }; 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4 15 | ], 16 | "quotes": [ 17 | "error", 18 | "double" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ], 24 | "no-console": [ 25 | "error", { 26 | "allow": ["warn", "error", "log"] 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /commands/utility/cleanup.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | try { 3 | let ids = ctx.channel.messages 4 | .filter((message) => message.author.id == ctx.client.user.id) 5 | .map((message) => message.id); 6 | 7 | for (let id in ids) { 8 | try { await ctx.client.deleteMessage(ctx.channel.id, id) } catch (err) {} 9 | } 10 | 11 | return ctx.delete(5000, ":white_check_mark:"); 12 | } catch (err) { 13 | ctx.error(err); 14 | return ctx.failure(ctx.strings.get("bot_generic_error")); 15 | } 16 | } 17 | 18 | module.exports = { 19 | name: "cleanup", 20 | category: "utility", 21 | exec 22 | }; 23 | -------------------------------------------------------------------------------- /dbots/post.js: -------------------------------------------------------------------------------- 1 | const needle = require("needle"); 2 | const util = require("util"); 3 | 4 | module.exports = client => { 5 | needle.post("https://discord.bots.gg/api/v1/bots/240209888771309568/stats", 6 | JSON.stringify({ serverCount: client.guilds.size, shardCount: client.shards.size }), 7 | { "headers": { "Authorization": client.config.dbots_token, "Content-Type": "application/json" }}, 8 | (err, resp) => { 9 | if (err) { 10 | util.log(err); 11 | return; 12 | } else if (resp.statusCode != 200) { 13 | util.log(resp.statusCode); 14 | } 15 | } 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /json/courage.json: -------------------------------------------------------------------------------- 1 | ["blink", "mekansm", "vladmir", "pipe", "sheepstick", "bloodthorn", "echo_sabre", "cyclone", "aether_lens", "force_staff", "hurricane_pike", "dagon", "necronomicon", "ultimate_scepter", "refresher", "assault", "heart", "black_king_bar", "shivas_guard", "bloodstone", "sphere", "lotus_orb", "crimson_guard", "blade_mail", "rapier", "monkey_king_bar", "radiance", "butterfly", "greater_crit", "basher", "bfury", "manta", "dragon_lance", "armlet", "silver_edge", "sange_and_yasha", "satanic", "mjollnir", "skadi", "helm_of_the_dominator", "desolator", "diffusal_blade", "ethereal_blade", "octarine_core", "ancient_janggo", "solar_crest", "veil_of_discord", "rod_of_atos", "abyssal_blade", "heavens_halberd"] 2 | -------------------------------------------------------------------------------- /commands/owner/botcheck.js: -------------------------------------------------------------------------------- 1 | async function checks(client, member) { 2 | return member.id == client.config.owner; 3 | } 4 | 5 | async function exec(ctx) { 6 | const ID = ctx.options[0]; 7 | if (ID) { 8 | let guild = ctx.client.guilds.get(ID); 9 | if (guild) { 10 | ctx.send(`${guild.members.filter(member => member.bot).length} bots / ${guild.members.size} members`); 11 | } else { 12 | ctx.send("Can't find guild."); 13 | } 14 | } else { 15 | ctx.send("No guild."); 16 | } 17 | } 18 | 19 | module.exports = { 20 | name: "botcheck", 21 | category: "owner", 22 | ignoreCooldowns: true, 23 | checks, 24 | exec 25 | }; 26 | -------------------------------------------------------------------------------- /embeds/talents.js: -------------------------------------------------------------------------------- 1 | const levels = [25, 20, 15, 10]; 2 | 3 | function talentEmbed(data) { 4 | let talents = []; 5 | 6 | while (data.talents.length) { 7 | talents.push(data.talents.splice(0, 2)); 8 | } 9 | 10 | talents.reverse(); 11 | 12 | return { 13 | author: { 14 | name: data.hero.local, 15 | url: `http://dota2.gamepedia.com/${data.hero.local.replace(/ /g, "_")}#Talents`, 16 | icon_url: `http://cdn.dota2.com/apps/dota2/images/heroes/${data.hero.name}_icon.png` 17 | }, 18 | title: "Talents", 19 | description: talents.map((row, index) => `**${levels[index]}:** ${row[1].dname} *or* ${row[0].dname}`).join("\n") 20 | }; 21 | } 22 | 23 | module.exports = talentEmbed; 24 | -------------------------------------------------------------------------------- /json/abilities.js: -------------------------------------------------------------------------------- 1 | var abilities = require("dotaconstants").abilities; 2 | const akeys = require("./keys.js"); 3 | const heroes = require("./aliases.json"); 4 | 5 | for (akey in akeys) { 6 | abilities[akey].key = akeys[akey]; 7 | } 8 | 9 | let keys = Object.keys(abilities); 10 | for (hero of heroes) { 11 | let searchstr = hero.name == "sand_king" ? "sandking" : hero.name; // valve 12 | let filtered = keys.filter((key) => key.startsWith(searchstr)); 13 | filtered.forEach((key) => { 14 | abilities[key].hero = hero; 15 | }); 16 | } 17 | 18 | abilities = require("../util/transformConstants")(abilities); 19 | abilities = abilities.filter((ability) => ability.hero && !ability.name.startsWith("special_bonus")); 20 | 21 | module.exports = abilities; 22 | -------------------------------------------------------------------------------- /migrations/fixup_subs.js: -------------------------------------------------------------------------------- 1 | async function fixupSubs(pg) { 2 | await pg.query("ALTER TABLE subs RENAME TO subsold;"); 3 | await pg.query([ 4 | "CREATE TABLE subs (", 5 | " owner BIGINT NOT NULL,", 6 | " channel BIGINT NOT NULL,", 7 | " type TEXT NOT NULL,", 8 | " value TEXT NOT NULL,", 9 | " CONSTRAINT subs_const unique (owner,channel,type,value)", 10 | ");" 11 | ].join(" ")); 12 | let old = await pg.query("SELECT * FROM subsold;"); 13 | let promises = old.rows.map((row) => pg.query({ 14 | text: "INSERT INTO subs VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING;", 15 | values: [row.owner, row.channel, row.type, row.value] 16 | })); 17 | let results = await Promise.all(promises); 18 | return Promise.resolve(results.length); 19 | } 20 | 21 | module.exports = fixupSubs; 22 | -------------------------------------------------------------------------------- /util/resolveVanityURL.js: -------------------------------------------------------------------------------- 1 | const needle = require("needle"); 2 | const steamKey = require("../config.json").steam_key; 3 | const queryString = require("./queryString"); 4 | 5 | function resolveVanityURL(name) { 6 | return new Promise((resolve, reject) => { 7 | let options = { 8 | key: steamKey, 9 | vanityurl: name 10 | }; 11 | 12 | needle.get(`http://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/${queryString(options)}`, (err, response) => { 13 | if (err) reject(err); 14 | let body = response.body.response; // thanks gabe 15 | if (!body) reject("what"); 16 | if (body.success == 1) { 17 | resolve(body.steamid); 18 | } else { 19 | reject(body); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | module.exports = resolveVanityURL; 26 | -------------------------------------------------------------------------------- /commands/fun/roll.js: -------------------------------------------------------------------------------- 1 | function sleep(ms) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, ms); 4 | }); 5 | } 6 | 7 | async function exec(ctx) { 8 | let min = 1; 9 | let max = 100; 10 | 11 | if (!isNaN(parseInt(ctx.options[0]))) { 12 | max = parseInt(ctx.options[0]); 13 | } 14 | 15 | if (!isNaN(parseInt(ctx.options[1]))) { 16 | min = max; 17 | max = parseInt(ctx.options[1]); 18 | } 19 | 20 | if (!ctx.options[1]) { 21 | min = 1; 22 | } 23 | 24 | let msg = await ctx.send("🎰🎰🎰"); 25 | 26 | let rand = Math.floor(Math.random() * (max - min)) + min; 27 | 28 | await sleep(1000); 29 | 30 | return msg.edit(`**${ctx.member.nick || ctx.member.username}** rolls (${min}/${max}): \`${rand}\``); 31 | } 32 | 33 | module.exports = { 34 | name: "roll", 35 | category: "fun", 36 | exec 37 | } -------------------------------------------------------------------------------- /commands/owner/restart.js: -------------------------------------------------------------------------------- 1 | const spawn = require("child_process").spawn; 2 | 3 | async function checks(client, member) { 4 | return member.id == client.config.owner; 5 | } 6 | 7 | async function exec(ctx) { 8 | let tasks = []; 9 | 10 | ctx.client.shards.forEach(shard => { 11 | tasks.push(shard.editStatus("invisible")); 12 | }); 13 | 14 | ctx.client.trivia.channels.forEach(channel => { 15 | tasks.push(ctx.client.createMessage(channel, ctx.strings.get("restart_trivia_message"))); 16 | }); 17 | 18 | await Promise.all(tasks); 19 | await ctx.send(`${ctx.client.shards.size} invised, ${tasks.length - ctx.client.shards.size} trivia games shut down`); 20 | await ctx.send(`${tasks.length} tasks complete, restarting`); 21 | spawn("./update.sh"); 22 | } 23 | 24 | module.exports = { 25 | name: "restart", 26 | category: "owner", 27 | checks, 28 | exec 29 | }; 30 | -------------------------------------------------------------------------------- /embeds/historyWith.js: -------------------------------------------------------------------------------- 1 | function historyWithEmbed(data) { 2 | return { 3 | title: this.get("history_with_title", data.p1_name, data.p2_name), 4 | fields: [{ 5 | name: this.get("history_with_with"), 6 | value: [ 7 | `**${this.get("history_with_winloss")}** ${data.winwith}/${data.with - data.winwith} (${data.with} games)`, 8 | `**${this.get("history_as_winrate")}:** ${Math.round(data.winwith / data.with * 10000) / 100}%` 9 | ].join("\n"), 10 | inline: true 11 | }, { 12 | name: this.get("history_with_against"), 13 | value: [ 14 | `**${data.p1_name}'s wins:** ${data.against[data.p1]}`, 15 | `**${data.p2_name}'s wins:** ${data.against[data.p2]}` 16 | ].join("\n"), 17 | inline: false 18 | }] 19 | }; 20 | } 21 | 22 | module.exports = historyWithEmbed; 23 | -------------------------------------------------------------------------------- /migrations/add_trivia.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | 3 | function add_trivia(pg) { 4 | pg.query("ALTER TABLE guilds ADD trivia bigint; UPDATE guilds SET trivia = 0;").then(res => { 5 | util.log(res); 6 | }).catch(err => { 7 | util.log(err); 8 | }); 9 | } 10 | 11 | function add_scores(pg) { 12 | pg.query([ 13 | "CREATE TABLE scores (", 14 | "id BIGINT,", 15 | "score BIGINT,", 16 | "PRIMARY KEY (id)", 17 | ");" 18 | ].join(" ")).then(res => { 19 | util.log(res); 20 | }).catch(err => { 21 | util.log(err); 22 | }); 23 | } 24 | 25 | function add_streaks(pg) { 26 | pg.query("ALTER TABLE scores ADD streak bigint; UPDATE scores SET streak = 0;").then(res => { 27 | util.log(res); 28 | }).catch(err => { 29 | util.log(err); 30 | }); 31 | } 32 | 33 | module.exports = { 34 | "add_trivia": add_trivia, 35 | "add_scores": add_scores, 36 | "add_streaks": add_streaks 37 | } 38 | -------------------------------------------------------------------------------- /util/getBuffer.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const http = require("http"); 3 | 4 | function getBuffer(url, dirname) { 5 | return new Promise((resolve, reject) => { 6 | fs.readdir(dirname.split("/").slice(0, -1).join("/"), (err, files) => { 7 | if (files.includes(dirname.split("/").slice(-1)[0])) { 8 | fs.readFile(dirname, (err, data) => { 9 | if (err) reject(err); 10 | resolve(data); 11 | }); 12 | } else { 13 | let file = fs.createWriteStream(dirname); 14 | http.get(url, (response) => { 15 | response.pipe(file).on("finish", () => { 16 | fs.readFile(dirname, (err, data) => { 17 | if (err) reject(err); 18 | resolve(data); 19 | }); 20 | }); 21 | }); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | module.exports = getBuffer; 28 | -------------------------------------------------------------------------------- /commands/owner/usage.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | let commands = []; 3 | 4 | for (name in ctx.client.commands) { 5 | commands.push({ 6 | name, 7 | category: ctx.client.commands[name].category, 8 | usage: ctx.client.all_usage.stats[name] 9 | }); 10 | } 11 | 12 | let categories = commands 13 | .map((command) => command.category) 14 | .filter((item, index, array) => array.indexOf(item) === index); 15 | 16 | let rows = []; 17 | 18 | categories.forEach((category) => { 19 | let row = commands 20 | .filter((command) => command.category == category) 21 | .map((command) => `\`${command.name}\`: ${command.usage || 0}`) 22 | .join(", "); 23 | 24 | rows.push(`**${category}:** ${row}`); 25 | }); 26 | 27 | return ctx.send(`**All:** ${ctx.client.all_usage.stats.all}\n${rows.join("\n")}`); 28 | } 29 | 30 | module.exports = { 31 | name: "usage", 32 | category: "owner", 33 | exec 34 | }; 35 | -------------------------------------------------------------------------------- /commands/static/talents.js: -------------------------------------------------------------------------------- 1 | const findHero = require("../../util/findHero"); 2 | const talentsEmbed = require("../../embeds/talents"); 3 | const hero_abilities = require("dotaconstants").hero_abilities; 4 | const abilities = require("dotaconstants").abilities; 5 | 6 | async function exec(ctx) { 7 | let hero = findHero(ctx.options.join(" ")); 8 | if (!hero) { 9 | return ctx.failure(ctx.strings.get("bot_no_hero_error")); 10 | } 11 | 12 | let data = {}; 13 | data.hero = hero; 14 | data.talents = []; 15 | 16 | hero_abilities[`npc_dota_hero_${hero.name}`].talents.forEach((talent) => { 17 | if (talent.name.startsWith("special_bonus")) { 18 | talent.dname = abilities[talent.name] ? abilities[talent.name].dname : "?"; 19 | data.talents.push(talent); 20 | } 21 | }); 22 | 23 | let embed = talentsEmbed(data); 24 | return ctx.embed(embed); 25 | } 26 | 27 | module.exports = { 28 | name: "talents", 29 | category: "static", 30 | triviaCheat: true, 31 | exec 32 | }; 33 | -------------------------------------------------------------------------------- /commands/owner/eval.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | 3 | async function checks(client, member) { 4 | return member.id == client.config.owner; 5 | } 6 | 7 | async function exec(ctx) { 8 | try { 9 | let evaled = eval(ctx.options.join(" ")); 10 | evaled = await Promise.resolve(evaled); 11 | if (typeof evaled !== "string") evaled = util.inspect(evaled); 12 | 13 | console.log(evaled); 14 | ctx.send(`${"```js\n"}${evaled}${"\n```"}`).catch((err) => { 15 | return ctx.send("result too long for one message"); 16 | }); 17 | return Promise.resolve(); 18 | } catch (evalError) { 19 | console.error(evalError); 20 | ctx.send(`${"`ERROR`\n```js\n"}${evalError}${"\n```"}`).catch((err) => { 21 | return ctx.send("error too long for one message"); 22 | }); 23 | return Promise.resolve(); 24 | } 25 | } 26 | 27 | module.exports = { 28 | name: "eval", 29 | category: "owner", 30 | ignoreCooldowns: true, 31 | checks, 32 | exec 33 | }; 34 | -------------------------------------------------------------------------------- /embeds/streams.js: -------------------------------------------------------------------------------- 1 | function streamsEmbed(streams) { 2 | return { 3 | "title": this.get("twitch_streams_heading", streams[0].channel.language.toUpperCase()), 4 | "fields": [{ 5 | "name": this.get("twitch_streams_title"), 6 | "value": streams.map((stream) => { 7 | let status = stream.channel.status; 8 | return str = (status.length > 20 ? `${status.slice(0, 20).trim()}...` : status) + "\u2000\u2000"; 9 | }).join("\n"), 10 | "inline": true 11 | }, { 12 | "name": this.get("twitch_streams_streamer"), 13 | "value": streams.map((stream) => `[${stream.channel.display_name}](${stream.channel.url})`).join("\n"), 14 | "inline": true 15 | }, { 16 | "name": this.get("twitch_streams_viewers"), 17 | "value": streams.map((stream) => stream.viewers).join("\n"), 18 | "inline": true 19 | }], 20 | "timestamp": new Date() 21 | }; 22 | } 23 | 24 | module.exports = streamsEmbed; 25 | -------------------------------------------------------------------------------- /commands/personal/unregister.js: -------------------------------------------------------------------------------- 1 | async function exec(ctx) { 2 | try { 3 | let res = await ctx.client.pg.query({ 4 | text: "SELECT * FROM public.users WHERE id = $1;", 5 | values: [ctx.author.id] 6 | }); 7 | 8 | if (res.rows.length) { 9 | await ctx.client.pg.query({ 10 | text: "DELETE FROM public.users WHERE id = $1;", 11 | values: [ctx.author.id] 12 | }); 13 | 14 | await ctx.client.pg.query({ 15 | text: "DELETE FROM subs WHERE value = $1;", 16 | values: [res.rows[0].dotaid] 17 | }); 18 | 19 | return ctx.success(ctx.strings.get("unregister_success")); 20 | } else { 21 | return ctx.failure(ctx.strings.get("unregister_no_account")); 22 | } 23 | } catch (err) { 24 | ctx.error(err); 25 | ctx.failure(ctx.strings.get("bot_generic_error")); 26 | } 27 | } 28 | 29 | module.exports = { 30 | name: "unregister", 31 | category: "personal", 32 | exec 33 | }; 34 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | const pm2 = require("pm2"); 2 | const async = require("async"); 3 | 4 | const apps = require("./apps.json"); 5 | const config = require("./config.json"); 6 | 7 | apps.services = apps.services.map((service) => { 8 | service.name = `${config.pm2_prefix}${service.name}`; 9 | return service; 10 | }); 11 | 12 | apps.bot.name = `${config.pm2_prefix}${apps.bot.name}`; 13 | 14 | pm2.connect((err) => { 15 | if (err) { 16 | console.error(err); 17 | process.exit(2); 18 | } 19 | 20 | async.each(apps.services, (app, cb) => { 21 | console.log(`starting ${app.name}`); 22 | pm2.start(app, cb); 23 | }, (err) => { 24 | if (err) { 25 | console.error(err); 26 | pm2.disconnect(); 27 | process.exit(1); 28 | } else { 29 | setTimeout(() => { 30 | console.log("starting bot"); 31 | pm2.start(apps.bot, (err) => { 32 | if (err) console.error(err); 33 | pm2.disconnect(); 34 | process.exit(err ? 1 : 0); 35 | }); 36 | }, 1500); 37 | } 38 | }) 39 | }); 40 | -------------------------------------------------------------------------------- /dbots/gen.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const help = require("../core/locale/en").com.help_topics; 3 | 4 | let bulma = ['
', 5 | 'Help Server', 6 | '
' 7 | ]; 8 | 9 | for (let cat in help) { 10 | bulma.push(`

${cat.charAt(0).toUpperCase()}${cat.slice(1)} Commands

`); 11 | cat = help[cat]; 12 | 13 | bulma.push(''); 14 | 15 | for (let topic in cat) { 16 | topic = cat[topic]; 17 | 18 | bulma.push(''); 19 | 20 | bulma.push(``); 21 | bulma.push(``); 22 | let code = topic.example ? `--${topic.example}` : ""; 23 | bulma.push(``) 24 | bulma.push(``) 25 | 26 | bulma.push('') 27 | } 28 | 29 | bulma.push('
CommandDescriptionExampleAliases
${topic.name}${topic.summary}${code}${topic.aliases ? topic.aliases.join(", ") : ""}
'); 30 | } 31 | 32 | fs.writeFile("./bulma.html", bulma.join(""), (err) => { 33 | if (err) console.log(err); 34 | console.log('wrote') 35 | }) 36 | -------------------------------------------------------------------------------- /migrations/doublecheck.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | 3 | module.exports = (message, client, helper) => { 4 | if (message.member.id == "102645408223731712") { 5 | util.log("migrating..."); 6 | let guilds_list = require("../json/guilds.json"); 7 | 8 | client.guilds.forEach(guild => { 9 | client.pg.query(`SELECT * FROM public.guilds WHERE id = '${guild.id}';`).then(res => { 10 | if (res.rowCount != 1) { 11 | qstring = "INSERT INTO public.guilds (id, name, prefix, climit, mlimit) VALUES (" + 12 | `'${guild.id}',` + 13 | `'${guild.name.replace("'", "")}',` + 14 | `'${require("../json/config.json").default_prefix}',` + 15 | `'0',` + 16 | `'0'` + 17 | ");" 18 | console.log(qstring); 19 | client.pg.query(qstring).then(res => { 20 | console.log(res); 21 | }).catch(err => { 22 | console.error(err); 23 | }); 24 | } 25 | }).catch(err => { 26 | console.error(err); 27 | }); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /commands/static/hero.js: -------------------------------------------------------------------------------- 1 | const abilities = require("../../json/abilities"); 2 | const heroes = require("../../json/heroes"); 3 | 4 | const findHero = require("../../util/findHero"); 5 | const heroEmbed = require("../../embeds/hero"); 6 | 7 | const qwedfr = { 8 | "Q": 0, 9 | "W": 1, 10 | "E": 2, 11 | "D": 3, 12 | "F": 4, 13 | "R": 5 14 | }; 15 | 16 | async function exec(ctx) { 17 | let alias = ctx.options.join(" ").toLowerCase(); 18 | let res = findHero(alias); 19 | if (!res) { 20 | return ctx.failure(ctx.strings.get("bot_no_hero_error")); 21 | } 22 | 23 | let hero_obj = heroes.find((hero) => hero.name == `npc_dota_hero_${res.name}`); 24 | hero_obj = JSON.parse(JSON.stringify(hero_obj)); 25 | hero_obj.abilities = hero_obj.abilities 26 | .map((ability) => { 27 | ability = abilities.find((a) => a.name == ability); 28 | if (!ability) return null; 29 | return ability.key && `${ability.key.toUpperCase()} - ${ability.dname}`; 30 | }) 31 | .filter((a) => a) 32 | .sort((a, b) => qwedfr[a.charAt(0)] - qwedfr[b.charAt(0)]); 33 | 34 | return ctx.embed(heroEmbed(ctx.client, hero_obj)); 35 | } 36 | 37 | module.exports = { 38 | name: "hero", 39 | category: "static", 40 | triviaCheat: true, 41 | aliases: ["skills", "abilities"], 42 | exec 43 | }; 44 | -------------------------------------------------------------------------------- /commands/personal/nextmatch.js: -------------------------------------------------------------------------------- 1 | const searchMembers = require("../../util/searchMembers"); 2 | const checkDiscordID = require("../../util/checkDiscordID"); 3 | 4 | async function exec(ctx) { 5 | try { 6 | let members; 7 | if (ctx.options.length) { 8 | members = await searchMembers(ctx.guild.members, ctx.options); 9 | if (!members.found) return ctx.failure(ctx.strings.get("bot_no_member")); 10 | } else { 11 | members = false; 12 | } 13 | 14 | let ID = members ? members.all[0] : ctx.author.id; 15 | let member = await checkDiscordID(ctx.client.pg, ID); 16 | 17 | if (!member) { 18 | return ctx.failure(ctx.strings.get("bot_not_registered", ctx.guild.members.get(ID).username, ctx.gcfg.prefix)); 19 | } 20 | 21 | await ctx.client.redis.rpushAsync(`listen:nextmatch:${member}`, ctx.channel.id); 22 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 23 | action: "refresh" 24 | })); 25 | 26 | return ctx.send(ctx.strings.get("nextmatch_success", ctx.client.users.get(ID).username)); 27 | } catch (err) { 28 | ctx.error(err); 29 | return ctx.failure(ctx.strings.get("bot_generic_error")); 30 | } 31 | } 32 | 33 | module.exports = { 34 | name: "nextmatch", 35 | aliases: ["nm", "nextgame"], 36 | category: "personal", 37 | typing: true, 38 | exec 39 | }; 40 | -------------------------------------------------------------------------------- /embeds/prommr.js: -------------------------------------------------------------------------------- 1 | function prommr(data) { 2 | let name = ""; 3 | let url = ""; 4 | if (data.regions.includes(data.region)) { 5 | name = data.region; 6 | url = data.region; 7 | } else { 8 | if (data.region == "all") { 9 | name = "all regions"; 10 | } else { 11 | name = data.region; 12 | } 13 | } 14 | 15 | return { 16 | "author": { 17 | "name": this.get("prommr_header", name), 18 | "url": `http://www.dota2.com/leaderboards/#${url}` 19 | }, 20 | "fields": [{ 21 | "name": this.get("prommr_name"), 22 | "value": data.leaderboard.slice(0, 10).map((player) => { 23 | let flag = player.country ? `:flag_${player.country}:` : ":grey_question:"; 24 | let str = `${flag} ${player.team_tag ? player.team_tag + "." : ""}**${player.name}**`; 25 | return str.replace(/`/g, "'"); 26 | }).join("\n"), 27 | "inline": true 28 | }, { 29 | "name": "Rank", 30 | "value": data.leaderboard.slice(0, 10).map((player) => `${player.solo_mmr}<:blank:279251926409936896>`).join("\n"), 31 | "inline": true 32 | }], 33 | "timestamp": new Date(data.time_posted * 1000), 34 | "footer": { 35 | "text": this.get("prommr_last_updated") 36 | } 37 | }; 38 | } 39 | 40 | module.exports = prommr; 41 | -------------------------------------------------------------------------------- /util/eat.js: -------------------------------------------------------------------------------- 1 | const searchMembers = require("./searchMembers"); 2 | 3 | async function eat(content, options, members) { 4 | let flags = Object.keys(options); 5 | let containsMember = flags.map((flag) => options[flag]).includes("member"); 6 | if (containsMember && members === undefined) { 7 | return Promise.reject("no members provided to search through"); 8 | } 9 | 10 | let result = {}; 11 | 12 | for (flag of flags) { 13 | let split = content.split(" "); 14 | if (!split.includes(flag)) continue; 15 | 16 | let match = split.slice(split.indexOf(flag) + 1); 17 | let otherFlags = new Array(match.length).fill(false); 18 | for (otherFlag of flags) { 19 | if (otherFlag === flag) continue; 20 | if (~match.indexOf(otherFlag)) otherFlags[match.indexOf(otherFlag)] = otherFlag; 21 | } 22 | 23 | let isThereOtherFlag = otherFlags.find((flag) => flag); 24 | if (isThereOtherFlag) { 25 | let lastIndex = otherFlags.indexOf(isThereOtherFlag); 26 | if (lastIndex) match = match.slice(0, lastIndex); 27 | } 28 | 29 | if (options[flag] === "string") { 30 | result[flag] = match.join(" "); 31 | } else if (options[flag] === "member") { 32 | result[flag] = await searchMembers(members, match); 33 | } 34 | } 35 | 36 | return Promise.resolve(result); 37 | } 38 | 39 | module.exports = eat; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "listen-bot", 3 | "version": "0.0.1", 4 | "description": "a dota 2 bot", 5 | "main": "bot.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/bippum/listen-bot.git" 12 | }, 13 | "keywords": [ 14 | "dota2" 15 | ], 16 | "author": "alexa", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/bippum/listen-bot/issues" 20 | }, 21 | "homepage": "https://github.com/bippum/listen-bot#readme", 22 | "dependencies": { 23 | "async": "^2.6.0", 24 | "bignumber.js": "4.0.0", 25 | "bluebird": "3.5.0", 26 | "canvas": "^1.6.12", 27 | "cheerio": "^0.22.0", 28 | "d": "1.0.0", 29 | "dnode": "^1.2.2", 30 | "dota2": "^5.1.0", 31 | "dotaconstants": "git+https://github.com/odota/dotaconstants.git", 32 | "eris": "^0.9.0", 33 | "erlpack": "0.1.2", 34 | "fuzzyset.js": "0.0.1", 35 | "gosugamers-api": "0.1.2", 36 | "mika": "^1.5.5", 37 | "needle": "1.4.5", 38 | "node-schedule": "1.2.0", 39 | "pad": "1.1.0", 40 | "pg": "7.4.3", 41 | "pm2": "^2.10.2", 42 | "pretty-ms": "2.1.0", 43 | "randomstring": "1.1.5", 44 | "redis": "2.6.5", 45 | "snekfetch": "^3.6.4", 46 | "sprintf-js": "1.0.3", 47 | "steam": "^1.4.0", 48 | "xml2js": "0.4.17" 49 | }, 50 | "devDependencies": { 51 | "eslint": "" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /commands/static/patch.js: -------------------------------------------------------------------------------- 1 | const findHero = require("../../util/findHero"); 2 | const patchEmbed = require("../../embeds/patch"); 3 | const Watcher = require("../../classes/watcher"); 4 | 5 | const dc = require("dotaconstants"); 6 | const versions = Object.keys(dc.patchnotes); 7 | const heroChanges = Object.values(dc.patchnotes).map((version) => version.heroes); 8 | 9 | async function exec(ctx) { 10 | let hero = findHero(ctx.options.join(" ")); 11 | if (!hero) { 12 | return ctx.failure(ctx.strings.get("bot_no_hero_error")); 13 | } 14 | 15 | let changes = heroChanges 16 | .map((change, index) => { 17 | return { 18 | version: versions[index].replace("_", "."), 19 | changes: change[hero.name] 20 | }; 21 | }) 22 | .filter((change) => change.changes) 23 | .reverse(); 24 | 25 | let notes = changes.map((change) => { 26 | return { 27 | content: ctx.strings.all("patch_message", "\n", hero.local, changes.map((change => change.version)).join(" ")), 28 | embed: patchEmbed(change.changes, hero, change.version) 29 | } 30 | }); 31 | 32 | let msg = await ctx.send(notes[0]); 33 | ctx.client.watchers[msg.id] = new Watcher(ctx.client, msg, ctx.author.id, "p/n", notes, 0); 34 | return Promise.resolve(); 35 | } 36 | 37 | module.exports = { 38 | name: "patch", 39 | category: "static", 40 | triviaCheat: true, 41 | exec 42 | }; 43 | -------------------------------------------------------------------------------- /classes/strings.js: -------------------------------------------------------------------------------- 1 | const sprintf = require("sprintf-js").sprintf; 2 | 3 | class Strings { 4 | constructor(strings) { 5 | this._strings = strings; 6 | this._keys = Object.keys(strings); 7 | } 8 | 9 | get(str) { 10 | if (~this._keys.indexOf(str)) { 11 | if (arguments.length > 1) { 12 | return sprintf(this._strings[str], ...Array.from(arguments).slice(1)); 13 | } else { 14 | return this._strings[str]; 15 | } 16 | } else { 17 | console.error(`BAD STRING: ${str}`); 18 | return str; 19 | } 20 | } 21 | 22 | all(str, delim) { 23 | let res = this._keys.filter((item) => item.includes(str)); 24 | 25 | if (res) { 26 | res = res.map((item) => this._strings[item]); 27 | if (delim !== "array") res = res.join(delim || "\n"); 28 | 29 | if (arguments.length > 2) { 30 | if (Array.isArray(res)) { 31 | let temp = res.join("\n"); 32 | temp = sprintf(temp, ...Array.from(arguments).slice(2)); 33 | return temp.split("\n"); 34 | } else { 35 | return sprintf(res, ...Array.from(arguments).slice(2)); 36 | } 37 | } else { 38 | return res; 39 | } 40 | } else { 41 | return false; 42 | } 43 | } 44 | } 45 | 46 | module.exports = Strings; 47 | -------------------------------------------------------------------------------- /embeds/matches.js: -------------------------------------------------------------------------------- 1 | const pad = require("pad"); 2 | const aliases = require("../json/aliases.json"); 3 | 4 | function matchesEmbed(ctx, matches) { 5 | let matchlist = [["Match ID", " ", "Hero", "K/D/A", "Time", "Date"]]; 6 | let fmatchlist = []; 7 | let highest = new Array(6).fill(0); 8 | 9 | matches.forEach(match => { 10 | let row = [ 11 | match.match_id.toString(), 12 | match.player_slot < 5 == match.radiant_win ? "W" : "L", 13 | aliases.find((hero) => hero.id == match.hero_id).local, 14 | [match.kills, match.deaths, match.assists].join("/"), 15 | `${Math.floor(match.duration / 60)}:${("00" + match.duration % 60).substr(-2, 2)}`, 16 | new Date((match.start_time + match.duration) * 1000).toDateString() 17 | ]; 18 | 19 | for (let val in row) { 20 | if (highest[val] <= row[val].length) { 21 | highest[val] = row[val].length; 22 | } 23 | } 24 | 25 | matchlist.push(row); 26 | }); 27 | 28 | matchlist.forEach(row => { 29 | for (let item in row) { 30 | row[item] = pad(row[item], highest[item]); 31 | } 32 | fmatchlist.push(row); 33 | }); 34 | 35 | let content = [ 36 | ctx.strings.get("matches_embed_title", ctx.gcfg.prefix), 37 | "" 38 | ]; 39 | 40 | content.push(...fmatchlist.map((row) => `\`${row.join(" ")}\``)); 41 | 42 | return content.join("\n"); 43 | } 44 | 45 | module.exports = matchesEmbed; 46 | -------------------------------------------------------------------------------- /classes/reactionChooser.js: -------------------------------------------------------------------------------- 1 | class ReactionChooser { 2 | constructor(ctx, msg, options) { 3 | this.ctx = ctx; 4 | this.msg = msg; 5 | this.options = options; 6 | 7 | let promises = options.map((item, index) => this.msg.addReaction(`${index}\u20e3`)); 8 | 9 | Promise.all(promises).catch((err) => { 10 | ctx.helper.log(this.ctx.message, "couldn't add reactions"); 11 | }); 12 | 13 | this.delete = setTimeout(() => { 14 | this.ctx.helper.log(this.ctx.message, `stopped watching ${this.msg.id}`); 15 | delete ctx.client.watchers[this.msg.id]; 16 | }, 600000); 17 | 18 | this.ctx.helper.log(this.ctx.message, `started watching ${this.msg.id}`); 19 | } 20 | 21 | handle(message, emoji, userID) { 22 | if (userID !== this.ctx.author.id) return; 23 | emoji = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; 24 | 25 | if (isNaN(emoji = parseInt(emoji[0]))) return; 26 | 27 | if (this.options[emoji]) { 28 | this.msg.edit({ embed: this.options[emoji] }).catch((err) => this.ctx.helper.log(this.ctx.message, "cant edit in reaction chooser")); 29 | this.msg.removeReactions().catch((err) => this.ctx.helper.log(this.ctx.message, "cant remove reactions in reaction chooser")); 30 | this.ctx.helper.log(this.ctx.message, `stopped watching ${this.msg.id}`); 31 | delete this.ctx.client.watchers[this.msg.id]; 32 | } 33 | } 34 | } 35 | 36 | module.exports = ReactionChooser; 37 | -------------------------------------------------------------------------------- /classes/unwatcher.js: -------------------------------------------------------------------------------- 1 | class Unwatcher { 2 | constructor(ctx, list) { 3 | this._ctx = ctx; 4 | this._list = list; 5 | this.timeout = setTimeout(() => { 6 | this._ctx.failure(this._ctx.strings.get("unsub_timeout")); 7 | delete this._ctx.client.unwatchers[`${ctx.channel.id}:${ctx.author.id}`]; 8 | }, 60000); 9 | } 10 | 11 | handle(string) { 12 | let index = parseInt(string); 13 | if (this._list[index] !== undefined) { 14 | let split = this._list[index].split(":"); 15 | this._ctx.client.pg.query({ 16 | "text": "DELETE FROM subs WHERE owner = $1 AND type = $2 AND value = $3;", 17 | "values": [this._ctx.author.id, split[0], split[1]] 18 | }).catch(this._ctx.error).then((res) => { 19 | let gone = this._list.splice(index)[0]; 20 | this._ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 21 | "action": "remove", 22 | "type": gone.split(":")[0], 23 | "ids": gone.split(":")[1] 24 | })); 25 | this._ctx.success(this._ctx.strings.get("unsub_success", gone)) 26 | .catch(this._ctx.error) 27 | .then(() => { 28 | clearTimeout(this.timeout); 29 | delete this._ctx.client.unwatchers[`${this._ctx.channel.id}:${this._ctx.author.id}`]; 30 | }); 31 | }); 32 | } else { 33 | this._ctx.faliure(this._ctx.strings.get("unsub_out_of_range")).catch(this._ctx.error); 34 | } 35 | } 36 | } 37 | 38 | module.exports = Unwatcher; 39 | -------------------------------------------------------------------------------- /commands/personal/badge.js: -------------------------------------------------------------------------------- 1 | const searchMembers = require("../../util/searchMembers"); 2 | const allMmrEmbed = require("../../embeds/allMmr"); 3 | const singleMmrEmbed = require("../../embeds/singleMmr"); 4 | const SteppedList = require("../../classes/SteppedList"); 5 | const prettyMs = require("pretty-ms"); 6 | 7 | async function exec(ctx) { 8 | try { 9 | let ID = ctx.author.id; 10 | if (ctx.options.length) { 11 | let members = await searchMembers(ctx.guild.members, ctx.options); 12 | if (!members.found) return ctx.failure(ctx.strings.get("bot_no_member")); 13 | ID = members.all[0]; 14 | } 15 | 16 | let member = ctx.guild.members.get(ID); 17 | let res = await ctx.client.pg.query({ 18 | "text": "SELECT * FROM public.users WHERE id = $1;", 19 | "values": [ID] 20 | }); 21 | 22 | if (!res.rows.length) { 23 | return ctx.failure(ctx.strings.get("bot_not_registered", member.username, ctx.gcfg.prefix)); 24 | } 25 | 26 | let profile = await ctx.client.mika.getPlayer(res.rows[0].dotaid); 27 | let winlose = await ctx.client.mika.getPlayerWL(res.rows[0].dotaid); 28 | 29 | let msg = await singleMmrEmbed.call(ctx.strings, { profile, winlose, member, tier: profile.rank_tier || 0, rank: profile.leaderboard_rank || false }); 30 | return ctx.send({ embed: msg.embed }, msg.file); 31 | } catch (err) { 32 | ctx.error(err); 33 | return ctx.failure(ctx.strings.get("bot_generic_error")); 34 | } 35 | } 36 | 37 | module.exports = { 38 | name: "badge", 39 | category: "personal", 40 | aliases: ["mmr", "rank", "medal"], 41 | typing: true, 42 | exec 43 | }; 44 | -------------------------------------------------------------------------------- /embeds/ability.js: -------------------------------------------------------------------------------- 1 | function wikitize(string) { 2 | return string.replace(/ /g, "_").replace(/[']/, ""); 3 | } 4 | 5 | function embed(ability) { 6 | return { 7 | "author": { 8 | "name": ability.dname, 9 | "url": `http://dota2.gamepedia.com/${wikitize(ability.hero.local)}#${wikitize(ability.dname)}`, 10 | "icon_url": `http://cdn.dota2.com${ability.img}` 11 | }, 12 | "description": ability.desc, 13 | "fields": [ 14 | { 15 | "name": `<:manacost:273535201337016320> ${ability.mc ? (Array.isArray(ability.mc) ? ability.mc.join(" / ") : ability.mc) : "Passive"}`, 16 | "value": ability.attrib.map((a) => { 17 | return `**${a.header || ""}** ${Array.isArray(a.value) ? a.value.join("/") : a.value} ${a.footer || ""}` 18 | }).join("\n"), 19 | "inline": true 20 | }, 21 | { 22 | "name": `<:cooldown:273535146320199680> ${ability.cd ? (Array.isArray(ability.cd) ? ability.cd.join(" / ") : ability.cd) : "None"}`, 23 | "value": [ 24 | ability.dmg && (`**Damage:** ${Array.isArray(ability.dmg) ? ability.dmg.join(" / ") : ability.dmg}`), 25 | ability.behavior && `**Behavior:** ${Array.isArray(ability.behavior) ? ability.behavior.map((a) => a).join(", ") : ability.behavior}`, 26 | ability.dmg_type && `**Damage Type:** ${ability.dmg_type}`, 27 | ability.bkbpierce && `**Pierces BKB:** ${ability.bkbpierce}` 28 | ].filter((a) => a).join("\n"), 29 | "inline": true 30 | } 31 | ] 32 | }; 33 | } 34 | 35 | module.exports = embed; 36 | -------------------------------------------------------------------------------- /migrations/migrate.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | 3 | module.exports = (message, client, helper) => { 4 | util.log("migrating..."); 5 | let guilds_list = require("../json/guilds.json"); 6 | 7 | client.pg.query("CREATE TABLE IF NOT EXISTS public.guilds (" + 8 | "id BIGINT NOT NULL," + 9 | "name VARCHAR (100)," + 10 | "prefix VARCHAR(20)," + 11 | "climit INT," + 12 | "mlimit INT," + 13 | "PRIMARY KEY (id)" + 14 | ");").then(res => { 15 | for (gid in guilds_list) { 16 | qstring = "INSERT INTO public.guilds (id, name, prefix, climit, mlimit) VALUES (" + 17 | `'${gid}',` + 18 | `'${guilds_list[gid].name.replace("'", "")}',` + 19 | `'${guilds_list[gid].prefix}',` + 20 | `'${guilds_list[gid].channel_limit || 0}',` + 21 | `'${guilds_list[gid].member_limit || 0}'` + 22 | ");" 23 | console.log(qstring); 24 | client.pg.query(qstring).then(res => { 25 | util.log(`migrated ${gid}/${guilds_list[gid].name}`); 26 | util.log(res); 27 | }).catch(err => { 28 | console.error(`something went wrong with inserting ${gid}/${guilds_list[gid].name}`); 29 | console.error(err); 30 | }); 31 | } 32 | }).catch(err => { 33 | console.error("something went wrong with creating the table"); 34 | console.error(err); 35 | }); 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /migrations/transform_items.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | //const od_items = require("../json/od_items.json"); 4 | // should require from odota/dotaconstants from now on tho 5 | 6 | const regex = /<[^>]*>/g; 7 | 8 | function clean(str, times = 10) { 9 | for (let i = 0; i <= times; i++) { 10 | str = str.replace("
", "\n") 11 | .replace(" ", " ") 12 | .replace("\r", "") 13 | .replace(regex, "") 14 | .replace("+ ", "+"); 15 | } 16 | 17 | let arr = str.split("\n"); 18 | for (item in arr) { 19 | if (arr[item] == "" || arr[item] == "\r") arr.splice(item, 1) 20 | } 21 | 22 | return arr; 23 | } 24 | 25 | var items = []; 26 | 27 | for (od_item in od_items) { 28 | od_obj = od_items[od_item] 29 | 30 | let item = {}; 31 | item.true_name = od_item; 32 | item.id = od_obj.id; 33 | item.format_name = od_obj.dname; 34 | item.cost = od_obj.cost; 35 | item.cooldown = od_obj.cd; 36 | if (od_obj.desc != "") { 37 | item.description = clean(od_obj.desc); 38 | } 39 | if (od_obj.notes != "") { 40 | item.notes = clean(od_obj.notes); 41 | } 42 | if (od_obj.attrib != "") { 43 | let len = od_obj.attrib.split("\n").length; 44 | item.attributes = clean(od_obj.attrib, len); 45 | if (item.attributes.length > 9) console.log(od_item) 46 | } 47 | if (od_obj.lore != "") { 48 | item.lore = clean(od_obj.lore); 49 | if (item.lore.length > 1) console.log(od_item); 50 | } 51 | 52 | 53 | items.push(item); 54 | } 55 | 56 | fs.writeFile("../json/items.json", JSON.stringify(items, null, 4), (err) => { 57 | if (err) console.log(err) 58 | console.log("wrote") 59 | }) 60 | -------------------------------------------------------------------------------- /commands/personal/matchinfo.js: -------------------------------------------------------------------------------- 1 | const matchEmbed = require("../../embeds/match"); 2 | 3 | async function exec(ctx) { 4 | let match_id = String(ctx.match_id || ctx.options[0]); 5 | if (!match_id) { 6 | return ctx.failure(ctx.strings.get("matches_no_matchid")); 7 | } 8 | 9 | if (match_id.includes("dotabuff") || match_id.includes("opendota")) { 10 | match_id = match_id.split("/").slice(-1)[0]; 11 | } 12 | 13 | if (isNaN(match_id)) { 14 | return ctx.failure(ctx.strings.get("matches_bad_matchid")); 15 | } 16 | 17 | try { 18 | let key = `matchinfo:${match_id}`; 19 | let match = await ctx.client.redis.getAsync(key); 20 | 21 | if (match) { 22 | match = JSON.parse(match); 23 | } else { 24 | match = await ctx.client.mika.getMatch(match_id); 25 | await ctx.client.redis.setexAsync(key, 86400, JSON.stringify(match)); 26 | } 27 | 28 | // check if opendota is giving us a real match 29 | if (match.match_seq_num === null) { 30 | return ctx.failure(ctx.strings.get("matches_bad_matchid")); 31 | } 32 | 33 | // sometimes the scores are broken 34 | match.radiant_score = match.players.slice(0, 5).map(player => player.kills).reduce((a, b) => a + b, 0); 35 | match.dire_score = match.players.slice(5, 10).map(player => player.kills).reduce((a, b) => a + b, 0); 36 | 37 | let embed = await matchEmbed.call(ctx.strings, ctx, match); 38 | return ctx.embed(embed); 39 | } catch (err) { 40 | ctx.error(err); 41 | return ctx.failure(ctx.strings.get("bot_generic_error")); 42 | } 43 | } 44 | 45 | module.exports = { 46 | name: "matchinfo", 47 | category: "personal", 48 | aliases: ["mi", "match"], 49 | typing: true, 50 | exec 51 | }; 52 | -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | class Helper { 2 | constructor() { 3 | this.last_guild = ""; 4 | this.last_channel = ""; 5 | } 6 | 7 | print(text, type) { 8 | let now = new Date().toJSON(); 9 | switch (type) { 10 | case "error": 11 | console.error(now, text); 12 | break; 13 | case "warn": 14 | console.warn(now, text); 15 | break; 16 | default: 17 | console.log(now, text); 18 | break; 19 | } 20 | } 21 | 22 | log(message, text, type = "log") { 23 | if (typeof message == "object") { 24 | if (this.last_guild == message.channel.guild.name) { 25 | if (this.last_channel == message.channel.name) { 26 | this.print(`CMDS: ${text.toString().trim()}`, type); 27 | } else { 28 | this.print(`CMDS: ${message.channel.name}: ${text.toString().trim()}`, type); 29 | } 30 | } else { 31 | this.print(`CMDS: ${message.channel.guild.name}/${message.channel.name}: ${text.toString().trim()}`, type); 32 | } 33 | 34 | this.last_guild = message.channel.guild.name; 35 | this.last_channel = message.channel.name; 36 | } else { 37 | this.print(`${message.toUpperCase()}: ${text}`, type); 38 | } 39 | } 40 | 41 | handle(message, err) { 42 | let result = err.toString().split(" ")[1]; 43 | if (result == "400") { 44 | this.log(message, "probably don't have permissions to embed here", "warn"); 45 | } else if (result == "403") { 46 | this.log(message, "probably don't have permissions to send messages here", "warn"); 47 | } else { 48 | this.log(message, err, "error"); 49 | } 50 | } 51 | } 52 | 53 | module.exports = Helper; 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _old 2 | json_ignore 3 | images 4 | 5 | config.json 6 | guilds.json 7 | usage.json 8 | od_heroes.json 9 | bulma.html 10 | 11 | sentry 12 | 13 | update.sh 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules 42 | jspm_packages 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # ========================= 51 | # Operating System Files 52 | # ========================= 53 | 54 | # OSX 55 | # ========================= 56 | 57 | .DS_Store 58 | .AppleDouble 59 | .LSOverride 60 | 61 | # Thumbnails 62 | ._* 63 | 64 | # Files that might appear in the root of a volume 65 | .DocumentRevisions-V100 66 | .fseventsd 67 | .Spotlight-V100 68 | .TemporaryItems 69 | .Trashes 70 | .VolumeIcon.icns 71 | 72 | # Directories potentially created on remote AFP share 73 | .AppleDB 74 | .AppleDesktop 75 | Network Trash Folder 76 | Temporary Items 77 | .apdisk 78 | 79 | # Windows 80 | # ========================= 81 | 82 | # Windows image file caches 83 | Thumbs.db 84 | ehthumbs.db 85 | 86 | # Folder config file 87 | Desktop.ini 88 | 89 | # Recycle Bin used on file shares 90 | $RECYCLE.BIN/ 91 | 92 | # Windows Installer files 93 | *.cab 94 | *.msi 95 | *.msm 96 | *.msp 97 | 98 | # Windows shortcuts 99 | *.lnk 100 | -------------------------------------------------------------------------------- /embeds/item.js: -------------------------------------------------------------------------------- 1 | function itemEmbed(item, items) { 2 | let costs = []; 3 | let desc = []; 4 | let att = []; 5 | 6 | if (item.mc) costs.push(`<:manacost:273535201337016320> ${item.mc}`); 7 | if (item.cd) costs.push(`<:cooldown:273535146320199680> ${item.cd}`); 8 | if (item.cost) costs.push(`<:gold:281572294030131200> ${item.cost}`); 9 | 10 | if (item.desc) desc.push(item.desc); 11 | if (item.notes) desc.push(item.notes); 12 | 13 | let attributes = item.attrib && item.attrib 14 | .filter((attrib) => !attrib.header.includes("TOOLTIP")) 15 | .map((attrib) => { 16 | if (attrib.footer) { 17 | return `**${attrib.header}${Array.isArray(attrib.value) ? attrib.value.join(" / ") : attrib.value}** ${attrib.footer}`; 18 | } else { 19 | return `**${attrib.header}** ${Array.isArray(attrib.value) ? attrib.value.join(" / ") : attrib.value}`; 20 | } 21 | }); 22 | 23 | let fields = [{ 24 | name: costs.length ? costs.join(" ") : "\u200b", 25 | value: attributes ? attributes.join("\n") : "Nothing special.", 26 | inline: true 27 | }]; 28 | 29 | if (item.created) { 30 | let comp = item.components.map((component) => items.find((item) => item.name == component)); 31 | fields.push({ 32 | name: "Components", 33 | value: comp.map((component) => `<:gold:281572294030131200> ${component.cost} - **${component.dname}**`).join("\n"), 34 | inline: true 35 | }); 36 | } 37 | 38 | return { 39 | "author": { 40 | "name": item.dname, 41 | "url": `http://dota2.gamepedia.com/${item.dname.replace(/ /g, "_")}`, 42 | "icon_url": `http://cdn.dota2.com${item.img}` 43 | }, 44 | fields, 45 | "description": desc.join("\n\n"), 46 | "footer": { 47 | "text": item.lore 48 | } 49 | }; 50 | } 51 | 52 | module.exports = itemEmbed; 53 | -------------------------------------------------------------------------------- /commands/esports/twitch.js: -------------------------------------------------------------------------------- 1 | const streamsEmbed = require("../../embeds/streams"); 2 | 3 | const subcommands = { 4 | clip: async function(ctx) { 5 | try { 6 | let clips = await ctx.client.redis.getAsync("twitch:clips"); 7 | clips = JSON.parse(clips).clips; 8 | 9 | let clip = clips[Math.floor(Math.random() * clips.length)]; 10 | 11 | return ctx.send([ 12 | ctx.strings.get("twitch_clip", clip.broadcaster.display_name, clip.curator.display_name, Math.ceil(clip.duration)), 13 | clip.url 14 | ].join("\n")); 15 | } catch (err) { 16 | ctx.error(err); 17 | return ctx.failure(ctx.strings.get("bot_generic_error")); 18 | } 19 | }, 20 | streams: async function(ctx) { 21 | let lang = ctx.options[0] || ctx.gcfg.locale; 22 | 23 | try { 24 | let streams = await ctx.client.redis.getAsync("twitch:streams"); 25 | streams = JSON.parse(streams).streams; 26 | 27 | let localstreams = streams.filter((stream) => stream.channel.language == lang); 28 | 29 | if (localstreams.length < 1) { 30 | localstreams = streams.filter((stream) => stream.channel.language == "en"); 31 | } 32 | 33 | let embed = streamsEmbed.call(ctx.strings, localstreams.slice(0, 5)); 34 | return ctx.embed(embed); 35 | } catch (err) { 36 | ctx.error(err); 37 | return ctx.failure(ctx.strings.get("bot_generic_error")); 38 | } 39 | } 40 | }; 41 | 42 | async function exec(ctx) { 43 | let subcommand = ctx.options.splice(0, 1)[0]; 44 | 45 | if (subcommands.hasOwnProperty(subcommand)) { 46 | return subcommands[subcommand](ctx); 47 | } else { 48 | return ctx.send(ctx.strings.get("bot_available_subcommands", Object.keys(subcommands).map((cmd) => `\`${cmd}\``).join(", "))); 49 | } 50 | } 51 | 52 | module.exports = { 53 | name: "twitch", 54 | category: "esports", 55 | typing: true, 56 | exec 57 | }; 58 | -------------------------------------------------------------------------------- /commands/personal/profile.js: -------------------------------------------------------------------------------- 1 | const searchMembers = require("../../util/searchMembers"); 2 | const checkDiscordID = require("../../util/checkDiscordID"); 3 | const playerinfoEmbed = require("../../embeds/playerinfo"); 4 | 5 | async function getProfile(ctx, id) { 6 | try { 7 | let promises = [ 8 | ctx.client.mika.getPlayer(id), 9 | ctx.client.mika.getPlayerWL(id), 10 | ctx.client.mika.getPlayerHeroes(id) 11 | ]; 12 | 13 | let results = await Promise.all(promises); 14 | 15 | let player = results[0]; 16 | player.wl = results[1]; 17 | player.heroes = results[2]; 18 | 19 | return player; 20 | } catch (err) { 21 | throw err; 22 | } 23 | } 24 | 25 | async function cacheProfile(ctx, id) { 26 | try { 27 | let key = `playerinfo:${id}`; 28 | let reply = await ctx.client.redis.getAsync(key); 29 | 30 | if (reply) return JSON.parse(reply); 31 | 32 | reply = await getProfile(ctx, id); 33 | await ctx.client.redis.setexAsync(key, 3600, JSON.stringify(reply)); 34 | 35 | return reply; 36 | } catch (err) { 37 | throw err; 38 | } 39 | } 40 | 41 | async function exec(ctx) { 42 | try { 43 | let members; 44 | if (ctx.options.length) { 45 | members = await searchMembers(ctx.guild.members, ctx.options); 46 | if (!members.found) return ctx.failure(ctx.strings.get("bot_no_member")); 47 | } else { 48 | members = false; 49 | } 50 | 51 | let ID = members ? members.all[0] : ctx.author.id; 52 | let member = await checkDiscordID(ctx.client.pg, ID); 53 | 54 | if (!member) { 55 | return ctx.failure(ctx.strings.get("bot_not_registered", ctx.guild.members.get(ID).username, ctx.gcfg.prefix)); 56 | } 57 | 58 | let profile = await cacheProfile(ctx, member); 59 | 60 | let embed = await playerinfoEmbed(profile); 61 | return ctx.send({ embed: embed.embed }, embed.file); 62 | } catch (err) { 63 | ctx.error(err); 64 | return ctx.failure(ctx.strings.get("bot_generic_error")); 65 | } 66 | } 67 | 68 | module.exports = { 69 | name: "profile", 70 | category: "personal", 71 | aliases: ["playerinfo"], 72 | typing: true, 73 | exec 74 | }; 75 | -------------------------------------------------------------------------------- /migrations/users.js: -------------------------------------------------------------------------------- 1 | const pg = require("pg"); 2 | const util = require("util"); 3 | 4 | module.exports = client => { 5 | var devconfig = JSON.parse(JSON.stringify(client.config.pgconfig)); 6 | devconfig.database = "listendev"; 7 | var dev = new pg.Client(devconfig); 8 | dev.connect((err) => { 9 | if (err) console.log(err); 10 | console.log("dev pg connected."); 11 | dev.query({ 12 | "text": "SELECT * FROM public.users;" 13 | }).then(res => { 14 | for (row in res.rows) { 15 | let newrow = JSON.parse(JSON.stringify(res.rows[row])); 16 | client.pg.query({ 17 | "text": "SELECT * FROM public.users WHERE id = $1;", 18 | "values": [res.rows[row].id] 19 | }).then((newres) => { 20 | if (newres.rowCount == 0) { 21 | client.pg.query({ 22 | "text": "INSERT INTO public.users (id, steamid, dotaid) VALUES ($1, $2, $3);", 23 | "values": [newrow.id, newrow.steamid, newrow.dotaid] 24 | }).then(() => { 25 | util.log(` inserted dota id ${newrow.id}`); 26 | }).catch(err => { 27 | util.log(" something went wrong inserting a user"); 28 | util.log(err); 29 | }); 30 | } else { 31 | client.pg.query({ 32 | "text": "UPDATE public.users SET id = $1, steamid = $2, dotaid = $3 WHERE id = $1;", 33 | "values": [newrow.id, newrow.steamid, newrow.dotaid] 34 | }).then(() => { 35 | util.log(` updated dota id ${newres.rows[0].dotaid} -> ${newrow.id}`); 36 | }).catch(err => { 37 | util.log(" something went wrong updating a user"); 38 | util.log(err); 39 | }); 40 | } 41 | }) 42 | } 43 | }).catch(err => { 44 | util.log(" something went wrong selecting a user"); 45 | util.log(err); 46 | }); 47 | }) 48 | }; -------------------------------------------------------------------------------- /templates/stats.js: -------------------------------------------------------------------------------- 1 | const eris_version = require("eris/package.json").version; 2 | const prettyms = require("pretty-ms"); 3 | 4 | module.exports = (client) => { 5 | return new Promise((resolve) => { 6 | let plist = [ 7 | client.pg.query("select version();"), 8 | client.pg.query("select count(*) from users;") 9 | ]; 10 | 11 | Promise.all(plist).then(res => { 12 | resolve({ 13 | "author": { 14 | "name": `Node.js Version: ${process.version}` 15 | }, 16 | "fields": [{ 17 | "name": "Eris Version", 18 | "value": eris_version, 19 | "inline": true 20 | }, { 21 | "name": "Redis Version", 22 | "value": client.redis.server_info.versions.join("."), 23 | "inline": true 24 | }, { 25 | "name": "Postgres Version", 26 | "value": res[0].rows[0].version.split(" ")[1], 27 | "inline": true 28 | }, { 29 | "name": "Memory Usage", 30 | "value": `${(process.memoryUsage().rss / (1024 * 1024)).toFixed(1)} MB`, 31 | "inline": true 32 | }, { 33 | "name": "Redis Usage", 34 | "value": `${(client.redis.server_info.used_memory_rss / (1024 * 1024)).toFixed(1)} MB`, 35 | "inline": true 36 | }, { 37 | "name": "Registered Users", 38 | "value": res[1].rows[0].count, 39 | "inline": true 40 | }, { 41 | "name": "Servers / Users", 42 | "value": `${client.guilds.size} / ${client.users.size}`, 43 | "inline": true 44 | }, { 45 | "name": "Uptime", 46 | "value": prettyms(client.uptime), 47 | "inline": true 48 | }, { 49 | "name": "Cmds (session / all time)", 50 | "value": `${client.usage.stats.all} / ${client.all_usage.stats.all}`, 51 | "inline": true 52 | }], 53 | "timestamp": new Date().toJSON() 54 | }); 55 | }); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /commands/meta/info.js: -------------------------------------------------------------------------------- 1 | const needle = require("needle"); 2 | const contributors = require("../../contributors.json"); 3 | 4 | async function exec(ctx) { 5 | needle.get("https://api.github.com/repos/bippum/listen-bot/commits", (err, response) => { 6 | let user_me = ctx.client.users.get(ctx.client.config.owner); 7 | let me = ctx.guild.members.has(ctx.client.config.owner) ? `<@${ctx.client.config.owner}>` : `${user_me.username}#${user_me.discriminator}`; 8 | 9 | let desc = ctx.strings.get("info_description", me); 10 | 11 | let links = [ 12 | "https://github.com/bippum/listen-bot", 13 | "https://bots.discord.pw/bots/240209888771309568", 14 | `https://discordapp.com/oauth2/authorize?permissions=${ctx.client.config.permissions}&scope=bot&client_id=${ctx.client.user.id}` 15 | ].map((item, index) => ctx.strings.get(`info_links_${index}`, item)); 16 | 17 | let gitlinks = err ? ["rip github"] : response.body.slice(0, 4).map((commit) => { 18 | let cmsg = commit.commit.message.slice(0, 40).split("\n")[0]; 19 | return `[\`${commit.sha.slice(0, 7)}\`](${commit.html_url}) - ${cmsg}${commit.commit.message.length > 40 ? "..." : ""}`; 20 | }); 21 | 22 | let contribs = Object.keys(contributors).map((key) => `**${ctx.strings.get(key)}:** ${contributors[key].join(", ")}`); 23 | 24 | return ctx.embed({ 25 | "timestamp": new Date().toJSON(), 26 | "description": desc, 27 | "fields": [{ 28 | "name": ctx.strings.get("info_github_log"), 29 | "value": gitlinks.join("\n"), 30 | "inline": true 31 | }, { 32 | "name": ctx.strings.get("info_links"), 33 | "value": links.join("\n"), 34 | "inline": true 35 | }, { 36 | "name": ctx.strings.get("info_special_thanks"), 37 | "value": contribs.join("\n"), 38 | "inline": false 39 | }, { 40 | "name": ctx.strings.get("info_links_3"), 41 | "value": ctx.strings.get("info_help_server_text", ctx.client.config.discord_invite), 42 | "inline": false 43 | }] 44 | }); 45 | }); 46 | } 47 | 48 | module.exports = { 49 | name: "info", 50 | category: "meta", 51 | aliases: ["about"], 52 | exec 53 | }; 54 | -------------------------------------------------------------------------------- /embeds/hero.js: -------------------------------------------------------------------------------- 1 | module.exports = (client, hero) => { 2 | let attrs = { 3 | str: `${hero.base_str} + ${Number(hero.str_gain)}`, 4 | agi: `${hero.base_agi} + ${Number(hero.agi_gain)}`, 5 | int: `${hero.base_int} + ${Number(hero.int_gain)}` 6 | }; 7 | 8 | let val1 = [ 9 | `Lv1. Health: ${hero.base_health + Math.round((hero.primary_attr === "str" ? 22.5 : 17) * hero.base_str)}`, 10 | `Lv1. Armor: ${hero.base_armor + Math.round((hero.primary_attr === "agi" ? 0.2 : 0.16) * hero.base_agi)}`, 11 | `Lv1. Mana: ${hero.base_mana + ((hero.primary_attr === "int" ? 15 : 12) * hero.base_int)}`, 12 | `Lv1. Spell Amp: ${Math.round(100 * (0.07 * hero.base_int)) / 100}%` 13 | ]; 14 | 15 | let val2 = [ 16 | `Lv1. Damage: ${hero.base_attack_min + hero[`base_${hero.primary_attr}`]} - ${hero.base_attack_max + hero[`base_${hero.primary_attr}`]}`, 17 | `BAT: ${hero.attack_rate}`, 18 | `Attack Range: ${hero.attack_range} (${hero.attack_type})`, 19 | `Projectile Speed: ${hero.projectile_speed}` 20 | ]; 21 | 22 | let val3 = [ 23 | `Move Speed: ${hero.move_speed}`, 24 | `Turn Rate: ${hero.turn_rate}`, 25 | `In Captains mode: ${hero.cm_enabled ? "Yes" : "No"}`, 26 | `Legs: ${hero.legs}` 27 | ]; 28 | 29 | attrs[hero.primary_attr] = `__${attrs[hero.primary_attr]}__`; 30 | 31 | return { 32 | "author": { 33 | "name": hero.localized_name, 34 | "url": `http://dota2.gamepedia.com/${hero.url}`, 35 | "icon_url": `http://cdn.dota2.com${hero.icon}` 36 | }, 37 | "fields": [{ 38 | "name": `${attrs.str} <:strength:281578819721363457>`, 39 | "value": val1.map(item => `**${item.split(": ")[0]}**: ${item.split(": ")[1]}`).join("\n"), 40 | "inline": true 41 | }, { 42 | "name": `${attrs.agi} <:agility:333769662515118080>`, 43 | "value": val2.map(item => `**${item.split(": ")[0]}**: ${item.split(": ")[1]}`).join("\n"), 44 | "inline": true 45 | }, { 46 | "name": `${attrs.int} <:intelligence:281578849190281217> `, 47 | "value": val3.map(item => `**${item.split(": ")[0]}**: ${item.split(": ")[1]}`).join("\n"), 48 | "inline": true 49 | }, { 50 | "name": "Abilities", 51 | "value": hero.abilities.join(", "), 52 | "inline": false 53 | }] 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /commands/personal/register.js: -------------------------------------------------------------------------------- 1 | const Bignumber = require("bignumber.js") 2 | const randomstring = require("randomstring"); 3 | const steamconst = "76561197960265728"; 4 | const resolveVanityURL = require("../../util/resolveVanityURL"); 5 | 6 | async function exec(ctx) { 7 | let search = ctx.options.join(" "); 8 | let ID = false; 9 | 10 | if (search.includes("dotabuff.com/players") || search.includes("opendota.com/players")) { 11 | ID = search.split("/").slice(-1)[0]; 12 | } 13 | 14 | if (search.includes("steamcommunity.com/")) { 15 | let split = search.split("/").reverse(); 16 | ID = split[0] || split[1]; 17 | 18 | if (!ID) { 19 | return ctx.failure(ctx.strings.get("register_no_id")); 20 | } 21 | 22 | if (isNaN(ID) || search.includes("/id/")) { 23 | try { 24 | ID = await resolveVanityURL(ID); 25 | } catch (err) { 26 | ctx.error(err); 27 | return ctx.failure(ctx.strings.get("register_bad_vanity")); 28 | } 29 | } 30 | 31 | ID = new Bignumber(ID).minus(steamconst); 32 | } 33 | 34 | if (!isNaN(search)) { 35 | ID = search; 36 | } 37 | 38 | if (!ID || isNaN(ID)) { 39 | return ctx.failure(ctx.strings.get("register_no_id")); 40 | } 41 | 42 | let res; 43 | try { 44 | res = await ctx.client.mika.getPlayer(ID); 45 | } catch (err) { 46 | ctx.error(err); 47 | return ctx.failure(ctx.strings.get("bot_generic_error")); 48 | } 49 | 50 | if (!res.profile) { 51 | return ctx.failure(ctx.strings.get("register_private_account")); 52 | } 53 | 54 | if (!res.profile.steamid) { 55 | return ctx.failure(ctx.strings.get("register_no_steam_id")); 56 | } 57 | 58 | try { 59 | ctx.client.redis.publish("steam", JSON.stringify({ 60 | discord_id: ctx.author.id, 61 | dota_id: res.profile.account_id 62 | })); 63 | 64 | try { await ctx.message.addReaction("✅") } catch (err) {} 65 | 66 | return Promise.resolve(); 67 | } catch (err) { 68 | if (err.response && JSON.parse(err.response).code === 50007) { 69 | return ctx.failure(ctx.strings.get("bot_register_error")); 70 | } else { 71 | ctx.error(err); 72 | return ctx.failure(ctx.strings.get("bot_generic_error")); 73 | } 74 | } 75 | } 76 | 77 | module.exports = { 78 | name: "register", 79 | category: "personal", 80 | exec 81 | }; 82 | -------------------------------------------------------------------------------- /util/searchMembers.js: -------------------------------------------------------------------------------- 1 | const FuzzySet = require("fuzzyset.js"); 2 | 3 | function decideMatch(nick, user) { 4 | if (!nick) return false; 5 | if (!user) return true; 6 | 7 | return nick[0][0] > user[0][0]; 8 | } 9 | 10 | async function searchMembers(members, terms, exact) { 11 | let res = { 12 | "all": [], 13 | "terms": {}, 14 | "found": false 15 | }; 16 | 17 | let usernames = FuzzySet(members.map((member) => member.username)); 18 | let nicknames = FuzzySet(members.filter((member) => member.nick).map((member) => member.nick)); 19 | 20 | for (let i = 0; i <= terms.length; i++) { 21 | for (let j = 0; j <= terms.length; j++) { 22 | if (i < j) { 23 | let term = terms.slice(i, j).join(" ").trim(); 24 | let search = members.find((member) => { 25 | if (res.all.includes(member.id)) return false; 26 | if (term.replace(/\D/g, "") === member.id) return true; 27 | 28 | if (member.username.toLowerCase() == term) return true; 29 | if (member.nick && member.nick.toLowerCase() == term) return true; 30 | }); 31 | 32 | if (search) { 33 | res.all.push(search.id); 34 | res.terms[term] = search.id; 35 | res.found = true; 36 | } 37 | } 38 | } 39 | } 40 | 41 | if (!exact) { 42 | let threshold = 0.8; 43 | 44 | if (members.size < 5000) { 45 | threshold = members.size / 5000 * 0.3 + 0.5; 46 | } 47 | 48 | let matchedUsername = usernames.get(terms.join(" ")); 49 | let matchedNickname = nicknames.get(terms.join(" ")); 50 | 51 | let nickOrUser = decideMatch(matchedNickname, matchedUsername); 52 | let matched = nickOrUser ? matchedNickname : matchedUsername; 53 | 54 | if (matched && matched[0][0] >= threshold && matched[0][1]) { 55 | let member = members.find((member) => { 56 | if (nickOrUser) { 57 | return member.nick === matched[0][1]; 58 | } else { 59 | return member.username === matched[0][1]; 60 | } 61 | }); 62 | 63 | res.all.push(member.id); 64 | res.terms[terms.join(" ")] = member.id; 65 | res.found = true; 66 | } 67 | } 68 | 69 | res.all = res.all.filter((item, index, array) => array.indexOf(item) === index); 70 | 71 | return res; 72 | } 73 | 74 | module.exports = searchMembers; 75 | -------------------------------------------------------------------------------- /classes/SteppedList.js: -------------------------------------------------------------------------------- 1 | class SteppedList { 2 | constructor(ctx, message, interval, template, headings, body) { 3 | this.ctx = ctx; 4 | this.message = message; 5 | this.template = template; 6 | this.interval = interval; 7 | this.headings = headings; 8 | this.body = body; 9 | 10 | this.step = 0; 11 | 12 | this.delete = setTimeout(() => { 13 | this.ctx.helper.log(this.ctx.message, `stopped watching ${this.message.id}`); 14 | delete this.ctx.client.watchers[this.message.id]; 15 | }, 600000); 16 | 17 | Promise.all([ 18 | this.message.addReaction("◀"), 19 | this.message.addReaction("▶") 20 | ]).catch((err) => { 21 | this.ctx.helper.log(`couldn't add reaction to ${this.message.id}`); 22 | }); 23 | } 24 | 25 | embed(step) { 26 | let embed = { 27 | fields: this.headings.map((h, index) => { 28 | return { 29 | name: h, 30 | value: this.body[index] 31 | .slice(this.interval * this.step) 32 | .slice(0, this.interval) 33 | .join("\n"), 34 | inline: true 35 | }; 36 | }), 37 | description: `Page ${this.step + 1}` 38 | }; 39 | 40 | embed = Object.assign(embed, this.template); 41 | return { embed }; 42 | } 43 | 44 | handle(message, emoji, userID) { 45 | if (userID !== this.ctx.author.id) return; 46 | if (!this.message) return; 47 | emoji = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; 48 | 49 | if (emoji === "◀") { 50 | if (this.step < 1) return; 51 | 52 | setTimeout(() => { 53 | this.message.removeReaction("◀", userID).catch((err) => { return; }); 54 | this.step = this.step - 1; 55 | let embed = this.embed(this.step); 56 | this.message.edit(embed).catch((err) => { return; }); 57 | }, 250); 58 | } 59 | 60 | if (emoji === "▶") { 61 | if ((this.step + 1) * this.interval > this.body[0].length) return; 62 | 63 | setTimeout(() => { 64 | this.message.removeReaction("▶", userID).catch((err) => { return; }); 65 | this.step = this.step + 1; 66 | let embed = this.embed(this.step); 67 | this.message.edit(embed).catch((err) => { return; }); 68 | }, 250); 69 | } 70 | } 71 | } 72 | 73 | module.exports = SteppedList; 74 | -------------------------------------------------------------------------------- /classes/watcher.js: -------------------------------------------------------------------------------- 1 | class Watcher { 2 | constructor(client, message, userID, behavior, map, start) { 3 | this.client = client; 4 | this.message = message; 5 | this.channelID = message.channel.id; 6 | this.messageID = message.id; 7 | this.userID = userID; 8 | this.behavior = behavior; 9 | this.map = map; 10 | this.working = true; 11 | this.delete = setTimeout(() => { 12 | this.client.helper.log(this.message, `stopped watching ${this.messageID}`); 13 | delete client.watchers[this.messageID]; 14 | }, 600000); 15 | 16 | if (this.behavior == "p/n") { 17 | this.position = start; 18 | this.previousEmoji = "◀"; 19 | this.nextEmoji = "▶"; 20 | 21 | Promise.all([ 22 | this.client.addMessageReaction(this.channelID, this.messageID, this.previousEmoji), 23 | this.client.addMessageReaction(this.channelID, this.messageID, this.nextEmoji) 24 | ]).catch((err) => { 25 | this.client.helper.log(`couldn't add reaction to ${this.messageID}`, err); 26 | this.working = false; 27 | }); 28 | } 29 | 30 | this.client.helper.log(this.message, `started watching ${this.messageID}`); 31 | } 32 | 33 | sleep(ms) { 34 | return new Promise((resolve) => setTimeout(resolve, ms)); 35 | } 36 | 37 | edit(emoji, userID) { 38 | if (this.working === false) return; 39 | 40 | this.client.removeMessageReaction(this.channelID, this.messageID, emoji, userID).catch((err) => { 41 | this.client.helper.log(this.message, err); 42 | }); 43 | this.client.editMessage(this.channelID, this.messageID, this.map[this.position]).catch((err) => { 44 | this.client.helper.handle(this.message, err); 45 | }); 46 | } 47 | 48 | handle(message, emoji, userID) { 49 | if (userID !== this.userID) return; 50 | emoji = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; 51 | 52 | if (this.behavior == "p/n") { 53 | if (emoji == this.previousEmoji && this.position > 0) { 54 | this.sleep(250).then(() => { 55 | this.position -= 1; 56 | this.edit(emoji, userID); 57 | }); 58 | } 59 | 60 | if (emoji == this.nextEmoji && this.position + 1 < this.map.length) { 61 | this.sleep(250).then(() => { 62 | this.position += 1; 63 | this.edit(emoji, userID); 64 | }); 65 | } 66 | } 67 | } 68 | } 69 | 70 | module.exports = Watcher; 71 | -------------------------------------------------------------------------------- /commands/static/ability.js: -------------------------------------------------------------------------------- 1 | const abilities = require("../../json/abilities"); 2 | const abilityEmbed = require("../../embeds/ability"); 3 | const findHero = require("../../util/findHero"); 4 | const ReactionChooser = require("../../classes/reactionChooser"); 5 | 6 | const FuzzySet = require("fuzzyset.js"); 7 | const fuzzy = FuzzySet(abilities.filter((a) => a.dname).map((a) => a.dname)); 8 | 9 | async function exec(ctx) { 10 | let hero_name = false; 11 | 12 | let key = ctx.options.find((option) => { 13 | if (["q", "w", "e", "d", "f", "r"].includes(option.toLowerCase())) return true; 14 | if ((option.length == 3 && option.toLowerCase().match(/[qwe][qwe][qwe]/)) || option.toLowerCase() == "emp") return true; 15 | }); 16 | if (key) ctx.options.splice(ctx.options.indexOf(key), 1); 17 | 18 | if (key && key.length == 1) { 19 | hero_name = findHero(ctx.options.join(" ")); 20 | hero_name = hero_name && hero_name.name; 21 | } else if (key && key.length == 3) { 22 | hero_name = "invoker"; 23 | if (key == "emp") key = "www"; 24 | key = key.split("").sort().join(""); 25 | } 26 | 27 | if (hero_name == "troll_warlord" && key == "e") key = "d"; 28 | if (hero_name == "troll_warlord" && key == "w" && ctx.content.match("melee")) key = "e"; 29 | 30 | let res = fuzzy.get(ctx.options.join(" ")); 31 | let skill = abilities.filter((ability) => { 32 | if (hero_name) { 33 | if (ability.hero.name == hero_name && ability.key == key.toLowerCase()) return true; 34 | } else { 35 | if (res && res[0][0] > 0.7 && res[0][1] == ability.dname) return true; 36 | } 37 | }); 38 | 39 | if (skill.length == 1) { 40 | return ctx.embed(abilityEmbed(skill[0])); 41 | } else { 42 | let conflicts = abilities.filter((ability) => ability.dname && ability.dname.toLowerCase().match(ctx.options.join(" "))); 43 | 44 | if (conflicts.length > 10) { 45 | return ctx.delete(10000, `:x: ${ctx.strings.get("ability_not_found_conflicts", conflicts.map((conflict) => conflict.dname).join(", "))}`); 46 | } else if (conflicts.length == 1) { 47 | return ctx.embed(abilityEmbed(conflicts[0])); 48 | } else if (conflicts.length) { 49 | let map = conflicts.map((item, index) => `${index}\u20e3 ${item.dname}`); 50 | map.unshift(ctx.strings.get("ability_multiple_conflicts"), ""); 51 | let msg = await ctx.embed({ 52 | description: map.join("\n") 53 | }); 54 | 55 | ctx.client.watchers[msg.id] = new ReactionChooser(ctx, msg, conflicts.map((conflict) => abilityEmbed(conflict))); 56 | return Promise.resolve(); 57 | } else { 58 | return ctx.delete(10000, `:x: ${ctx.strings.get("ability_not_found")}`); 59 | } 60 | } 61 | } 62 | 63 | module.exports = { 64 | name: "ability", 65 | category: "static", 66 | aliases: ["skill", "spell"], 67 | triviaCheat: true, 68 | exec 69 | }; 70 | -------------------------------------------------------------------------------- /commands/esports/unsub.js: -------------------------------------------------------------------------------- 1 | const Unwatcher = require("../../classes/unwatcher"); 2 | 3 | async function checks(client, member) { 4 | return member.permission.has("manageMessages"); 5 | } 6 | 7 | async function exec(ctx) { 8 | if (ctx.options[0] == "nuke") { 9 | try { 10 | let res = await ctx.client.pg.query({ 11 | text: "DELETE FROM subs WHERE channel = $1;", 12 | values: [ctx.channel.id] 13 | }); 14 | 15 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 16 | action: "refresh" 17 | })); 18 | 19 | return ctx.success(ctx.strings.get("unsub_nuke_success", res.rowCount, ctx.channel.mention)); 20 | } catch (err) { 21 | ctx.error(err); 22 | return ctx.failure(ctx.strings.get("bot_generic_error")); 23 | } 24 | } 25 | 26 | if (ctx.options[0] == "stacks") { 27 | try { 28 | let res = await ctx.client.pg.query({ 29 | text: "DELETE FROM subs WHERE owner = $1;", 30 | values: [ctx.guild.id] 31 | }); 32 | 33 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 34 | action: "refresh" 35 | })); 36 | 37 | return ctx.success(ctx.strings.get("unsub_success", "stacks")); 38 | } catch (err) { 39 | ctx.error(err); 40 | return ctx.failure(ctx.strings.get("bot_generic_error")); 41 | } 42 | } 43 | 44 | try { 45 | let res = await ctx.client.pg.query({ 46 | "text": "SELECT * FROM subs WHERE channel = $1;", 47 | "values": [ctx.channel.id] 48 | }); 49 | 50 | let currentSubs = []; 51 | 52 | if (res.rows.find((row) => row.owner === ctx.guild.id)) { 53 | currentSubs.push("stacks"); 54 | } 55 | 56 | currentSubs.push(...res.rows.filter((row) => row.owner === ctx.author.id).map((row) => `${row.type}:${row.value}`)); 57 | 58 | if (currentSubs.length) { 59 | let submap = currentSubs.map((sub, index) => `${index}: \`${sub}\``).join(", "); 60 | let msg = [ 61 | ctx.strings.get("unsub_message_1", ctx.author.mention), 62 | "", 63 | submap, 64 | "", 65 | ctx.strings.get("unsub_message_3"), 66 | ctx.strings.get("unsub_message_4", ctx.gcfg.prefix) 67 | ].join("\n"); 68 | 69 | await ctx.send(msg); 70 | ctx.client.unwatchers[`${ctx.channel.id}:${ctx.author.id}`] = new Unwatcher(ctx, currentSubs); 71 | return Promise.resolve(); 72 | } else { 73 | return ctx.failure(ctx.strings.get("unsub_no_feeds", ctx.author.mention)); 74 | } 75 | } catch (err) { 76 | ctx.error(err); 77 | return ctx.failure(ctx.strings.get("bot_generic_error")); 78 | } 79 | } 80 | 81 | module.exports = { 82 | name: "unsub", 83 | category: "esports", 84 | checks, 85 | exec 86 | }; 87 | -------------------------------------------------------------------------------- /commands/fun/courage.js: -------------------------------------------------------------------------------- 1 | const findHero = require("../../util/findHero"); 2 | const getBuffer = require("../../util/getBuffer"); 3 | 4 | const aliases = require("../../json/aliases.json"); 5 | const allItems = require("../../util/transformConstants")(require("dotaconstants").items); 6 | const courageItems = require("../../json/courage.json"); 7 | 8 | var Canvas = require("canvas"); 9 | var Image = Canvas.Image; 10 | 11 | const all_boots = [ 12 | "travel_boots", 13 | "phase_boots", 14 | "power_treads", 15 | "arcane_boots", 16 | "tranquil_boots", 17 | "guardian_greaves" 18 | ]; 19 | 20 | const locations = [ 21 | {"x": 0, "y": 0, "w": 110, "h": 128}, 22 | {"x": 110, "y": 0, "w": 85, "h": 64}, 23 | {"x": 195, "y": 0, "w": 85, "h": 64}, 24 | {"x": 280, "y": 0, "w": 85, "h": 64}, 25 | {"x": 110, "y": 64, "w": 85, "h": 64}, 26 | {"x": 195, "y": 64, "w": 85, "h": 64}, 27 | {"x": 280, "y": 64, "w": 85, "h": 64}, 28 | ]; 29 | 30 | async function exec(ctx) { 31 | let override = ctx.options.join(" "); 32 | 33 | let hero = findHero(override) || aliases[Math.floor(Math.random() * aliases.length)]; 34 | let rand_boot = all_boots[Math.floor(Math.random() * all_boots.length)]; 35 | let boots = allItems.find((item) => item.name == rand_boot); 36 | 37 | let items = []; 38 | for (let i = 0; i < 5; i++) { 39 | items.push(courageItems[Math.floor(Math.random() * courageItems.length)]); 40 | } 41 | 42 | let copypaste = `${hero.local} with ${boots.dname}, ${items.map((item) => allItems.find((a) => a.name == item).dname).join(", ")}`; 43 | let to_download = [ 44 | `/images/heroes/${hero.name}_vert.jpg`, 45 | `/images/items/${boots.name}_lg.png` 46 | ]; 47 | to_download.push(...items.map((item) => `/images/items/${item}_lg.png`)); 48 | 49 | let res; 50 | try { 51 | let promises = to_download.map((uri) => getBuffer(`http://cdn.dota2.com/apps/dota2${uri}`, `${__dirname}/../..${uri}`)); 52 | res = await Promise.all(promises); 53 | } catch (err) { 54 | ctx.error(err); 55 | return ctx.failure(ctx.strings.get("bot_generic_error")); 56 | } 57 | 58 | var canvas = new Canvas(365, 128); 59 | var context = canvas.getContext("2d"); 60 | for (let img in res) { 61 | let image = new Image(); 62 | image.src = res[img]; 63 | 64 | let loc = locations[img]; 65 | context.drawImage(image, loc.x, loc.y, loc.w, loc.h); 66 | 67 | if (img == res.length - 1) { 68 | let buff = []; 69 | canvas.pngStream().on("data", (data) => { 70 | buff.push(data); 71 | }).on("end", () => { 72 | let data = Buffer.concat(buff); 73 | 74 | return ctx.send({ 75 | "content": copypaste 76 | }, { 77 | "file": data, 78 | "name": "lol.png" 79 | }); 80 | }); 81 | } 82 | } 83 | } 84 | 85 | module.exports = { 86 | name: "courage", 87 | category: "fun", 88 | typing: true, 89 | exec 90 | }; 91 | -------------------------------------------------------------------------------- /commands/esports/prommr.js: -------------------------------------------------------------------------------- 1 | const prommrEmbed = require("../../embeds/prommr"); 2 | const regions = ["americas", "europe", "se_asia", "china"]; 3 | 4 | async function exec(ctx) { 5 | if (regions.includes(ctx.options[0])) { 6 | try { 7 | let reply = await ctx.client.redis.getAsync(`prommr:${ctx.options[0]}`); 8 | reply = JSON.parse(reply); 9 | reply.leaderboard = reply.leaderboard.map((item, index) => { 10 | item.solo_mmr = index + 1; 11 | return item; 12 | }); 13 | 14 | reply.region = ctx.options[0]; 15 | reply.regions = regions; 16 | 17 | let embed = prommrEmbed.call(ctx.strings, reply); 18 | return ctx.embed(embed); 19 | } catch (err) { 20 | ctx.error(err); 21 | return ctx.failure(ctx.strings.get("bot_generic_error")); 22 | } 23 | } else { 24 | let promises = regions.map((region) => ctx.client.redis.getAsync(`prommr:${region}`)); 25 | let replies = await Promise.all(promises); 26 | replies = replies.map((reply) => JSON.parse(reply)); 27 | 28 | let all = { 29 | time_posted: 0, 30 | region: "all", 31 | regions 32 | }; 33 | 34 | let leaderboard = []; 35 | 36 | replies.forEach((reply) => { 37 | reply.leaderboard.forEach((item, index) => { 38 | item.solo_mmr = index + 1; 39 | leaderboard.push(item); 40 | }); 41 | 42 | if (all.time_posted === 0 || all.time_posted > reply.time_posted) { 43 | all.time_posted = reply.time_posted; 44 | } 45 | }); 46 | 47 | leaderboard.sort((a, b) => a.solo_mmr - b.solo_mmr); 48 | 49 | let filtered; 50 | if (ctx.options[0]) { 51 | filtered = leaderboard.filter((row) => row.country == ctx.options[0]); 52 | all.region = ctx.options[0]; 53 | } else { 54 | filtered = leaderboard; 55 | } 56 | 57 | if (filtered.length) { 58 | all.leaderboard = filtered; 59 | let embed = prommrEmbed.call(ctx.strings, all); 60 | return ctx.embed(embed); 61 | } else { 62 | let regionsmap = regions.map((region) => `\`${region}\``).join(", "); 63 | let countries = leaderboard 64 | .map((row) => row.country) 65 | .filter((item, index, array) => item && array.indexOf(item) === index) 66 | .map((country) => `:flag_${country}: \`${country}\``); 67 | 68 | let rows = []; 69 | 70 | while (countries.length && rows.map((row) => row.join(" ")).join("\n").length < 1750) { 71 | rows.push(countries.splice(0, 8)); 72 | } 73 | 74 | text = [ 75 | ctx.strings.get("prommr_success", regionsmap), 76 | rows.map((row) => row.join(" ")).join("\n") 77 | ].join("\n"); 78 | return ctx.send(text); 79 | } 80 | } 81 | } 82 | 83 | module.exports = { 84 | name: "prommr", 85 | category: "esports", 86 | exec 87 | }; 88 | -------------------------------------------------------------------------------- /commands/static/item.js: -------------------------------------------------------------------------------- 1 | const too_short = ["of", "and", "in", "the", "de"]; 2 | 3 | const items = require("../../json/items"); 4 | const itemEmbed = require("../../embeds/item"); 5 | const ReactionChooser = require("../../classes/reactionChooser"); 6 | 7 | async function exec(ctx) { 8 | let options = ctx.content.toLowerCase().split(" ").slice(1); 9 | 10 | let result = false; 11 | let search; 12 | let conflicts = []; 13 | 14 | loop1: for (let i = 0; i <= options.length; i++) { 15 | loop2: for (let j = 0; j <= options.length; j++) { 16 | if (i < j) { 17 | let term = options.slice(i, j).join(" "); 18 | if (too_short.includes(term)) break loop2; 19 | 20 | search = items.filter((item) => { 21 | if (item.name.startsWith("recipe")) return false; 22 | if (item.name == "sange_and_yasha" & (term.toLowerCase() == "yasha" | term.toLowerCase() == "sange")) return false; 23 | if (item.name == "sange" && options.join(" ").toLowerCase() == "sange and yasha") return false; 24 | if (too_short.includes(item)) return false; 25 | if (item.dname && item.dname.toLowerCase().match(term) && term.length > 2) return true; 26 | //if ((item.aliases || []).includes(term)) return true; 27 | if (item.name.split("_").includes(term)) return true; 28 | if (item.dname && item.dname.split(" ").map(item => item.charAt(0)).join("").toLowerCase() == term) return true; 29 | }); 30 | 31 | if (search.length == 1) { 32 | result = search[0]; 33 | break loop1; 34 | } else if (search.length > 1) { 35 | conflicts.push(...search); 36 | } 37 | } 38 | } 39 | } 40 | 41 | if (result && search.length > 0) { 42 | let embed = itemEmbed(result, items); 43 | return ctx.embed(embed); 44 | } else { 45 | let content = ctx.strings.get("item_not_found"); 46 | let reduced = conflicts.filter((item, inc, newlist) => newlist.indexOf(item) === inc); 47 | if (reduced.length > 1) { 48 | let map = reduced.map((item, index) => `${index}\u20e3 ${item.dname}`); 49 | map.unshift(ctx.strings.get("item_multiple_conflicts"), ""); 50 | let msg = await ctx.embed({ 51 | description: map.join("\n") 52 | }); 53 | 54 | ctx.client.watchers[msg.id] = new ReactionChooser(ctx, msg, reduced.map((conflict) => itemEmbed(conflict, items))); 55 | return Promise.resolve(); 56 | } else if (reduced.length == 1) { 57 | let embed = itemEmbed(items.find((item) => item.dname == conflicts[0]), items); 58 | return ctx.embed(embed); 59 | } 60 | 61 | return ctx.send(content).then((new_message) => { 62 | setTimeout(() => { 63 | new_message.delete(); 64 | }, 10000); 65 | }); 66 | } 67 | } 68 | 69 | module.exports = { 70 | name: "item", 71 | category: "static", 72 | triviaCheat: true, 73 | exec 74 | }; 75 | -------------------------------------------------------------------------------- /embeds/singleMmr.js: -------------------------------------------------------------------------------- 1 | const snekfetch = require("snekfetch"); 2 | const tnhConfig = require("../config.json").thinknohands; 3 | 4 | const ranks = { 5 | "0": "Uncalibrated", 6 | 7 | "10": "Herald", 8 | "11": "Herald [1]", 9 | "12": "Herald [2]", 10 | "13": "Herald [3]", 11 | "14": "Herald [4]", 12 | "15": "Herald [5]", 13 | "16": "Herald [6]", 14 | "17": "Herald [7]", 15 | 16 | "20": "Guardian", 17 | "21": "Guardian [1]", 18 | "22": "Guardian [2]", 19 | "23": "Guardian [3]", 20 | "24": "Guardian [4]", 21 | "25": "Guardian [5]", 22 | "26": "Guardian [6]", 23 | "27": "Guardian [7]", 24 | 25 | "30": "Crusader", 26 | "31": "Crusader [1]", 27 | "32": "Crusader [2]", 28 | "33": "Crusader [3]", 29 | "34": "Crusader [4]", 30 | "35": "Crusader [5]", 31 | "36": "Crusader [6]", 32 | "37": "Crusader [7]", 33 | 34 | "40": "Archon", 35 | "41": "Archon [1]", 36 | "42": "Archon [2]", 37 | "43": "Archon [3]", 38 | "44": "Archon [4]", 39 | "45": "Archon [5]", 40 | "46": "Archon [6]", 41 | "47": "Archon [7]", 42 | 43 | "50": "Legend", 44 | "51": "Legend [1]", 45 | "52": "Legend [2]", 46 | "53": "Legend [3]", 47 | "54": "Legend [4]", 48 | "55": "Legend [5]", 49 | "56": "Legend [6]", 50 | "57": "Legend [7]", 51 | 52 | "60": "Ancient", 53 | "61": "Ancient [1]", 54 | "62": "Ancient [2]", 55 | "63": "Ancient [3]", 56 | "64": "Ancient [4]", 57 | "65": "Ancient [5]", 58 | "66": "Ancient [6]", 59 | "67": "Ancient [7]", 60 | 61 | "70": "Divine", 62 | "71": "Divine [1]", 63 | "72": "Divine [2]", 64 | "73": "Divine [3]", 65 | "74": "Divine [4]", 66 | "75": "Divine [5]", 67 | "76": "Divine [6]", 68 | "77": "Divine [7]", 69 | 70 | "80": "Immortal", 71 | "81": "Immortal", 72 | "82": "Immortal", 73 | } 74 | 75 | async function singleMmr(data) { 76 | let url = `${tnhConfig.url}/rank?key=${tnhConfig.key}&badge=${data.tier}`; 77 | if (data.rank) url += `&rank=${data.rank}`; 78 | let image = await snekfetch.get(url); 79 | 80 | return { 81 | "file": { 82 | "name": "rank.png", 83 | "file": image.body 84 | }, 85 | "embed": { 86 | "author": { 87 | "icon_url": data.member.avatarURL, 88 | "name": `${data.member.username} is ${data.rank ? "Rank " + data.rank : ranks[data.tier]}` 89 | }, 90 | "description": [ 91 | `**${this.get("history_with_winloss")}:** ${data.winlose.win}/${data.winlose.lose}`, 92 | `**${this.get("history_as_games")}:** ${data.winlose.win + data.winlose.lose}`, 93 | `**${this.get("history_as_winrate")}:** ${Math.round(data.winlose.win / (data.winlose.win + data.winlose.lose) * 10000) / 100}%` 94 | ].join("\n"), 95 | "timestamp": new Date(Date.now()), 96 | "footer": { 97 | "text": this.get("prommr_last_updated") 98 | }, 99 | "thumbnail": { 100 | "url": "attachment://rank.png" 101 | } 102 | } 103 | }; 104 | } 105 | 106 | module.exports = singleMmr; 107 | -------------------------------------------------------------------------------- /embeds/liveMatch.js: -------------------------------------------------------------------------------- 1 | const tnhConfig = require("../config.json").thinknohands; 2 | const prettyms = require("pretty-ms"); 3 | const heroes = require("dotaconstants").heroes; 4 | const snekfetch = require("snekfetch"); 5 | 6 | const types = { 7 | "0": "Best of 1", 8 | "1": "Best of 3", 9 | "2": "Best of 5" 10 | }; 11 | 12 | async function liveMatchEmbed(match) { 13 | let response = {}; 14 | 15 | let embed = { 16 | title: `\`${match.match_id}\`: ${match.radiant_team.team_name} vs. ${match.dire_team.team_name}`, 17 | fields: [] 18 | }; 19 | 20 | if (match.scoreboard.duration > 0) { 21 | embed.fields.push({ 22 | name: "Radiant", 23 | value: match.players 24 | .filter((player) => player.team == 0) 25 | .map((player) => `**${player.name}** playing **${heroes[player.hero_id] ? heroes[player.hero_id].localized_name : "Unknown Hero"}**`) 26 | .join("\n"), 27 | inline: true 28 | }, { 29 | name: "Dire", 30 | value: match.players 31 | .filter((player) => player.team == 1) 32 | .map((player) => `**${player.name}** playing **${heroes[player.hero_id] ? heroes[player.hero_id].localized_name : "Unknown Hero"}**`) 33 | .join("\n"), 34 | inline: true 35 | }, { 36 | name: "Stats", 37 | value: [ 38 | `**Radiant:** ${match.scoreboard.radiant.score}`, 39 | `**In Game Time:** ${prettyms(Math.floor(match.scoreboard.duration) * 1000)}`, 40 | `**Spectators:** ${match.spectators}`, 41 | `**Series Type:** ${types[match.series_type]}`, 42 | ].join("\n"), 43 | inline: true 44 | }, { 45 | name: "\u200b", 46 | value: [ 47 | `**Dire:** ${match.scoreboard.dire.score}`, 48 | match.scoreboard.roshan_respawn_timer > 0 ? `**Roshan respawns in:** ${prettyms(match.scoreboard.roshan_respawn_timer * 1000)}` : "Roshan is **alive**", 49 | `**Stream Delay:** ${match.stream_delay_s}`, 50 | match.series_type && `**Radiant Score:** ${match.radiant_series_wins}, **Dire Score:** ${match.dire_series_wins}` 51 | ].join("\n"), 52 | inline: true 53 | }); 54 | } else if (match.scoreboard.radiant && match.scoreboard.dire) { 55 | embed.image = { url: "attachment://draft.png" }; 56 | 57 | let url = `${tnhConfig.url}/draft?key=${tnhConfig.key}&`; 58 | let queries = []; 59 | if (match.scoreboard.radiant.picks.length) queries.push(`radiant_picks=${match.scoreboard.radiant.picks.join(",")}`); 60 | if (match.scoreboard.dire.picks.length) queries.push(`dire_picks=${match.scoreboard.dire.picks.join(",")}`); 61 | if (match.scoreboard.radiant.bans.length) queries.push(`radiant_bans=${match.scoreboard.radiant.bans.join(",")}`); 62 | if (match.scoreboard.dire.bans.length) queries.push(`dire_bans=${match.scoreboard.dire.bans.join(",")}`); 63 | url += queries.join("&"); 64 | 65 | let img = await snekfetch.get(url); 66 | response.file = { 67 | name: "draft.png", 68 | file: img.body 69 | }; 70 | } else { 71 | embed.description = "Nothing has happened yet!"; 72 | } 73 | 74 | response.content = { embed }; 75 | 76 | return response; 77 | } 78 | 79 | module.exports = liveMatchEmbed; 80 | -------------------------------------------------------------------------------- /json/feeds.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Dota 2 Blog", 3 | "key": "listen:rss:blog", 4 | "url": "http://blog.dota2.com/feed/", 5 | "type": "rss", 6 | "cron": "*/5 * * * *", 7 | "action": "publish" 8 | }, { 9 | "name": "Steam News Feed", 10 | "key": "listen:rss:steamnews", 11 | "url": "http://api.steampowered.com/ISteamNews/GetNewsForApp/v0002/", 12 | "querystring": { 13 | "appid": 570, 14 | "maxlength": 300, 15 | "format": "json" 16 | }, 17 | "type": "json", 18 | "cron": "*/5 * * * *", 19 | "action": "publish" 20 | }, { 21 | "name": "Sir Belvedere Updates", 22 | "key": "listen:rss:belvedere", 23 | "url": "https://old.reddit.com/user/SirBelvedere/submitted/.rss", 24 | "type": "rss", 25 | "cron": "*/5 * * *", 26 | "action": "publish" 27 | }, { 28 | "name": "Magesunite Updates", 29 | "key": "listen:rss:magesunite", 30 | "url": "https://old.reddit.com/user/Magesunite/submitted/.rss", 31 | "type": "rss", 32 | "cron": "*/5 * * *", 33 | "action": "publish" 34 | }, { 35 | "name": "Wykrhm Updates", 36 | "key": "listen:rss:wykrhm", 37 | "url": "https://old.reddit.com/user/wykrhm/submitted/.rss", 38 | "type": "rss", 39 | "cron": "*/5 * * * *", 40 | "action": "publish" 41 | }, { 42 | "name": "Pro MMR: Americas", 43 | "key": "prommr:americas", 44 | "url": "http://www.dota2.com/webapi/ILeaderboard/GetDivisionLeaderboard/v0001", 45 | "querystring": { 46 | "division": "americas" 47 | }, 48 | "type": "json", 49 | "cron": "15 * * * *", 50 | "action": "store" 51 | }, { 52 | "name": "Pro MMR: Europe", 53 | "key": "prommr:europe", 54 | "url": "http://www.dota2.com/webapi/ILeaderboard/GetDivisionLeaderboard/v0001", 55 | "querystring": { 56 | "division": "europe" 57 | }, 58 | "type": "json", 59 | "cron": "15 * * * *", 60 | "action": "store" 61 | }, { 62 | "name": "Pro MMR: South East Asia", 63 | "key": "prommr:se_asia", 64 | "url": "http://www.dota2.com/webapi/ILeaderboard/GetDivisionLeaderboard/v0001", 65 | "querystring": { 66 | "division": "se_asia" 67 | }, 68 | "type": "json", 69 | "cron": "15 * * * *", 70 | "action": "store" 71 | }, { 72 | "name": "Pro MMR: China", 73 | "key": "prommr:china", 74 | "url": "http://www.dota2.com/webapi/ILeaderboard/GetDivisionLeaderboard/v0001", 75 | "querystring": { 76 | "division": "china" 77 | }, 78 | "type": "json", 79 | "cron": "15 * * * *", 80 | "action": "store" 81 | }, { 82 | "name": "Twitch Clips", 83 | "key": "twitch:clips", 84 | "url": "https://api.twitch.tv/kraken/clips/top", 85 | "querystring": { 86 | "game": "Dota 2", 87 | "period": "day", 88 | "limit": 20 89 | }, 90 | "headers": { 91 | "Accept": "application/vnd.twitchtv.v4+json" 92 | }, 93 | "authtype": "twitch", 94 | "type": "json", 95 | "cron": "*/60 * * * *", 96 | "action": "store" 97 | }, { 98 | "name": "Twitch Streams", 99 | "key": "twitch:streams", 100 | "url": "https://api.twitch.tv/kraken/streams", 101 | "querystring": { 102 | "game": "Dota 2", 103 | "limit": 100 104 | }, 105 | "headers": { 106 | "Accept": "application/vnd.twitchtv.v5+json" 107 | }, 108 | "authtype": "twitch", 109 | "type": "json", 110 | "cron": "*/10 * * * *", 111 | "action": "store" 112 | }] 113 | -------------------------------------------------------------------------------- /commands/personal/lastmatch.js: -------------------------------------------------------------------------------- 1 | const eat = require("../../util/eat"); 2 | const searchMembers = require("../../util/searchMembers"); 3 | const findHero = require("../../util/findHero"); 4 | const checkDiscordID = require("../../util/checkDiscordID"); 5 | 6 | async function exec(ctx) { 7 | let result = await eat(ctx.content, { 8 | of: "member", 9 | as: "string", 10 | with: "member", 11 | in: "string" 12 | }, ctx.guild.members); 13 | 14 | // here we do a whole bunch of bullshit 15 | let discordIDs = []; 16 | let mikaOpts = { 17 | limit: 1, 18 | significant: 0 19 | }; 20 | 21 | // if no match assume its a member 22 | if (!(Object.keys(result).length)) { 23 | let searchRes = await searchMembers(ctx.guild.members, ctx.options); 24 | if (searchRes.found) { 25 | result.of = searchRes.all; 26 | } else { 27 | discordIDs.push(ctx.author.id); 28 | } 29 | } 30 | 31 | if (result.of && result.of.found) { 32 | discordIDs.push(...result.of.all); 33 | } 34 | 35 | if (result.with && result.with.found) { 36 | discordIDs.push(ctx.author.id); 37 | discordIDs.push(...result.with.all); 38 | } 39 | 40 | if (result.of && result.with) { 41 | discordIDs.splice(discordIDs.indexOf(ctx.author.id), 1) 42 | } 43 | 44 | if (!discordIDs.length) { 45 | discordIDs.push(ctx.author.id); 46 | } 47 | 48 | if (result.as) { 49 | let hero = findHero(result.as); 50 | if (hero) { 51 | mikaOpts.hero_id = hero.id; 52 | } else { 53 | return ctx.failure(ctx.strings.get("bot_no_hero_error")); 54 | } 55 | } 56 | 57 | let results; 58 | try { 59 | let promises = discordIDs.map((id) => checkDiscordID(ctx.client.pg, id)); 60 | results = await Promise.all(promises); 61 | } catch (err) { 62 | ctx.error(err); 63 | return ctx.failure(ctx.strings.get("bot_generic_error")); 64 | } 65 | 66 | results = results.map((result, index) => { 67 | return { 68 | discordID: discordIDs[index], 69 | dotaID: result 70 | }; 71 | }); 72 | 73 | let nullcheck = results.find((result) => result.dotaID === null); 74 | if (nullcheck) { 75 | let username = ctx.client.users.get(nullcheck.discordID).username; 76 | return ctx.failure(ctx.strings.get("bot_not_registered", username, ctx.gcfg.prefix)); 77 | } 78 | 79 | if (results.length > 1) { 80 | mikaOpts.included_account_id = results.map((result) => result.dotaID).slice(1); 81 | } 82 | 83 | if (result.in) { 84 | if (result.in == "ranked") { 85 | mikaOpts.lobby_type = 7; 86 | } else if (result.in == "normal") { 87 | mikaOpts.lobby_type = 0; 88 | } 89 | } 90 | 91 | let match; 92 | try { 93 | match = await ctx.client.mika.getPlayerMatches(results[0].dotaID, mikaOpts); 94 | if (match.length) { 95 | match = match[0]; 96 | } else { 97 | return ctx.failure(ctx.strings.get("lastmatch_no_match")); 98 | } 99 | } catch (err) { 100 | ctx.error(err); 101 | return ctx.failure(ctx.strings.get("bot_mika_error")); 102 | } 103 | 104 | ctx.match_id = match.match_id; 105 | return ctx.client.commands.matchinfo.exec(ctx); 106 | } 107 | 108 | module.exports = { 109 | name: "lastmatch", 110 | category: "personal", 111 | aliases: ["lm", "lastgame"], 112 | typing: true, 113 | exec 114 | }; 115 | -------------------------------------------------------------------------------- /commands/personal/matches.js: -------------------------------------------------------------------------------- 1 | const eat = require("../../util/eat"); 2 | const searchMembers = require("../../util/searchMembers"); 3 | const findHero = require("../../util/findHero"); 4 | const checkDiscordID = require("../../util/checkDiscordID"); 5 | const matchesEmbed = require("../../embeds/matches"); 6 | 7 | async function exec(ctx) { 8 | let result = await eat(ctx.content, { 9 | of: "member", 10 | as: "string", 11 | with: "member", 12 | in: "string" 13 | }, ctx.guild.members); 14 | 15 | // here we do a whole bunch of bullshit 16 | let discordIDs = []; 17 | let mikaOpts = { 18 | limit: 12, 19 | significant: 0 20 | }; 21 | 22 | // if no match assume its a member 23 | if (!(Object.keys(result).length)) { 24 | let searchRes = await searchMembers(ctx.guild.members, ctx.options); 25 | if (searchRes.found) { 26 | result.of = searchRes.all; 27 | } else { 28 | discordIDs.push(ctx.author.id); 29 | } 30 | } 31 | 32 | if (result.of && result.of.found) { 33 | discordIDs.push(...result.of.all); 34 | } 35 | 36 | if (result.with && result.with.found) { 37 | discordIDs.push(ctx.author.id); 38 | discordIDs.push(...result.with.all); 39 | } 40 | 41 | if (result.of && result.with) { 42 | discordIDs.splice(discordIDs.indexOf(ctx.author.id), 1) 43 | } 44 | 45 | if (!discordIDs.length) { 46 | discordIDs.push(ctx.author.id); 47 | } 48 | 49 | if (result.as) { 50 | let hero = findHero(result.as); 51 | if (hero) { 52 | mikaOpts.hero_id = hero.id; 53 | } else { 54 | return ctx.failure(ctx.strings.get("bot_no_hero_error")); 55 | } 56 | } 57 | 58 | let results; 59 | try { 60 | let promises = discordIDs.map((id) => checkDiscordID(ctx.client.pg, id)); 61 | results = await Promise.all(promises); 62 | } catch (err) { 63 | ctx.error(err); 64 | return ctx.failure(ctx.strings.get("bot_generic_error")); 65 | } 66 | 67 | results = results.map((result, index) => { 68 | return { 69 | discordID: discordIDs[index], 70 | dotaID: result 71 | }; 72 | }); 73 | 74 | let nullcheck = results.find((result) => result.dotaID === null); 75 | if (nullcheck) { 76 | let username = ctx.client.users.get(nullcheck.discordID).username; 77 | return ctx.failure(ctx.strings.get("bot_not_registered", username, ctx.gcfg.prefix)); 78 | } 79 | 80 | if (results.length > 1) { 81 | mikaOpts.included_account_id = results.map((result) => result.dotaID).slice(1); 82 | } 83 | 84 | if (result.in) { 85 | if (result.in == "ranked") { 86 | mikaOpts.lobby_type = 7; 87 | } else if (result.in == "normal") { 88 | mikaOpts.lobby_type = 0; 89 | } 90 | } 91 | 92 | let matches; 93 | try { 94 | matches = await ctx.client.mika.getPlayerMatches(results[0].dotaID, mikaOpts); 95 | if (!matches.length) { 96 | return ctx.failure(ctx.strings.get("lastmatch_no_match")); 97 | } 98 | } catch (err) { 99 | ctx.error(err); 100 | return ctx.failure(ctx.strings.get("bot_mika_error")); 101 | } 102 | 103 | let text = matchesEmbed.call(ctx.strings, ctx, matches); 104 | return ctx.send(text); 105 | } 106 | 107 | module.exports = { 108 | name: "matches", 109 | category: "personal", 110 | typing: true, 111 | aliases: ["games", "m"], 112 | exec 113 | }; 114 | -------------------------------------------------------------------------------- /commands/meta/help.js: -------------------------------------------------------------------------------- 1 | const pad = require("pad"); 2 | 3 | function bold(arg1, arg2) { 4 | return `**${arg1}:** ${arg2}`; 5 | } 6 | 7 | async function formatHelp(ctx, command) { 8 | let rows = []; 9 | let searchString = `help_cmd_${command.name}_`; 10 | 11 | rows.push(bold(ctx.strings.get("help_name"), command.name)); 12 | 13 | let usage = ctx.strings.get(searchString + "usage", ctx.gcfg.prefix); 14 | if (usage && usage !== searchString + "usage") { 15 | rows.push(bold(ctx.strings.get("help_usage"), `\`${usage}\``)); 16 | } 17 | 18 | let desc = ctx.strings.get(searchString + "desc"); 19 | if (desc && desc !== searchString + "desc") { 20 | rows.push("", desc); 21 | } 22 | 23 | if (command.aliases) { 24 | rows.push("", `**${ctx.strings.get("help_aliases")}:** ${command.aliases.map((a) => `\`${a}\``).join(", ")}`); 25 | } 26 | 27 | let filters = ctx.strings.all(searchString + "filters", "array"); 28 | if (filters.length) { 29 | rows.push("", `**${ctx.strings.get("help_filters")}:**`, ...filters); 30 | } 31 | 32 | let args = ctx.strings.all(searchString + "args", "array"); 33 | if (args.length) { 34 | rows.push("", `**${ctx.strings.get("help_args")}:**`, ...args); 35 | } 36 | 37 | if (Math.floor(Math.random() * 5) == 0) rows.push("", ctx.strings.get("help_footer")); 38 | 39 | return ctx.send(rows.join("\n")); 40 | } 41 | 42 | async function exec(ctx) { 43 | let cmds = {}; 44 | let aliases = {}; 45 | let disabled = []; 46 | 47 | for (let command in ctx.client.commands) { 48 | command = ctx.client.commands[command]; 49 | 50 | if (command.checks) { 51 | let res = await command.checks(ctx.client, ctx.member); 52 | if (!res) continue; 53 | } 54 | 55 | if (ctx.gcfg.disabled && ctx.gcfg.disabled[ctx.channel.id] && ctx.gcfg.disabled[ctx.channel.id].includes(command.name)) { 56 | disabled.push(command.name); 57 | if (command.aliases) disabled.push(...command.aliases); 58 | continue; 59 | } 60 | 61 | if (command.category) { 62 | if (!cmds[command.category]) cmds[command.category] = []; 63 | cmds[command.category].push(command.name); 64 | } else { 65 | if (!cmds.uncategorized) cmds.uncategorized = []; 66 | cmds.uncategorized.push(command.name); 67 | } 68 | 69 | if (command.aliases) { 70 | command.aliases.forEach((alias) => { 71 | aliases[alias] = command.name; 72 | }); 73 | } 74 | 75 | aliases[command.name] = command.name; 76 | } 77 | 78 | if (aliases.hasOwnProperty(ctx.options[0])) { 79 | let command = ctx.client.commands[aliases[ctx.options[0]]]; 80 | return formatHelp(ctx, command); 81 | } else if (ctx.options.length === 0) { 82 | let longest = 0; 83 | for (let group in cmds) { 84 | group = cmds[group]; 85 | longest = longest < group.length ? group.length : longest; 86 | } 87 | 88 | let padlength = Object.values(aliases).sort((a, b) => b.length - a.length)[0].length; 89 | 90 | let rows = Object.keys(cmds).map((item) => pad(padlength, item.toUpperCase()) + ":"); 91 | rows = rows.map((row, index) => `${row} ${Object.values(cmds)[index].join(", ")}`) 92 | 93 | let grid = rows.join("\n"); 94 | 95 | let msg = [ 96 | ctx.strings.get("help_list_of_commands") + "```", 97 | grid, 98 | "```", 99 | ctx.strings.get("help_instruction", ctx.gcfg.prefix), 100 | ctx.strings.get("help_footer") 101 | ].join("\n"); 102 | 103 | return ctx.send(msg); 104 | } else if (disabled.includes(ctx.options[0])) { 105 | if (ctx.gcfg.botspam > 0) { 106 | return ctx.failure(ctx.strings.get("bot_botspam_redirect", ctx.gcfg.botspam)); 107 | } else { 108 | return ctx.failure(ctx.strings.get("bot_botspam")); 109 | } 110 | } else { 111 | return ctx.failure(ctx.strings.get("help_cant_find", ctx.options.join(" "))); 112 | } 113 | } 114 | 115 | module.exports = { 116 | name: "help", 117 | category: "meta", 118 | exec 119 | }; 120 | -------------------------------------------------------------------------------- /services/feeds.js: -------------------------------------------------------------------------------- 1 | const config = require("../config.json"); 2 | const feeds = require("../json/feeds.json"); 3 | 4 | const Redis = require("redis"); 5 | const schedule = require("node-schedule"); 6 | const needle = require("needle"); 7 | const queryString = require("../util/queryString"); 8 | 9 | const redis = Redis.createClient(); 10 | const jobs = {}; 11 | 12 | function log(str) { 13 | if (typeof str === "string") { 14 | console.log(`${new Date().toJSON()} [L_CRON] ${str}`); 15 | } else { 16 | console.log(str); 17 | } 18 | } 19 | 20 | function encodeURL(url, qs) { 21 | if (qs) url += queryString(qs); 22 | return url; 23 | } 24 | 25 | async function consumeResponse(feed, body) { 26 | if (feed.action === "store") { 27 | redis.set(feed.key, JSON.stringify(body), (err) => { 28 | err ? console.error(err) : log(`stored feed ${feed.name} in redis`); 29 | }); 30 | } else if (feed.action === "publish") { 31 | let post = {}; 32 | post.author = feed.name; 33 | 34 | if (feed.key === "listen:rss:blog") { 35 | post.title = body.rss.channel.item[0].title; 36 | post.link = body.rss.channel.item[0].link; 37 | post.guid = body.rss.channel.item[0].guid._; 38 | } 39 | 40 | if (feed.key === "listen:rss:steamnews") { 41 | let newsItem = body.appnews.newsitems.filter((item) => item.feedname === "steam_updates" || item.feedname === "steam_community_announcements")[0]; 42 | if (newsItem.date * 1000 > Date.now() - 3600000) { 43 | post.title = newsItem.title; 44 | post.link = newsItem.url; 45 | post.guid = newsItem.gid; 46 | } 47 | } 48 | 49 | if (feed.key === "listen:rss:belvedere" || feed.key === "listen:rss:wykrhm" || feed.key === "listen:rss:magesunite") { 50 | post.title = body.feed.entry[0].title; 51 | post.link = body.feed.entry[0].link.$.href; 52 | post.guid = body.feed.entry[0].id; 53 | 54 | if ((feed.key === "listen:rss:magesunite" || feed.key === "listen:rss:belvedere") && (!post.title.includes("Update")) || !post.link.includes("DotA2")) { 55 | post.guid = false; 56 | } 57 | } 58 | 59 | if (post.guid) { 60 | redis.get(`${feed.key}:last`, (err, reply) => { 61 | if (err) { 62 | console.error(err); 63 | } else if (reply !== post.guid) { 64 | log(`new post found for ${feed.name}, publishing`); 65 | redis.publish(feed.key, JSON.stringify(post)); 66 | redis.set(`${feed.key}:last`, post.guid, (err) => { 67 | if (err) console.log(require("util").inspect(err)); 68 | }); 69 | } 70 | }); 71 | } 72 | } 73 | } 74 | 75 | function executeJob(feed) { 76 | let url = encodeURL(feed.url, feed.querystring); 77 | 78 | let headers = {}; 79 | 80 | if (feed.authtype) { 81 | if (feed.authtype === "twitch") { 82 | headers["Client-ID"] = config.twitch.client_id; 83 | } 84 | } 85 | 86 | if (feed.headers) { 87 | for (header in feed.headers) { 88 | headers[header] = feed.headers[header]; 89 | } 90 | } 91 | 92 | needle.get(url, { headers }, (err, response, body) => { 93 | if (err || (response && response.statusCode !== 200)) { 94 | console.error(`something wrong with ${feed.name}.`); 95 | console.error(err); 96 | console.error(`status code: ${response ? response.statusCode : "0"}`); 97 | } else { 98 | consumeResponse(feed, body); 99 | } 100 | }); 101 | } 102 | 103 | redis.on("ready", () => { 104 | feeds.forEach((feed) => { 105 | log(`executing first job ${feed.name}`); 106 | executeJob(feed); 107 | }); 108 | 109 | feeds.forEach((feed) => { 110 | log(`subscribing to feed ${feed.name}: ${feed.cron}`); 111 | jobs[feed.key] = schedule.scheduleJob(feed.cron, function(feed) { 112 | log(`executing job ${feed.name}`); 113 | executeJob(feed); 114 | }.bind(null, feed)); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /embeds/playerinfo.js: -------------------------------------------------------------------------------- 1 | const snekfetch = require("snekfetch"); 2 | const tnhConfig = require("../config.json").thinknohands; 3 | const aliases = require("../json/aliases.json"); 4 | 5 | // i lazy 6 | const ranks = { 7 | "0": "Uncalibrated", 8 | 9 | "10": "Herald", 10 | "11": "Herald [1]", 11 | "12": "Herald [2]", 12 | "13": "Herald [3]", 13 | "14": "Herald [4]", 14 | "15": "Herald [5]", 15 | 16 | "20": "Guardian", 17 | "21": "Guardian [1]", 18 | "22": "Guardian [2]", 19 | "23": "Guardian [3]", 20 | "24": "Guardian [4]", 21 | "25": "Guardian [5]", 22 | 23 | "30": "Crusader", 24 | "31": "Crusader [1]", 25 | "32": "Crusader [2]", 26 | "33": "Crusader [3]", 27 | "34": "Crusader [4]", 28 | "35": "Crusader [5]", 29 | 30 | "40": "Archon", 31 | "41": "Archon [1]", 32 | "42": "Archon [2]", 33 | "43": "Archon [3]", 34 | "44": "Archon [4]", 35 | "45": "Archon [5]", 36 | 37 | "50": "Legend", 38 | "51": "Legend [1]", 39 | "52": "Legend [2]", 40 | "53": "Legend [3]", 41 | "54": "Legend [4]", 42 | "55": "Legend [5]", 43 | 44 | "60": "Ancient", 45 | "61": "Ancient [1]", 46 | "62": "Ancient [2]", 47 | "63": "Ancient [3]", 48 | "64": "Ancient [4]", 49 | "65": "Ancient [5]", 50 | 51 | "70": "Divine", 52 | "71": "Divine [1]", 53 | "72": "Divine [2]", 54 | "73": "Divine [3]", 55 | "74": "Divine [4]", 56 | "75": "Divine [5]", 57 | "76": "Divine Elite" 58 | } 59 | 60 | async function playerinfoEmbed(player) { 61 | if (!player.rank_tier) player.rank_tier = 0; 62 | let url = `${tnhConfig.url}/rank?key=${tnhConfig.key}&badge=${player.rank_tier}`; 63 | if (player.leaderboard_rank) url += `&rank=${player.leaderboard_rank}`; 64 | let image = await snekfetch.get(url); 65 | 66 | let winrate = (player.wl.win / (player.wl.win + player.wl.lose)); 67 | winrate = winrate * 10000; 68 | winrate = Math.round(winrate); 69 | winrate = winrate / 100; 70 | 71 | let countrycode = player.profile.loccountrycode || "Unknown"; 72 | let flag = countrycode == "Unknown" ? "" : `:flag_${countrycode.toLowerCase()}:`; 73 | 74 | let display_heroes = player.heroes 75 | .slice(0, 5) 76 | .map((hero) => { 77 | let name = aliases.find((alias) => alias.id == hero.hero_id).local; 78 | let winrate = (hero.win / hero.games); 79 | winrate = winrate * 10000; 80 | winrate = Math.round(winrate); 81 | winrate = winrate / 100; 82 | 83 | return `**${name}**: ${winrate}% with ${hero.games} games`; 84 | }); 85 | 86 | let dotabuff_link = `https://www.dotabuff.com/players/${player.profile.account_id}`; 87 | let opendota_link = `https://www.opendota.com/players/${player.profile.account_id}`; 88 | let stratz_link = `https://stratz.com/player/${player.profile.account_id}`; 89 | 90 | return { 91 | "file": { 92 | "name": "rank.png", 93 | "file": image.body 94 | }, 95 | "embed": { 96 | "author": { 97 | "name": player.profile.personaname, 98 | "icon_url": player.profile.avatarfull 99 | }, 100 | "fields": [{ 101 | "name": `Medal`, 102 | "value": player.leaderboard_rank ? "Rank " + player.leaderboard_rank : ranks[player.rank_tier], 103 | "inline": true 104 | }, { 105 | "name": "Wins/Losses", 106 | "value": `${player.wl.win}/${player.wl.lose} (${winrate}%)`, 107 | "inline": true 108 | }, { 109 | "name": "Country", 110 | "value": `${flag} ${countrycode}`, 111 | "inline": true 112 | }, { 113 | "name": "Links", 114 | "value": `[DB](${dotabuff_link}) / [OD](${opendota_link}) / [STRATZ](${stratz_link})`, 115 | "inline": true 116 | }, { 117 | "name": "Top 5 Heroes", 118 | "value": display_heroes.join("\n"), 119 | "inline": false 120 | }], 121 | "thumbnail": { 122 | "url": "attachment://rank.png" 123 | } 124 | } 125 | }; 126 | } 127 | 128 | module.exports = playerinfoEmbed; 129 | -------------------------------------------------------------------------------- /services/matches.js: -------------------------------------------------------------------------------- 1 | // subscribes to live match updates for players 2 | 3 | const config = require("../config.json"); 4 | const Redis = require("redis"); 5 | const Postgres = require("pg"); 6 | const FeedClient = require("mika").FeedClient; 7 | var client = new FeedClient(); 8 | 9 | const redis = Redis.createClient(); 10 | const sub = Redis.createClient(); 11 | const pg = new Postgres.Client(config.pgconfig); 12 | 13 | const types = ["player", "team", "league"]; 14 | 15 | sub.subscribe("listen:matches:new"); 16 | 17 | function log(str) { 18 | if (typeof str == "string") { 19 | console.log(`${new Date().toJSON()} [L_MATCHES] ${str}`); 20 | } else { 21 | console.log(str); 22 | } 23 | } 24 | 25 | function getNextMatchPlayers() { 26 | return new Promise((resolve, reject) => { 27 | redis.keys("listen:nextmatch:*", (err, reply) => { 28 | if (err) return reject(err); 29 | 30 | let map = reply.map((str) => Number(str.split(":")[2])); 31 | return resolve(map); 32 | }); 33 | }); 34 | } 35 | 36 | async function refresh() { 37 | try { 38 | let subs = await client.getSubs(); 39 | let res = await pg.query("SELECT * FROM subs WHERE type = 'player';"); 40 | 41 | let oldplayers = subs.player; 42 | let newplayers = res.rows.filter((row) => row.value != "1").map((row) => parseInt(row.value)).filter((item, index, array) => array.indexOf(item) === index); 43 | 44 | let nmplayers = await getNextMatchPlayers(); 45 | newplayers.push(...nmplayers); 46 | newplayers = newplayers.filter((item, index, array) => array.indexOf(item) === index); 47 | 48 | let toAdd = newplayers.filter((player) => !~oldplayers.indexOf(player)); 49 | let toRemove = oldplayers.filter((player) => !~newplayers.indexOf(player)); 50 | 51 | log(`attempting to add ${toAdd.length || 0} subs and remove ${toRemove.length || 0}, old count: ${oldplayers.length || 0}`); 52 | 53 | let all = await client.subscribe("player", toAdd); 54 | let removed = await client.unsubscribe("player", toRemove); 55 | let newsubs = await client.getSubs(); 56 | 57 | log(`added ${toAdd.length || 0}, removed ${removed.ids.length || 0}, new count: ${newsubs.player.length}`); 58 | } catch (err) { 59 | console.error(err); 60 | process.exit(1); 61 | } 62 | } 63 | 64 | sub.on("message", (channel, message) => { 65 | try { 66 | message = JSON.parse(message); 67 | } catch (err) { 68 | console.log(err); 69 | return; 70 | } 71 | 72 | if (message.action == "add") { 73 | client.subscribe(message.type, message.ids).catch((err) => console.error(err)).then((res) => { 74 | log(`added some ids of type ${res.type}, new length ${res.ids.length || 0}`); 75 | }); 76 | } else if (message.action == "remove") { 77 | client.unsubscribe(message.type, message.ids).catch((err) => console.error(err)).then((res) => { 78 | log(`removed some ids of type ${res.type}, amount removed ${res.ids.length || 0}`); 79 | }); 80 | } else if (message.action == "refresh") { 81 | refresh(); 82 | } 83 | }); 84 | 85 | pg.connect((err) => { 86 | if (err) { 87 | console.error(err); 88 | process.exit(1); 89 | } 90 | 91 | log("pg ready"); 92 | client.connect(); 93 | }); 94 | 95 | client.on("match", (match, found, origin) => { 96 | if (origin == "scanner") redis.publish("listen:matches:out", JSON.stringify({ 97 | id: match.match_id, 98 | found 99 | })); 100 | }); 101 | 102 | client.on("error", (err) => { 103 | console.error(err) 104 | process.exit(1); 105 | }); 106 | 107 | client.on("ready", () => { 108 | log("feed client ready"); 109 | pg.query("SELECT * FROM subs;").catch((err) => console.error(err)).then((res) => { 110 | let rows = res.rows.filter((row) => types.includes(row.type)); 111 | types.forEach((type) => { 112 | if (type == "player") { 113 | refresh(); 114 | } else { 115 | let ids = rows.filter((row) => row.type == type && row.value != "1").map((id) => id.value); 116 | if (ids.length > 0) { 117 | client.subscribe(type, ids).catch((err) => console.error(err)).then((res) => { 118 | log(`subscribed to ${res.ids.length || 0} ids of type ${type}`); 119 | }); 120 | } else { 121 | log(`couldn't find ids for type ${type}`); 122 | } 123 | } 124 | }); 125 | }); 126 | }); 127 | 128 | redis.on("ready", () => log("redis ready")); 129 | sub.on("ready", () => log("redis sub ready")); 130 | 131 | -------------------------------------------------------------------------------- /commands/fun/trivia.js: -------------------------------------------------------------------------------- 1 | const searchMembers = require("../../util/searchMembers"); 2 | 3 | const subcommands = { 4 | start: async function(ctx) { 5 | if (ctx.gcfg.trivia == ctx.channel.id && !ctx.client.trivia.channels.includes(ctx.channel.id)) { 6 | ctx.client.trivia.init(ctx.client, ctx.channel.id); 7 | return Promise.resolve(); 8 | } else if (ctx.gcfg.trivia != 0 && ctx.channel.id != ctx.gcfg.trivia) { 9 | return ctx.send(ctx.strings.get("trivia_wrong_channel", ctx.gcfg.trivia)); 10 | } else { 11 | return ctx.send(ctx.strings.get("trivia_no_channel", ctx.gcfg.prefix)); 12 | } 13 | 14 | return Promise.resolve(); 15 | }, 16 | stop: async function(ctx) { 17 | if (ctx.client.trivia.channels.includes(ctx.channel.id)) { 18 | ctx.client.trivia.channels.splice(ctx.client.trivia.channels.indexOf(ctx.gcfg.trivia), 1); 19 | return ctx.success(ctx.strings.get("trivia_stopped")); 20 | } 21 | }, 22 | top: async function(ctx) { 23 | let res; 24 | try { 25 | res = await ctx.client.pg.query("SELECT * FROM scores ORDER BY score DESC;"); 26 | } catch (err) { 27 | ctx.error(err); 28 | return ctx.failure(ctx.strings.get("bot_generic_error")); 29 | } 30 | 31 | let msg = []; 32 | let rows = res.rows; 33 | if (ctx.options[0] == "all") { 34 | msg.push(ctx.strings.get("trivia_top_ten_bot")); 35 | } else { 36 | msg.push(ctx.strings.get("trivia_top_ten_server")); 37 | rows = rows.filter((row) => ctx.guild.members.get(row.id)); 38 | } 39 | 40 | rows = rows.slice(0, 10); 41 | 42 | msg = msg.concat(rows.map((row) => { 43 | let user = ctx.client.users.find((user) => user.id == row.id); 44 | return `${user ? user.username : ctx.strings.get("trivia_unkown_user")}: ${row.score}`; 45 | })); 46 | 47 | return ctx.send(msg.join("\n")); 48 | }, 49 | stats: async function(ctx) { 50 | let res; 51 | try { 52 | res = await ctx.client.pg.query("SELECT * FROM scores ORDER BY score DESC LIMIT 1;") 53 | } catch (err) { 54 | ctx.error(err); 55 | return ctx.failure(ctx.strings.get("bot_generic_error")); 56 | } 57 | 58 | let user = ctx.client.users.get(res.rows[0].id); 59 | 60 | return ctx.send([ 61 | `**${ctx.strings.get("trivia_total_questions")}** ${ctx.client.trivia.questions.length}`, 62 | `**${ctx.strings.get("trivia_first_place")}** ${user ? user.username : ctx.strings.get("trivia_unkown_user")} - ${res.rows[0].score}`, 63 | `**${ctx.strings.get("trivia_concurrent_games")}** ${ctx.client.trivia.channels.length}` 64 | ].join("\n")); 65 | }, 66 | points: async function(ctx) { 67 | let ID = ctx.author.id; 68 | 69 | if (ctx.options.length) { 70 | let result = await searchMembers(ctx.guild.members, ctx.options); 71 | if (result) ID = result.all[0]; 72 | } 73 | 74 | let res; 75 | try { 76 | res = await ctx.client.pg.query("SELECT * FROM scores ORDER BY score DESC;"); 77 | } catch (err) { 78 | ctx.error(err); 79 | return ctx.failure(ctx.strings.get("bot_generic_error")); 80 | } 81 | 82 | let user = ctx.client.users.get(ID); 83 | let row = res.rows.find((row) => row.id == ID); 84 | let guild = res.rows.filter((row) => ctx.guild.members.get(row.id)); 85 | 86 | if (!row) { 87 | return ctx.send(ctx.strings.get("trivia_has_not_played", user.username)); 88 | } 89 | 90 | let embed = { 91 | author: { 92 | name: user.username, 93 | icon_url: user.avatarURL 94 | }, 95 | description: [ 96 | `**${ctx.strings.get("trivia_points")}** ${row.score}`, 97 | `**${ctx.strings.get("trivia_highest_streak")}** ${row.streak}`, 98 | `**${ctx.strings.get("trivia_server_rank")}** ${guild.indexOf(row) + 1}/${guild.length}`, 99 | `**${ctx.strings.get("trivia_global_rank")}** ${res.rows.indexOf(row) + 1}/${res.rows.length}` 100 | ].join("\n") 101 | } 102 | 103 | return ctx.embed(embed); 104 | } 105 | } 106 | 107 | async function exec(ctx) { 108 | const subcommand = ctx.options[0]; 109 | 110 | if (subcommands.hasOwnProperty(subcommand)) { 111 | ctx.options = ctx.options.slice(1); 112 | return subcommands[subcommand](ctx); 113 | } else { 114 | return ctx.send(ctx.strings.get("bot_available_subcommands", Object.keys(subcommands).map((cmd) => `\`${cmd}\``).join(", "))); 115 | } 116 | } 117 | 118 | module.exports = { 119 | name: "trivia", 120 | category: "fun", 121 | exec 122 | }; 123 | -------------------------------------------------------------------------------- /embeds/match.js: -------------------------------------------------------------------------------- 1 | const checkDotaID = require("../util/checkDotaID"); 2 | const aliases = require("../json/aliases.json") 3 | const pad = require("pad"); 4 | 5 | const game_modes = [ 6 | "Unknown", 7 | "All Pick", 8 | "Captain's Mode", 9 | "Random Draft", 10 | "Single Draft", 11 | "All Random", 12 | "Intro", // ??? 13 | "Diretide", 14 | "Reverse Captain's Mode", 15 | "Greevling", 16 | "Tutorial", // k 17 | "Mid Only", 18 | "Least Played", 19 | "Limited Heroes", 20 | "Compendium Ranked", 21 | "Custom", 22 | "Captain's Draft", 23 | "Balanced Draft", 24 | "Ability Draft", 25 | "Event", 26 | "ARDM", 27 | "1v1 Mid", 28 | "All Pick", // ranked all pick 29 | "Turbo" 30 | ]; 31 | 32 | const lobby_types = [ 33 | "Normal", 34 | "Practice", 35 | "Tournament", 36 | "Tutorial", 37 | "Co-op Bots", 38 | "Team Ranked", 39 | "Solo Ranked", 40 | "Ranked", 41 | "1v1 Mid", 42 | "Battle Cup" 43 | ]; 44 | 45 | const skills = [ 46 | "Unknown", 47 | "Normal", 48 | "High", 49 | "Very High" 50 | ]; 51 | 52 | async function matchEmbed(ctx, match_data) { 53 | let queries = []; 54 | 55 | match_data.players.forEach((player) => { 56 | queries.push(checkDotaID(ctx.client.pg, player.account_id)); 57 | }); 58 | 59 | let results = await Promise.all(queries); 60 | 61 | match_data.players.forEach((player, index) => { 62 | if (results[index] !== null && ctx.guild.members.get(results[index])) { 63 | match_data.players[index].mention_str = `<@${results[index]}>`; 64 | } 65 | }); 66 | 67 | let heading = ["L.", "Hero", "K/D/A", "LH/D", "HD", "TD", "GPM", "XPM", "\u200b"]; 68 | let table = []; 69 | let ftable = []; 70 | let highest = new Array(9).fill(0); 71 | 72 | match_data.players.forEach(player => { 73 | let row = [ 74 | player.level.toString(), 75 | aliases.find((hero) => hero.id == player.hero_id).local, 76 | `${player.kills}/${player.deaths}/${player.assists}`, 77 | `${player.last_hits}/${player.denies}`, 78 | player.hero_damage < 1000 ? player.hero_damage.toString() : `${(player.hero_damage / 1000).toFixed(1)}k`, 79 | player.tower_damage < 1000 ? player.tower_damage.toString() : `${(player.tower_damage / 1000).toFixed(1)}k`, 80 | player.gold_per_min.toString(), 81 | player.xp_per_min.toString(), 82 | (player.mention_str || player.personaname || "Unknown").replace(/`/g, "'") 83 | ]; 84 | 85 | for (let val in row) { 86 | if (highest[val] < row[val].length) { 87 | highest[val] = row[val].length; 88 | } 89 | } 90 | 91 | table.push(row); 92 | }); 93 | 94 | table.splice(5, 0, heading); 95 | table.splice(0, 0, heading); 96 | 97 | table.forEach(row => { 98 | for (let item in row) { 99 | if (item != row.length - 1) { 100 | row[item] = pad(row[item], highest[item], " "); 101 | }; 102 | } 103 | ftable.push(`\`${row.slice(0, row.length - 1).join(" ")}\`  ${row[row.length - 1]}`); 104 | }); 105 | 106 | let victory = match_data.radiant_win ? ctx.strings.get("matchinfo_match_radiant_victory") : ctx.strings.get("matchinfo_match_dire_victory"); 107 | let ptime = `${Math.floor(match_data.duration / 60)}:${("00" + match_data.duration % 60).substr(-2, 2)}`; 108 | let skill = match_data.skill ? skills[match_data.skill] : skills[0]; 109 | 110 | let od_link = `https://www.opendota.com/matches/${match_data.match_id}`; 111 | let db_link = `https://www.dotabuff.com/matches/${match_data.match_id}`; 112 | let stratz_link = `https://stratz.com/match/${match_data.match_id}`; 113 | 114 | return { 115 | "title": victory, 116 | "footer": { 117 | "text": ctx.strings.get("matchinfo_match_completed") 118 | }, 119 | "timestamp": new Date((match_data.start_time + match_data.duration) * 1000), 120 | "fields": [{ 121 | "name": `${match_data.radiant_score} - ${match_data.dire_score}, ${ptime}`, 122 | "value": match_data.match_id, 123 | "inline": true 124 | }, { 125 | "name": lobby_types[match_data.lobby_type] || "Unknown Lobby", 126 | "value": game_modes[match_data.game_mode] || "Unknown Game Mode", 127 | "inline": true 128 | }, { 129 | "name": `${skill} Skill`, 130 | "value": `[OD](${od_link}) / [DB](${db_link}) / [STRATZ](${stratz_link})`, 131 | "inline": true 132 | }, { 133 | "name": "Radiant", 134 | "value": ftable.slice(0, 6).join("\n"), 135 | "inline": false 136 | }, { 137 | "name": "Dire", 138 | "value": ftable.slice(6, 12).join("\n"), 139 | "inline": false 140 | }] 141 | }; 142 | } 143 | 144 | module.exports = matchEmbed; 145 | -------------------------------------------------------------------------------- /commands/personal/twenty.js: -------------------------------------------------------------------------------- 1 | const aliases = require("../../json/aliases.json"); 2 | const searchMembers = require("../../util/searchMembers"); 3 | const checkDiscordID = require("../../util/checkDiscordID"); 4 | 5 | let keys = ["kills", "deaths", "assists", "xp_per_min", "gold_per_min", "hero_damage", "tower_damage", "hero_healing", "last_hits", "duration"]; 6 | 7 | function fmtK(num) { 8 | return num < 1000 ? num : `${(num / 1000).toFixed(1)}k`; 9 | } 10 | 11 | function fmtTime(num) { 12 | return `${Math.floor(num / 60)}:${("00" + num % 60).substr(-2, 2)}`; 13 | } 14 | 15 | class Counter { 16 | constructor(name) { 17 | this.name = name; 18 | this.data = []; 19 | this._max = { 20 | value: 0, 21 | match: 0, 22 | hero: 0 23 | }; 24 | } 25 | 26 | add(value, match, hero) { 27 | this.data.push(value); 28 | if (this.data.sort((a, b) => b - a)[0] <= value) { 29 | this._max.value = value; 30 | this._max.match = match; 31 | this._max.hero = hero; 32 | } 33 | } 34 | 35 | avg() { 36 | let total = this.data.reduce((total, next) => total + next); 37 | return Math.floor(total / this.data.length); 38 | } 39 | 40 | max() { 41 | return { 42 | value: this._max.value, 43 | match: this._max.match, 44 | hero: `<:${this._max.hero}:${aliases.find((a) => a.id == this._max.hero).emoji}>` 45 | } 46 | } 47 | } 48 | 49 | async function exec(ctx) { 50 | let member, ID; 51 | try { 52 | let members; 53 | if (ctx.options.length) { 54 | members = await searchMembers(ctx.guild.members, ctx.options); 55 | if (!members.found) return ctx.failure(ctx.strings.get("bot_no_member")); 56 | } else { 57 | members = false; 58 | } 59 | 60 | ID = members ? members.all[0] : ctx.author.id; 61 | member = await checkDiscordID(ctx.client.pg, ID); 62 | 63 | if (!member) { 64 | return ctx.failure(ctx.strings.get("bot_not_registered", ctx.guild.members.get(ID).username, ctx.gcfg.prefix)); 65 | } 66 | } catch (err) { 67 | ctx.error(err); 68 | return ctx.failure(ctx.strings.get("bot_generic_error")); 69 | } 70 | 71 | try { 72 | let data = await ctx.client.mika.getPlayerRecentMatches(member); 73 | 74 | let groomed = {}; 75 | 76 | for (let key of keys) { 77 | groomed[key] = new Counter(key); 78 | 79 | for (let match of data) { 80 | groomed[key].add(match[key], match.match_id, match.hero_id); 81 | } 82 | } 83 | 84 | let wins = data.map((match) => match.player_slot < 6 == match.radiant_win).filter((result) => result).length; 85 | let winrate = Math.round(wins / data.length * 10000) / 100; 86 | 87 | let msg = [ 88 | `**Summaries for ${ctx.client.users.get(ID).username}'s last ${data.length} matches**`, 89 | // "Name: **Average** (*maximum as hero in* `match ID`)", 90 | "", 91 | `${ctx.strings.get("history_as_winrate")}: **${winrate}%**`, 92 | `${"K"}: **${groomed.kills.avg()}** (*${groomed.kills.max().value} as ${groomed.kills.max().hero} in* \`${groomed.kills.max().match}\`)`, 93 | `${"D"}: **${groomed.deaths.avg()}** (*${groomed.deaths.max().value} as ${groomed.deaths.max().hero} in* \`${groomed.deaths.max().match}\`)`, 94 | `${"A"}: **${groomed.assists.avg()}** (*${groomed.assists.max().value} as ${groomed.assists.max().hero} in* \`${groomed.assists.max().match}\`)`, 95 | `${"XPM"}: **${groomed.xp_per_min.avg()}** (*${groomed.xp_per_min.max().value} as ${groomed.xp_per_min.max().hero} in* \`${groomed.xp_per_min.max().match}\`)`, 96 | `${"GPM"}: **${groomed.gold_per_min.avg()}** (*${groomed.gold_per_min.max().value} as ${groomed.gold_per_min.max().hero} in* \`${groomed.gold_per_min.max().match}\`)`, 97 | `${"HD"}: **${fmtK(groomed.hero_damage.avg())}** (*${fmtK(groomed.hero_damage.max().value)} as ${groomed.hero_damage.max().hero} in* \`${groomed.hero_damage.max().match}\`)`, 98 | `${"TD"}: **${fmtK(groomed.tower_damage.avg())}** (*${fmtK(groomed.tower_damage.max().value)} as ${groomed.tower_damage.max().hero} in* \`${groomed.tower_damage.max().match}\`)`, 99 | `${"HH"}: **${fmtK(groomed.hero_healing.avg())}** (*${fmtK(groomed.hero_healing.max().value)} as ${groomed.hero_healing.max().hero} in* \`${groomed.hero_healing.max().match}\`)`, 100 | `${"LH"}: **${groomed.last_hits.avg()}** (*${groomed.last_hits.max().value} as ${groomed.last_hits.max().hero} in* \`${groomed.last_hits.max().match}\`)`, 101 | `${"D"}: **${fmtTime(groomed.duration.avg())}** (*${fmtTime(groomed.duration.max().value)} as ${groomed.duration.max().hero} in* \`${groomed.duration.max().match}\`)` 102 | ]; 103 | 104 | return ctx.send(msg.join("\n")); 105 | } catch (err) { 106 | ctx.error(err); 107 | return ctx.failure(ctx.strings.get("bot_mika_error")); 108 | } 109 | } 110 | 111 | module.exports = { 112 | name: "twenty", 113 | category: "personal", 114 | typing: true, 115 | exec 116 | } 117 | -------------------------------------------------------------------------------- /commands/esports/sub.js: -------------------------------------------------------------------------------- 1 | const checkDiscordID = require("../../util/checkDiscordID"); 2 | 3 | const newsfeeds = ["belvedere", "wykrhm", "blog", "steamnews", "magesunite"]; 4 | const newsmap = newsfeeds.map((type) => `\`${type}\``).join(", "); 5 | 6 | async function query(pg, values) { 7 | return pg.query({ 8 | text: [ 9 | "INSERT INTO public.subs (owner, channel, type, value) VALUES ($1, $2, $3, $4)", 10 | "ON CONFLICT DO NOTHING;" 11 | ].join(" "), 12 | values: values 13 | }); 14 | } 15 | 16 | const subcommands = { 17 | player: async function(ctx, type, item) { 18 | if (ctx.message.mentions.length != 1) { 19 | return ctx.failure(ctx.strings.get("sub_invalid_syntax")); 20 | } 21 | 22 | let dotaID; 23 | try { 24 | dotaID = await checkDiscordID(ctx.client.pg, ctx.message.mentions[0].id); 25 | } catch (err) { 26 | ctx.error(err); 27 | return ctx.failure(ctx.strings.get("sub_failure")); 28 | } 29 | 30 | if (!dotaID) { 31 | return ctx.failure(ctx.strings.get("bot_not_registered", ctx.message.mentions[0].username, ctx.gcfg.prefix)); 32 | } 33 | 34 | try { 35 | let res = await query(ctx.client.pg, [ctx.author.id, ctx.channel.id, "player", dotaID]); 36 | 37 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 38 | "action": "add", 39 | "type": "player", 40 | "ids": dotaID 41 | })); 42 | 43 | return ctx.success(ctx.strings.get("sub_success")); 44 | } catch (err) { 45 | ctx.error(err); 46 | return ctx.failure(ctx.strings.get("sub_failure")); 47 | } 48 | }, 49 | team: async function(ctx, type, item) { 50 | if (!item || isNaN(item)) { 51 | return ctx.failure(ctx.strings.get("sub_team_id")); 52 | } 53 | 54 | try { 55 | let res = await query(ctx.client.pg, [ctx.author.id, ctx.channel.id, type, item]); 56 | 57 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 58 | "action": "add", 59 | "type": type, 60 | "ids": item 61 | })); 62 | 63 | return ctx.success(ctx.strings.get("sub_success")); 64 | } catch (err) { 65 | ctx.error(err); 66 | return ctx.failure(ctx.strings.get("sub_failure")); 67 | } 68 | }, 69 | league: async function(ctx, type, item) { 70 | if (!item || isNaN(item)) { 71 | return ctx.failure(ctx.strings.get("sub_league_id")); 72 | } 73 | 74 | try { 75 | let res = await query(ctx.client.pg, [ctx.author.id, ctx.channel.id, type, item]); 76 | 77 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 78 | "action": "add", 79 | "type": type, 80 | "ids": item 81 | })); 82 | 83 | return ctx.success(ctx.strings.get("sub_success")); 84 | } catch (err) { 85 | ctx.error(err); 86 | return ctx.failure(ctx.strings.get("sub_failure")); 87 | } 88 | }, 89 | newsfeed: async function(ctx, type, item) { 90 | if (!newsfeeds.includes(item)) { 91 | return ctx.failure(ctx.strings.get("sub_wrong_newsfeed", newsmap)); 92 | } else { 93 | try { 94 | let res = await query(ctx.client.pg, [ctx.author.id, ctx.channel.id, type, item]); 95 | 96 | return ctx.success(ctx.strings.get("sub_success")); 97 | } catch (err) { 98 | ctx.error(err); 99 | return ctx.failure(ctx.strings.get("sub_failure")); 100 | } 101 | } 102 | }, 103 | stacks: async function(ctx, type) { 104 | try { 105 | await ctx.client.pg.query({ 106 | text: "DELETE FROM subs WHERE owner = $1;", 107 | values: [ctx.guild.id] 108 | }); 109 | 110 | let users = await ctx.client.pg.query("SELECT * FROM public.users;"); 111 | users = users.rows.filter((user) => ctx.guild.members.get(user.id)); 112 | users.push({ dotaid: 1 }); 113 | 114 | let promises = users.map((user) => query(ctx.client.pg, [ctx.guild.id, ctx.channel.id, "player", user.dotaid])); 115 | let results = await Promise.all(promises); 116 | 117 | ctx.client.redis.publish("listen:matches:new", JSON.stringify({ 118 | action: "refresh" 119 | })); 120 | 121 | return ctx.success(ctx.strings.get("sub_success")); 122 | } catch (err) { 123 | ctx.error(err); 124 | return ctx.failure(ctx.strings.get("sub_failure")); 125 | } 126 | } 127 | } 128 | 129 | const map = Object.keys(subcommands).map((type) => `\`${type}\``).join(", "); 130 | 131 | async function checks(client, member) { 132 | return member.permission.has("manageMessages"); 133 | } 134 | 135 | async function exec(ctx) { 136 | const type = ctx.options[0]; 137 | 138 | if (!type) { 139 | return ctx.failure(ctx.strings.get("sub_no_type", map)); 140 | } 141 | 142 | if (subcommands.hasOwnProperty(type)) { 143 | const item = ctx.options[1]; 144 | return subcommands[type](ctx, type, item); 145 | } else { 146 | return ctx.failure(ctx.strings.get("sub_wrong_type", map)); 147 | } 148 | } 149 | 150 | module.exports = { 151 | name: "sub", 152 | category: "esports", 153 | checks, 154 | exec 155 | }; 156 | -------------------------------------------------------------------------------- /util/genQuestions.js: -------------------------------------------------------------------------------- 1 | const dc = require("dotaconstants"); 2 | const aliases = require("../json/aliases.json"); 3 | const heroes = require("../json/heroes"); 4 | 5 | const fullnames = { 6 | "str": "Strength", 7 | "agi": "Agility", 8 | "int": "Intelligence" 9 | }; 10 | 11 | function clean(str) { 12 | return str.toString().toLowerCase().trim(); // .replace(/[+\-%s\.]/g, ""); 13 | } 14 | 15 | function formatAttribute(name, attribute, index, category) { 16 | let q = []; 17 | if (name) q.push(`${name}:`); 18 | if (index) q.push(`Level ${parseInt(index) + 1}`); 19 | attribute.header && q.push(attribute.header); 20 | attribute.footer && q.push(attribute.footer); 21 | q = q.filter((a) => a.length).join(" "); 22 | if (!q.includes(":")) q += ":"; 23 | 24 | return { 25 | question: q, 26 | answer: clean(index ? attribute.value[index] : attribute.value), 27 | category: category 28 | }; 29 | } 30 | 31 | function formatTalent(hero, dname) { 32 | res = dname 33 | .split(" ") 34 | .map((item, index, array) => { 35 | if (!isNaN(clean(item))) { 36 | let q = []; 37 | 38 | q.push(`Talents (${hero}):`); 39 | 40 | let header = array.slice(0, index); 41 | if (header.length) { 42 | q.push(...header); 43 | } 44 | 45 | q.push(item.replace(/\d/g, "•")); 46 | 47 | let footer = array.slice(index + 1); 48 | if (footer.length) { 49 | q.push(...footer); 50 | } 51 | 52 | q = q.filter((a) => a.length).join(" "); 53 | 54 | return { 55 | "question": q, 56 | "answer": clean(item), 57 | "category": "talents" 58 | }; 59 | } 60 | }) 61 | .filter((a) => a); 62 | 63 | return res; 64 | } 65 | 66 | let questions = []; 67 | 68 | for (hero_name in dc.hero_abilities) { 69 | let hero = dc.hero_abilities[hero_name]; 70 | let ahero = aliases.find((alias) => `npc_dota_hero_${alias.name}` == hero_name); 71 | 72 | for (ability_name of hero.abilities) { 73 | if (ability_name == "ogre_magi_multicast") continue; 74 | 75 | let ability = dc.abilities[ability_name]; 76 | 77 | if (ability.dmg_type) { 78 | questions.push({ 79 | question: `Damage Type: ${ability.dname}?`, 80 | answer: ability.dmg_type.toLowerCase(), 81 | category: "abilities_stats" 82 | }); 83 | } 84 | 85 | if (ability.bkbpierce) { 86 | questions.push({ 87 | question: `y/n: ${ability.dname} pierces BKB`, 88 | answer: ability.bkbpierce.charAt(0).toLowerCase(), 89 | category: "abilities_stats" 90 | }); 91 | } 92 | 93 | if (ability.attrib) { 94 | ability.attrib.forEach((attribute) => { 95 | if (Array.isArray(attribute.value)) { 96 | for (index in attribute.value) { 97 | questions.push(formatAttribute(ability.dname, attribute, index, "abilities_attributes")); 98 | } 99 | } else { 100 | questions.push(formatAttribute(ability.dname, attribute, false, "abilities_attributes")); 101 | } 102 | }) 103 | } 104 | } 105 | 106 | hero.talents.forEach((t) => { 107 | let talent = dc.abilities[t.name]; 108 | if (talent.dname) questions.push(...formatTalent(ahero.local, talent.dname)); 109 | }); 110 | 111 | let oldHero = aliases.find((h) => h.name == hero_name); 112 | 113 | if (oldHero) { 114 | questions.push({ 115 | question: `Names/Titles: ${oldHero.local}?`, 116 | answer: oldHero.oldname, 117 | category: "hero_names" 118 | }); 119 | questions.push({ 120 | question: `Names/Titles: ${oldHero.oldname}?`, 121 | answer: oldHero.local, 122 | category: "hero_names" 123 | }); 124 | } 125 | } 126 | 127 | for (item_name in dc.items) { 128 | if (item_name.includes("dagon")) continue; 129 | if (item_name.includes("diffusal")) continue; 130 | if (item_name.includes("recipe")) continue; 131 | if (item_name.includes("necronomicon")) continue; 132 | 133 | let item = dc.items[item_name]; 134 | 135 | if (item.cost) { 136 | questions.push({ 137 | question: `Cost: ${item.dname}?`, 138 | answer: item.cost, 139 | category: "items_stats" 140 | }); 141 | } 142 | 143 | if (item.mc) { 144 | questions.push({ 145 | question: `Mana Cost: ${item.dname}?`, 146 | answer: item.mc, 147 | category: "items_stats" 148 | }); 149 | } 150 | 151 | if (item.cd) { 152 | questions.push({ 153 | question: `Cooldown: ${item.dname}?`, 154 | answer: item.cd, 155 | category: "items_stats" 156 | }); 157 | } 158 | 159 | if (item.attrib) { 160 | item.attrib.forEach((attribute) => { 161 | questions.push(formatAttribute(item.dname, attribute, false, "items_attributes")) 162 | }); 163 | } 164 | 165 | if (item.created) { 166 | let list = item.components.slice(); 167 | 168 | if (Object.keys(dc.items).includes(`recipe_${item_name}`)) { 169 | list.push(`recipe_${item_name}`); 170 | } 171 | 172 | list = list.filter((name) => dc.items[name] && dc.items[name].dname); 173 | 174 | list.forEach((iteme, index, array) => { 175 | if (!dc.items[iteme]) return; 176 | 177 | let without = array.slice(); 178 | without = without.map((name) => dc.items[name].dname); 179 | without.splice(index, 1, dc.items[iteme].dname.replace(/[^ ]/g, "•")); 180 | 181 | questions.push({ 182 | question: `${item.dname} is built from ${without.join(", ")}`, 183 | answer: dc.items[iteme].dname, 184 | category: "items_created" 185 | }); 186 | }) 187 | } 188 | } 189 | 190 | questions = questions 191 | .filter((q) => q.answer) 192 | .filter((q) => !q.question.includes("undefined")); 193 | 194 | module.exports = questions; 195 | -------------------------------------------------------------------------------- /commands/personal/history.js: -------------------------------------------------------------------------------- 1 | const eat = require("../../util/eat"); 2 | const checkDiscordID = require("../../util/checkDiscordID"); 3 | const findHero = require("../../util/findHero"); 4 | const historyWithEmbed = require("../../embeds/historyWith"); 5 | 6 | function findPlayerTeam(match, account_id) { 7 | let slot = -1; 8 | 9 | for (let hero in match.heroes) { 10 | if (match.heroes[hero].account_id == account_id) slot = hero; 11 | } 12 | 13 | return slot < 5; 14 | } 15 | 16 | async function historyWith(ctx, _with, _of, _in, _last) { 17 | if (!_with.found) { 18 | return ctx.failure(ctx.strings.get("bot_no_member")); 19 | } 20 | 21 | if (_with.all.length !== 1) { 22 | return ctx.failure(ctx.strings.get("history_with_wrong_data")); 23 | } 24 | 25 | if (parseInt(_last) == NaN) { 26 | _last = 0; 27 | } 28 | 29 | _with.all.push(_of || ctx.author.id); 30 | 31 | let results; 32 | try { 33 | results = await Promise.all(_with.all.map((id) => checkDiscordID(ctx.client.pg, id))); 34 | } catch (err) { 35 | ctx.error(err); 36 | return ctx.failure(ctx.strings.get("bot_generic_error")); 37 | } 38 | 39 | let nullcheck = results.indexOf(null); 40 | if (~nullcheck) { 41 | return ctx.failure(ctx.strings.get("bot_not_registered", ctx.client.users.get(_with.all[nullcheck]).username, ctx.gcfg.prefix)); 42 | } 43 | 44 | let constraints = { 45 | limit: _last, 46 | }; 47 | 48 | if (results.length == 2) { 49 | constraints.included_account_id = results[1]; 50 | } 51 | 52 | if (_in) { 53 | if (_in == "ranked") { 54 | constraints.lobby_type = 7; 55 | } else if (_in == "normal") { 56 | constraints.lobby_type = 0; 57 | } 58 | } 59 | 60 | let matches; 61 | try { 62 | matches = await ctx.client.mika.getPlayerMatches(results[0], constraints); 63 | } catch (err) { 64 | ctx.error(err); 65 | return ctx.failure(ctx.strings.get("bot_mika_error")); 66 | } 67 | 68 | 69 | let data = { 70 | p1: results[0], 71 | p1_name: ctx.guild.members.get(_with.all[0]).username, 72 | p2: results[1], 73 | p2_name: ctx.guild.members.get(_with.all[1]).username, 74 | total: matches.length, 75 | with: 0, 76 | winwith: 0 77 | }; 78 | 79 | if (results.length > 1) { 80 | data.against = {}; 81 | data.against[data.p1] = 0; 82 | data.against[data.p2] = 0; 83 | } 84 | 85 | matches.forEach((match) => { 86 | let p1_team = findPlayerTeam(match, results[0]); 87 | let p2_team = data.against ? findPlayerTeam(match, results[1]) : p1_team; 88 | 89 | if (p1_team == p2_team) { 90 | data.with += 1; 91 | if (p1_team == match.radiant_win) { 92 | data.winwith += 1; 93 | } 94 | } else { 95 | p1_team == match.radiant_win ? data.against[results[0]] += 1 : data.against[results[1]] += 1; 96 | } 97 | }); 98 | 99 | let embed = historyWithEmbed.call(ctx.strings, data); 100 | return ctx.embed(embed); 101 | } 102 | 103 | async function historyAs(ctx, _as, _of, _in, _last) { 104 | if (!_of) _of = ctx.author.id; 105 | 106 | let hero; 107 | if (_as) { 108 | hero = findHero(_as); 109 | if (!hero) { 110 | return ctx.failure(ctx.strings.get("bot_no_hero_error")); 111 | } 112 | } 113 | 114 | let result; 115 | try { 116 | result = await checkDiscordID(ctx.client.pg, _of); 117 | } catch (err) { 118 | ctx.error(err); 119 | return ctx.failure(ctx.strings.get("bot_generic_error")); 120 | } 121 | 122 | if (result === null) { 123 | return ctx.failure(ctx.strings.get("bot_not_registered", ctx.client.users.get(_of).username, ctx.gcfg.prefix)); 124 | } 125 | 126 | if (parseInt(_last) == NaN) { 127 | _last = 0; 128 | } 129 | 130 | let mikaOpts = { 131 | significant: 0, 132 | limit: _last 133 | }; 134 | 135 | if (_as) { 136 | mikaOpts.hero_id = hero.id; 137 | } 138 | 139 | if (_in) { 140 | if (_in == "ranked") { 141 | mikaOpts.lobby_type = 7; 142 | } else if (_in == "normal") { 143 | mikaOpts.lobby_type = 0; 144 | } 145 | } 146 | 147 | let matches; 148 | try { 149 | matches = await ctx.client.mika.getPlayerMatches(result, mikaOpts); 150 | } catch (err) { 151 | ctx.error(err); 152 | return ctx.failure(ctx.strings.get("bot_mika_error")); 153 | } 154 | 155 | let wins = matches.filter(match => match.radiant_win == (match.player_slot < 5)); 156 | 157 | let embed = { 158 | "description": [ 159 | `**${ctx.strings.get("history_as_wins")}:** ${wins.length}`, 160 | `**${ctx.strings.get("history_as_games")}:** ${matches.length}`, 161 | `**${ctx.strings.get("history_as_winrate")}:** ${Math.round(wins.length / matches.length * 10000) / 100}%` 162 | ].join("\n") 163 | }; 164 | 165 | if (hero) { 166 | embed.author = { 167 | "icon_url": `http://cdn.dota2.com/apps/dota2/images/heroes/${hero.name}_icon.png`, 168 | "name": `${ctx.guild.members.get(_of).username} as ${hero.local}` 169 | }; 170 | } else { 171 | embed.author = { 172 | "name": `${ctx.guild.members.get(_of).username} in ${_in}` 173 | }; 174 | } 175 | 176 | return ctx.embed(embed); 177 | } 178 | 179 | async function exec(ctx) { 180 | let response = await eat(ctx.content, { 181 | of: "member", 182 | with: "member", 183 | as: "string", 184 | in: "string", 185 | last: "string" 186 | }, ctx.guild.members); 187 | 188 | if (!Object.keys(response).length) { 189 | return ctx.failure(ctx.strings.get("bot_wrong_data")); 190 | } 191 | 192 | if (response.with) return historyWith(ctx, response.with, ((response.of && response.of.found) && ctx.client.users.get(response.of.all[0]).id), response.in, response.last); 193 | if (response.as) return historyAs(ctx, response.as, ((response.of && response.of.found) && ctx.client.users.get(response.of.all[0]).id), response.in, response.last); 194 | if (response.in) return historyAs(ctx, null, ((response.of && response.of.found) && ctx.client.users.get(response.of.all[0]).id), response.in, response.last); 195 | } 196 | 197 | module.exports = { 198 | name: "history", 199 | category: "personal", 200 | typing: true, 201 | aliases: ["h", "winrate"], 202 | exec 203 | }; 204 | -------------------------------------------------------------------------------- /classes/trivia.js: -------------------------------------------------------------------------------- 1 | class Trivia { 2 | constructor(questions, categories) { 3 | this.questions = questions; 4 | this.categories = categories; 5 | this.active_questions = {}; 6 | this.channels = []; 7 | this.hints = {}; 8 | this.hlock = {}; 9 | this.points = {}; 10 | this.streaks = {}; 11 | } 12 | 13 | clean(str) { 14 | return str.toString().toLowerCase().replace(/[+\-%s]/g, "").trim(); 15 | } 16 | 17 | notping(author) { 18 | return `**${author.username}#${author.discriminator}**`; 19 | } 20 | 21 | get_new_question(old_question, redis, channel, retries = 2) { 22 | let cat = this.categories[Math.floor(Math.random() * this.categories.length)]; 23 | let fil = this.questions.filter(question => question.category == cat); 24 | let res = fil[Math.floor(Math.random() * fil.length)]; 25 | let ret = old_question.question != "new" && old_question.question == res.question ? this.get_new_question(old_question) : res; 26 | 27 | ret.answer = ret.answer.toString(); 28 | 29 | if (redis && channel) { 30 | this.hlock[channel] = true; 31 | this.active_questions[channel] = ret; 32 | this.hints[channel] = ret.answer.replace(/[^+\-%s\. ]/g, "•"); 33 | if (ret.name) this.hints[channel] = this.hints[channel].replace(/[s]/g, "•"); 34 | 35 | redis.set(`trivia:${channel}:hint`, true, () => { 36 | redis.expire(`trivia:${channel}:hint`, 10, () => { 37 | redis.set(`trivia:${channel}:retries`, retries, () => { 38 | this.hlock[channel] = false; 39 | }); 40 | }); 41 | }); // AAAAAAAAAAAAAAAAAAAAAAAAAAAa 42 | } 43 | 44 | return ret; 45 | } 46 | 47 | init(client, channel) { 48 | this.channels.push(channel); 49 | client.createMessage(channel, "Trivia game started in this channel."); 50 | let question = this.get_new_question("new", client.redis, channel); 51 | client.createMessage(channel, `**${question.question}** (Hint: ${this.hints[channel]})`); 52 | } 53 | 54 | increment_user(client, user_id, score) { 55 | let sql = [ 56 | "INSERT INTO public.scores (id, score, streak, banned)", 57 | "VALUES ($1, $2, 1, false)", 58 | "ON CONFLICT (id) DO", 59 | "UPDATE SET score = public.scores.score + (SELECT CASE WHEN banned IS TRUE THEN 1 ELSE EXCLUDED.score END FROM public.scores WHERE id = $1)", 60 | "WHERE scores.id = $1;" 61 | ].join(" "); 62 | 63 | client.pg.query({ 64 | "text": sql, 65 | "values": [user_id, score] 66 | }).catch(err => client.helper.log("postgres", err)); 67 | } 68 | 69 | store_streak(client, user_id, streak) { 70 | client.pg.query({ 71 | "text": "UPDATE scores SET streak = $1 WHERE id = $2 AND streak <= $1;", 72 | "values": [streak, user_id] 73 | }).catch(err => client.helper.log("postgres", err)); 74 | } 75 | 76 | handle(message, client) { 77 | let question = this.active_questions[message.channel.id]; 78 | this.points[message.channel.id] = this.points[message.channel.id] || {}; 79 | this.points[message.channel.id][message.author.id] = this.points[message.channel.id][message.author.id] || 5; 80 | if (this.clean(message.content) == this.clean(question.answer)) { 81 | let new_question = this.get_new_question(question, client.redis, message.channel.id); 82 | this.increment_user(client, message.author.id, this.points[message.channel.id][message.author.id]); 83 | 84 | let streakstr = ""; 85 | 86 | if (!this.streaks[message.channel.id]) { 87 | this.streaks[message.channel.id] = { 88 | "user": message.author.id, 89 | "streak": 0 90 | }; 91 | } 92 | 93 | if (this.streaks[message.channel.id].user == message.author.id) { 94 | this.streaks[message.channel.id].streak += 1; 95 | streakstr = `${this.notping(message.author)} is on a streak of ${this.streaks[message.channel.id].streak}! `; 96 | } else { 97 | if (this.streaks[message.channel.id].streak > 2) { 98 | streakstr = `${this.notping(message.author)} broke ${this.notping(client.users.get(this.streaks[message.channel.id].user))}'s streak of ${this.streaks[message.channel.id].streak}! `; 99 | } 100 | 101 | this.store_streak(client, this.streaks[message.channel.id].user, this.streaks[message.channel.id].streak); 102 | 103 | this.streaks[message.channel.id] = { 104 | "user": message.author.id, 105 | "streak": 1 106 | }; 107 | } 108 | 109 | message.channel.createMessage(`(+${this.points[message.channel.id][message.author.id]}) ${this.notping(message.author)} is correct! The answer was **${question.answer}**. ${streakstr}New question:\n\n**${new_question.question}** (Hint: ${this.hints[message.channel.id]})`); 110 | delete this.points[message.channel.id]; 111 | } else { 112 | let pts = this.points[message.channel.id][message.author.id]; 113 | this.points[message.channel.id][message.author.id] = pts > 1 ? pts - 1 : 1; 114 | } 115 | } 116 | 117 | replace(str, orig) { 118 | if (str.split("•").length === 1) return orig; 119 | let index = Math.floor(Math.random() * str.length); 120 | if (str.charAt(index) == "•") { 121 | return `${str.substr(0, index)}${orig.charAt(index)}${str.substr(index + 1)}`; 122 | } else { 123 | return this.replace(str, orig); 124 | } 125 | } 126 | 127 | keyevent(message, client) { 128 | let split_content = message.split(":"); 129 | let channel = split_content[1], 130 | code = split_content[2]; 131 | if (!this.channels.includes(channel)) return; 132 | 133 | if (code == "hint") { 134 | if (!this.hlock[channel]) { 135 | let question = this.active_questions[channel]; 136 | this.hints[channel] = this.replace(this.hints[channel], question.answer); 137 | if (this.hints[channel].length > 10) this.hints[channel] = this.replace(this.hints[channel], question.answer); 138 | if (question.answer == this.hints[channel]) { 139 | client.redis.get(`trivia:${channel}:retries`, (err, reply) => { 140 | if (reply > 0) { 141 | let new_question = this.get_new_question(question, client.redis, channel, reply - 1); 142 | client.createMessage(channel, `Time's up! The answer was **${question.answer}**. New question:\n\n**${new_question.question}** (Hint: ${this.hints[channel]})`) 143 | .catch(err => client.helper.handle("trivia", err)); 144 | } else { 145 | this.channels.splice(this.channels.indexOf(channel), 1); 146 | client.createMessage(channel, `Time's up! The answer was **${question.answer}**. Not enough activity detected in this channel.\nUse \`--trivia start\` to start up a new game.`) 147 | .catch(err => client.helper.handle("trivia", err)); 148 | client.helper.log("trivia", `${channel}: trivia timed out`); 149 | } 150 | 151 | if (this.streaks[channel]) this.store_streak(client, this.streaks[channel].user, this.streaks[channel].streak); 152 | delete this.points[channel]; 153 | delete this.streaks[channel]; 154 | }); 155 | } else { 156 | client.redis.set(`trivia:${channel}:hint`, true); 157 | client.redis.expire(`trivia:${channel}:hint`, 10); 158 | client.createMessage(channel, `Hint: ${this.hints[channel]}`).catch(err => client.helper.handle("trivia", err)); 159 | } 160 | } else { 161 | client.helper.log("trivia", `lock jiggled in ${channel}`); 162 | } 163 | } 164 | } 165 | } 166 | 167 | module.exports = Trivia; 168 | -------------------------------------------------------------------------------- /commands/esports/live.js: -------------------------------------------------------------------------------- 1 | const dc = require("dotaconstants"); 2 | const prettyms = require("pretty-ms"); 3 | 4 | const findHero = require("../../util/findHero"); 5 | const liveMatchEmbed = require("../../embeds/liveMatch"); 6 | const talentsEmbed = require("../../embeds/talents"); 7 | 8 | async function checkMatchStatus(ctx, match) { 9 | if (!match) return ctx.strings.get("live_no_match", ctx.gcfg.prefix); 10 | if (match.completed) return ctx.strings.get("live_match_finished", ctx.gcfg.prefix, match.match_id); 11 | 12 | return false; 13 | } 14 | 15 | function formatItem(id) { 16 | let name = dc.item_ids[id]; 17 | return dc.items[name].dname; 18 | } 19 | 20 | const subcommands = { 21 | list: async function(ctx) { 22 | try { 23 | let list = await ctx.client.leagues.getList(); 24 | list = list 25 | .slice(0, 5) 26 | .map((match) => ctx.strings.get("live_list_format", match.match_id, match.radiant_team.team_name, match.dire_team.team_name, match.spectators)); 27 | 28 | list.splice(0, 0, ctx.strings.get("live_list_heading"), ""); 29 | list.push("", ctx.strings.get("live_list_footer", ctx.gcfg.prefix)); 30 | 31 | return ctx.send(list.join("\n")); 32 | } catch (err) { 33 | ctx.error(err); 34 | return ctx.failure(ctx.strings.get("bot_generic_error")); 35 | } 36 | }, 37 | watch: async function(ctx) { 38 | try { 39 | if (isNaN(ctx.options[0])) { 40 | return ctx.failure(ctx.strings.get("matches_bad_matchid")); 41 | } 42 | 43 | let match = await ctx.client.leagues.getMatch(null, ctx.options[0]); 44 | 45 | let message = await checkMatchStatus(ctx, match); 46 | if (message) return ctx.failure(message); 47 | 48 | await ctx.client.leagues.associate(ctx.channel.id, match.match_id); 49 | 50 | return this.info(ctx); 51 | } catch (err) { 52 | ctx.error(err); 53 | return ctx.failure(ctx.strings.get("bot_generic_error")); 54 | } 55 | }, 56 | info: async function(ctx) { 57 | try { 58 | let match = await ctx.client.leagues.getMatch(ctx.channel.id); 59 | 60 | let message = await checkMatchStatus(ctx, match); 61 | if (message) return ctx.failure(message); 62 | 63 | let msg = await liveMatchEmbed(match); 64 | return ctx.send(msg.content, msg.file); 65 | } catch (err) { 66 | ctx.error(err); 67 | return ctx.failure(ctx.strings.get("bot_generic_error")); 68 | } 69 | }, 70 | talents: async function(ctx) { 71 | try { 72 | let match = await ctx.client.leagues.getMatch(ctx.channel.id); 73 | 74 | let message = await checkMatchStatus(ctx, match); 75 | if (message) return ctx.failure(message); 76 | 77 | let ourHero = await findHero(ctx.options.join(" ")); 78 | if (!ourHero) return ctx.failure(ctx.strings.get("bot_no_hero_error")); 79 | 80 | let heroes = await ctx.client.leagues.getMatchHeroes(match); 81 | let theirHero = heroes.find((hero) => hero.hero_id == ourHero.id); 82 | if (!theirHero) return ctx.failure(ctx.strings.get("live_no_hero")); 83 | 84 | let embedData = {}; 85 | embedData.hero = ourHero; 86 | embedData.talents = []; 87 | 88 | let ourTalents = JSON.parse(JSON.stringify(dc.hero_abilities[`npc_dota_hero_${ourHero.name}`].talents)); 89 | let theirTalents = Object.keys(theirHero.abilities) 90 | .map((abilityID) => dc.ability_ids[abilityID]) 91 | .filter((ability) => ability.startsWith("special_bonus")); 92 | 93 | ourTalents.forEach((talent) => { 94 | if (talent.name.startsWith("special_bonus")) { 95 | talent.dname = dc.abilities[talent.name] ? dc.abilities[talent.name].dname : "?"; 96 | talent.dname = theirTalents.includes(talent.name) ? `**${talent.dname}**` : talent.dname; 97 | embedData.talents.push(talent); 98 | } 99 | }); 100 | 101 | let embed = talentsEmbed(embedData); 102 | embed = Object.assign(embed, { 103 | title: match.players.find((player) => player.account_id == theirHero.account_id).name, 104 | footer: { 105 | text: `level ${theirHero.level} as of ${prettyms(Math.floor(match.scoreboard.duration) * 1000)}` 106 | } 107 | }); 108 | 109 | return ctx.embed(embed); 110 | } catch (err) { 111 | ctx.error(err); 112 | return ctx.failure(ctx.strings.get("bot_generic_error")); 113 | } 114 | }, 115 | hero: async function(ctx) { 116 | try { 117 | let match = await ctx.client.leagues.getMatch(ctx.channel.id); 118 | 119 | let message = await checkMatchStatus(ctx, match); 120 | if (message) return ctx.failure(message); 121 | 122 | let ourHero = await findHero(ctx.options.join(" ")); 123 | if (!ourHero) return ctx.failure(ctx.strings.get("bot_no_hero_error")); 124 | ourHero = dc.heroes[ourHero.id]; 125 | 126 | let heroes = await ctx.client.leagues.getMatchHeroes(match); 127 | let theirHero = heroes.find((hero) => hero.hero_id == ourHero.id); 128 | if (!theirHero) return ctx.failure(ctx.strings.get("live_no_hero")); 129 | 130 | let theirSkills = {}; 131 | 132 | if (theirHero.abilities) { 133 | for (let item of Object.keys(theirHero.abilities)) { 134 | if (!dc.ability_ids[item].startsWith("special_bonus")) { 135 | theirSkills[dc.ability_ids[item]] = theirHero.abilities[item]; 136 | } 137 | } 138 | } 139 | 140 | let embed = { 141 | author: { 142 | name: ourHero.localized_name, 143 | url: `http://dota2.gamepedia.com/${ourHero.url}`, 144 | icon_url: `http://cdn.dota2.com${ourHero.icon}` 145 | }, 146 | fields: [{ 147 | name: "Stats", 148 | value: [ 149 | `**Player Name**: ${match.players.find((player) => player.hero_id == theirHero.hero_id).name}`, 150 | `**K/D/A:** ${theirHero.kills}/${theirHero.death}/${theirHero.assists}`, 151 | `**LH/D:** ${theirHero.last_hits}/${theirHero.denies}` 152 | ].join("\n"), 153 | inline: true 154 | }, { 155 | name: `Level ${theirHero.level}`, 156 | value: [ 157 | `**GPM/XPM:** ${theirHero.gold_per_min}/${theirHero.xp_per_min}`, 158 | `**Net Worth (Current Gold):** ${theirHero.net_worth} (${theirHero.gold})`, 159 | theirHero.respawn_timer ? `**Respawn Time:** ${theirHero.respawn_timer}` : "Currently **Alive**" 160 | ].join("\n"), 161 | inline: true, 162 | }, { 163 | name: "Skill Build", 164 | value: Object.keys(theirSkills).length 165 | ? Object.keys(theirSkills).map((skill) => `${dc.abilities[skill].dname} is level ${theirSkills[skill]}`).join("\n") 166 | : "nothing skilled", 167 | inline: true 168 | }, { 169 | name: "Item Build", 170 | value: [ 171 | Array(3).fill(0).map((item, index) => theirHero[`item${index}`] > 0 ? formatItem(theirHero[`item${index}`]) : "No Item").join(", "), 172 | Array(3).fill(0).map((item, index) => theirHero[`item${index + 3}`] > 0 ? formatItem(theirHero[`item${index + 3}`]) : "No Item").join(", "), 173 | ].join("\n"), 174 | inline: true 175 | }], 176 | footer: { 177 | text: `as of ${prettyms(Math.floor(match.scoreboard.duration) * 1000)}` 178 | } 179 | } 180 | 181 | return ctx.embed(embed); 182 | } catch (err) { 183 | ctx.error(err); 184 | return ctx.failure(ctx.strings.get("bot_generic_error")); 185 | } 186 | } 187 | }; 188 | 189 | async function exec(ctx) { 190 | let subcommand = ctx.options.splice(0, 1)[0]; 191 | 192 | if (subcommands.hasOwnProperty(subcommand)) { 193 | return subcommands[subcommand](ctx); 194 | } else { 195 | return ctx.send(ctx.strings.get("bot_available_subcommands", Object.keys(subcommands).map((cmd) => `\`${cmd}\``).join(", "))); 196 | } 197 | } 198 | 199 | module.exports = { 200 | name: "live", 201 | category: "esports", 202 | typing: true, 203 | exec 204 | }; 205 | -------------------------------------------------------------------------------- /classes/LeagueUtils.js: -------------------------------------------------------------------------------- 1 | const needle = require("needle"); 2 | const dc = require("dotaconstants"); 3 | 4 | class LeagueUtils { 5 | constructor(key) { 6 | this.key = key; 7 | this.matches = new Map(); 8 | this.channels = new Map(); 9 | } 10 | 11 | _formatAbilities(abilities) { 12 | if (Array.isArray(abilities)) { 13 | return abilities.map((ability) => { 14 | if (Array.isArray(ability.ability)) { 15 | let ret = {}; 16 | for (let item of ability.ability) { // valve 17 | ret[item.ability_id] = item.ability_level; 18 | } 19 | return ret; 20 | } else { 21 | let ret = {}; 22 | ret[ability.ability_id] = ability.ability_level; 23 | return ret; 24 | } 25 | }); 26 | } else { 27 | if (Array.isArray(abilities.ability)) { 28 | let ret = {}; 29 | for (let item of abilities.ability) { // valve 30 | ret[item.ability_id] = item.ability_level; 31 | } 32 | return [ret]; 33 | } else { 34 | let ret = {}; 35 | ret[abilities.ability.ability_id] = abilities.ability.ability_level; 36 | return [ret]; 37 | } 38 | } 39 | } 40 | 41 | _formatPlayer(player, abilities) { 42 | if (typeof player != "object") return; 43 | 44 | for (let key in player) { 45 | if (key != "account_id" && !isNaN(player[key])) { 46 | player[key] = Number(player[key]); 47 | } 48 | } 49 | 50 | if (!abilities) return player; 51 | 52 | try { 53 | for (let ability of abilities) { 54 | let name = dc.ability_ids[Object.keys(ability)[0]]; 55 | if (!name) continue; 56 | 57 | let hero = Object.values(dc.hero_abilities).find((h) => { 58 | if (h.abilities.includes(name)) return true; 59 | if (h.talents.find((talent) => talent.name == name)) return true; 60 | return false; 61 | }); 62 | if (!hero) continue; 63 | 64 | let hero_name = Object.keys(dc.hero_abilities)[Object.values(dc.hero_abilities).indexOf(hero)]; 65 | if (!hero_name) continue; 66 | 67 | let id = dc.hero_names[hero_name].id; 68 | 69 | if (player.hero_id == id) { 70 | player.abilities = ability; 71 | break; 72 | } 73 | } 74 | 75 | return player; 76 | } catch (err) { 77 | console.error(err); 78 | console.error("what the fuck"); 79 | console.error(abilities); 80 | } 81 | } 82 | 83 | _formatTeam(team) { 84 | let abil = team.abilities ? this._formatAbilities(team.abilities) : null; 85 | 86 | let nuteam = { 87 | score: Number(team.score), 88 | tower_state: Number(team.tower_state), 89 | barracks_state: Number(team.barracks_state), 90 | picks: team.picks ? Object.values(team.picks.pick).map((pick) => pick.hero_id) : [], 91 | bans: team.bans ? Object.values(team.bans.ban).map((ban) => ban.hero_id) : [], 92 | players: team.players ? Object.values(team.players.player).map((player) => this._formatPlayer(player, abil)) : [] 93 | }; 94 | 95 | return nuteam; 96 | } 97 | 98 | _formatScoreboard(scoreboard) { 99 | if (!scoreboard) { 100 | return {}; 101 | } else { 102 | let nuscoreboard = { 103 | duration: Number(scoreboard.duration), 104 | roshan_respawn_timer: Number(scoreboard.roshan_respawn_timer), 105 | radiant: this._formatTeam(scoreboard.radiant), 106 | dire: this._formatTeam(scoreboard.dire) 107 | }; 108 | 109 | return nuscoreboard; 110 | } 111 | } 112 | 113 | _formatGame(game) { 114 | let nugame = { 115 | players: game.players.player, 116 | radiant_team: game.radiant_team || { 117 | team_name: "Unknown Team", 118 | team_id: 0, 119 | team_logo: 0, 120 | complete: false 121 | }, 122 | dire_team: game.dire_team || { 123 | team_name: "Unknown Team", 124 | team_id: 0, 125 | team_logo: 0, 126 | complete: false 127 | }, 128 | lobby_id: game.lobby_id, 129 | match_id: game.match_id, 130 | spectators: Number(game.spectators), 131 | series_id: Number(game.series_id), 132 | game_number: Number(game.game_number), 133 | league_id: Number(game.league_id), 134 | stream_delay_s: Number(game.stream_delay_s), 135 | radiant_series_wins: Number(game.radiant_series_wins), 136 | dire_series_wins: Number(game.dire_series_wins), 137 | series_type: Number(game.series_type), 138 | league_series_id: Number(game.league_series_id), 139 | league_game_id: Number(game.league_game_id), 140 | stage_name: game.stage_name, 141 | league_tier: Number(game.league_tier), 142 | scoreboard: this._formatScoreboard(game.scoreboard) 143 | }; 144 | 145 | return nugame; 146 | } 147 | 148 | _getMatches(override, cb) { 149 | if (!override && this.expires > Date.now()) return cb(); 150 | 151 | let url = `http://api.steampowered.com/IDOTA2Match_570/GetLiveLeagueGames/v1/?key=${this.key}&format=xml`; 152 | needle.get(url, (err, response, body) => { 153 | if (err) { 154 | return cb(err); 155 | } else { 156 | try { 157 | if (body.result.status !== "200") return cb(new Error(body.result.status)); 158 | let matches = body.result.games.game.map((game) => this._formatGame(game)); 159 | 160 | let oldMatches = Array.from(this.matches.keys()); 161 | let toDelete = oldMatches.filter((matchID) => matches.indexOf(matchID) == -1); 162 | 163 | toDelete.forEach((item) => { 164 | this.matches.set(item, { 165 | match_id: item, 166 | completed: true 167 | }); 168 | }); 169 | 170 | for (let match of matches) { 171 | this.matches.set(match.match_id, match); 172 | } 173 | 174 | this.expires = Date.now() + 60000; 175 | 176 | return cb(); 177 | } catch (err) { 178 | return cb(err); 179 | } 180 | } 181 | }); 182 | } 183 | 184 | getAllData() { 185 | return new Promise((resolve, reject) => { 186 | this._getMatches(true, (err) => { 187 | if (err) return reject(err); 188 | 189 | return resolve(); 190 | }); 191 | }); 192 | } 193 | 194 | getList() { 195 | return new Promise((resolve, reject) => { 196 | this._getMatches(false, (err) => { 197 | if (err) return reject(err); 198 | 199 | let list = Array.from(this.matches.values()) 200 | .filter((match) => !match.completed) 201 | .map((match) => { 202 | return { 203 | match_id: match.match_id, 204 | radiant_team: match.radiant_team, 205 | dire_team: match.dire_team, 206 | spectators: match.spectators, 207 | league_id: match.league_id, 208 | duration: match.scoreboard && match.scoreboard.duration 209 | }; 210 | }) 211 | .sort((a, b) => b.spectators - a.spectators); 212 | 213 | return resolve(list); 214 | }); 215 | }); 216 | } 217 | 218 | getMatch(channelID, matchID) { 219 | return new Promise((resolve, reject) => { 220 | this._getMatches(false, (err) => { 221 | if (err) return reject(err); 222 | 223 | if (channelID) matchID = this.channels.get(channelID); 224 | return resolve(this.matches.get(matchID)); 225 | }); 226 | }); 227 | } 228 | 229 | associate(channelID, matchID) { 230 | return Promise.resolve(this.channels.set(channelID, matchID)); 231 | } 232 | 233 | getMatchHeroes(match) { 234 | return match.scoreboard.radiant.players.concat(match.scoreboard.dire.players); 235 | } 236 | } 237 | 238 | module.exports = LeagueUtils; 239 | -------------------------------------------------------------------------------- /json/mike.json: -------------------------------------------------------------------------------- 1 | [ 2 | "teals a full blown peruvian retard", 3 | "maybe i should wai ta week ujntil everyones done thinking theyre fy and shit", 4 | "cant uguys smkoe and KILL FUCKING YELLELWWO", 5 | "gbreys grabegr", 6 | "KAMNoi", 7 | "am i hard otg hera ow hat", 8 | "roll is a piece of fucking bread", 9 | "thast whjat io tel mseyfl", 10 | "spoielreres", 11 | "u wann aplay in ur OWN REGION", 12 | "u know if u were anyh good we woulda lost so logn ago", 13 | "im gunna enjoy breaking all ur streaks u gay ass mexicans", 14 | "theres a upgraded wand necrolyte chink in myh lane", 15 | "meepos an ultra shit hero andu will make my game harder if u pick it", 16 | "DAME EL WARD", 17 | "STUF ORNAGE", 18 | "fucking ODDGER BITCGH", 19 | "tidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetidetide", 20 | "god damn apes", 21 | "halwf my kleys d]\\on]\\tn w,]\\wrkl]\\", 22 | "i7ve g]\\o]\\ tkleyb]\\o]\\rd i7ssuies]\\", 23 | "wahgt ehg hell", 24 | "cancer people get muted", 25 | "L,APO", 26 | "UR SO BAD ORNAG", 27 | "Fuck yo moon", 28 | "I don't got or correct player cards that's poverty shit", 29 | "blues FUCKING YAO GANK HIM", 30 | "normnally i root against maericans but im rooting foru.", 31 | "u need to start taking this event seriously ppd....", 32 | "erw ;post", 33 | "l;et sorsh", 34 | "teweet a tme agian motherf ufkcer", 35 | "@GranDGranT oh fuck off", 36 | "europeans are gay as hell", 37 | "FIREFLY IS FUCKING BUGGED AND SHITTY", 38 | "im streetwise as shit", 39 | "im not losing to fucking mexicans", 40 | "this heroes so suropiean", 41 | "get rich or die trying", 42 | "browns fuckking mexican", 43 | "blkue d0ont knwoj what the fuck hes odin", 44 | "WHO THE FUKCS DG", 45 | "casters are saying i fu sucked less ass dg ur team woulda won 20 minutes ago", 46 | "rmemeber uapused tiwcwe", 47 | "that dbe a rax if teal was playign dota", 48 | "imf ucuking tweetuibg at valve", 49 | "oimt eh shit", 50 | "holy sjhit yellows not teal", 51 | "cNA U STFU MAUT", 52 | "only one of u guys had ot be good to win this game", 53 | "ther a fucking trina outside hjwp anoynig", 54 | "puynch urself u uncept cunt", 55 | "oh my god ive killed lbs will ot play dota", 56 | "ur ugnna mak eme lose to ryuuboruz", 57 | "dont feed couriers u dumb cocksuckers", 58 | "it hoguht ifu ucekd hta tup", 59 | "MNALMop", 60 | "kjpoimn tje ficlomg ga,e are ui real life", 61 | "brown u stupid cocksucker", 62 | "is cheese a vegetbale", 63 | "send kme hope u ueslss asshole", 64 | "no noe fucmkina rpociatrse m", 65 | "RIP", 66 | "u gusy fucking ,lost hwen i reive", 67 | "chinga tu madre", 68 | "go fuck urself and ur motehr", 69 | "wat teh fucskohoihngon", 70 | "fucking unprofessional mother fuckers", 71 | "lamo orabge u useless motjher fucker u", 72 | "smnoieskd", 73 | "shut eh fuck up u god dman momkney", 74 | "whatthe cukcs ogign", 75 | "hwger eth efuck am i gunna fwerm 300 0gold", 76 | "every day above grounds a good day u ungrateful bastard", 77 | "oincomeptme trfucking mother fuckers", 78 | "CNA TU DOPR THE SENTRY THAT SIN UR INVENTORY", 79 | "wjatys hits lane ven eod", 80 | "whjos fauktls that im goijgn right back they sued l ike 3 buyouts", 81 | "ur not getting chummy iwht me mother fucker", 82 | "kamnio", 83 | "slkeep down uf uckking tool", 84 | "change ur steam name by the end of the drafting phas eor im banning u forever", 85 | "stop being no fucking english", 86 | "even the viewers know uf ucked up jsut stuf ockscucsker", 87 | "my internesfr uckign rbeoine", 88 | "can my rgamdohte rget the fuck o0ff ht eponhoemn", 89 | "thberyes wapPING", 90 | "rubvick aghreg", 91 | "nos elepe", 92 | "KLANI]", 93 | "im a big deal", 94 | "look at ur name u drooling ass mongoloid", 95 | "poh mgy soh an ac", 96 | "outpicked u droooling ass monkey", 97 | "i was guna ban ta if i didnt get food cockcuksuier", 98 | "@8FrontFlips nice glyph", 99 | "NICE GLOH", 100 | "><AMO", 101 | "ua ll lok ead", 102 | "whjos fucking dnetying itr.", 103 | "ur the greediest selfishest stupdiest player i ever seen blue", 104 | "ive yet to jack off.", 105 | "im not ur bro u god damn hoodlum", 106 | "KLAMNIO", 107 | "cui an go onverge", 108 | "how do u think im talking to u u self centered cunt", 109 | "cna u stfu teal u sux sock", 110 | "ur dead cocksucker dg", 111 | "udmbasseds", 112 | "hello welcome to my crib", 113 | "im not brown im dark white", 114 | "i thoguth u emant item sor som ehsbit", 115 | "yellows ungkabale", 116 | "cockscking cocksucker", 117 | "ateezy looks like a mob boss", 118 | "well its not mh yu fualt u got outpicekd", 119 | "lemem famr this cocksucker get eht fuck outt athe lane", 120 | "gmaelocsing ocksuckers", 121 | "thast new york u fuckkoijg monikey", 122 | "wgast everyoens rpoblem", 123 | "cat u puck up the fuckkgn gme", 124 | "ur on teh wrong og damn unit purple", 125 | "know ur role and shut ur god damn mouth", 126 | "does urple haev mikb", 127 | ";A,P", 128 | "hwo come ud ont sprotui", 129 | "hope u get cancer :P", 130 | "uve gotta be the top 10 biggest disappoitments to ever grace this game", 131 | "its forf the ufuter eoccksucker", 132 | "u sux major major moajor major major amorjoa cocks dg", 133 | "see how dadyd pick dumbasses", 134 | "LKM<PO", 135 | "ar eu tlaking to meare u tlak", 136 | "have iu treid leaving cocksuckres", 137 | "greys ungamewinnable", 138 | "LKAMO", 139 | "give me my bonuis time u stupid fukc", 140 | "all dead animals is good", 141 | "bet ubeeiv eureslf yellwo", 142 | "fucking bitch ass cokcsuerks", 143 | "hat et uf", 144 | "the whole country was littered with poorly tasting food", 145 | "yop tugys", 146 | "i got dfruknk as hell thi", 147 | "can u type ready so i can start ht egam mother fuckesrf", 148 | "hlerp my kegbaords ufkced up wheres my question ark", 149 | "LA<MP", 150 | "what he h ehl", 151 | "thsi potm ult pwjinng", 152 | "LAMPO", 153 | "hows he have the audacity to tweet when hes fuckign some lol girl oinstead of playign dota", 154 | "LAMO UVE GOT A CMOPOLEXIYT ICONM", 155 | "ur a hug eass let odwn by the way pruple", 156 | "wegio t anaix prboe,m", 157 | "how do i use your peanut butter", 158 | "i may as well jkack off for the last time this week.", 159 | "nboit me imapel", 160 | "cuz u dumb cunts are quad laniung and spamming ropckets and lfaks", 161 | "click a hero u cocksucker", 162 | "tc live sin a third world fucking country so hes got a layover", 163 | "demon is tcs cousin", 164 | "congrats dumbasses", 165 | "see blue lyijg ht0o hiumself", 166 | "ur sutpi da asll ehll", 167 | "how moc eu dind tod htat 30 jmitunes ago", 168 | "hw ocme he see ems ena di odn tse ehim", 169 | "u just posted on the fourusm defending slardfar", 170 | "cant u jsut be useful instead of saying shit like can u manfight gay ass fag", 171 | "lam,po......", 172 | "enter fucking roshan u cocksuckers", 173 | "im black", 174 | "ud be up like 3kills if ur build sucked less ass dg", 175 | "mac users can fuck off", 176 | "@MSSDota ty", 177 | "@LiquidFLUFF no prisoners", 178 | "A<PO", 179 | "leave or im waiting for 3 more roshes", 180 | "whers mys quadron", 181 | "@Dsmk12 he's fuckin ruthless", 182 | "u suck cock yellow u dont deserve to be alive", 183 | "u press blast u stupid cunt", 184 | "tehter u fcocksucker", 185 | "blkas tit", 186 | "uy cant be seirous rbwon", 187 | "nao rmor", 188 | "hows the air out there? should i bring a gas mask?", 189 | "i got a 1.5 in chem", 190 | "trees so demoarlaiziung", 191 | "gof uckurself htogh blue.", 192 | "juby dust cocksucker", 193 | "cpom,e bottom", 194 | "holy shit we ogt a weaver i havetn sene him once", 195 | "fucking timewlak levleler", 196 | "serbias not a real country", 197 | "trees a goed dman bproblem", 198 | "i only play wisp and tree mother fucker", 199 | "thjis isnt how dota owrk slbue", 200 | "i dont even think i di da nything ihts game.", 201 | "hes go tgbentdetata", 202 | "ff", 203 | "i lost my voice in a terrible automobile accident", 204 | "@ALawley it seems he uses his backspace key when typing on reddit", 205 | "i dont have many good days", 206 | "he said his friends called him fluff irl or some shit", 207 | "waht du open pwind", 208 | "thats not third child dumbass", 209 | "jopy hsit", 210 | "im so fucking ham", 211 | "ud ont even got spells what sur probvlem", 212 | "(Simon & Garfunkel - Cloudy) ~ this is the hammest song", 213 | "remmerb wehne he made tha ttl blog", 214 | "@LiquidKorok what the shit", 215 | "thats why i asked u big ass condescnding ejkrk", 216 | "do whales have feet", 217 | "gop kork it", 218 | "bey yall flele acomplsiehd i sho9uld be lvl 4", 219 | "are u a dumbass", 220 | "why msy glypho dwon", 221 | "we go ta prboem", 222 | "pinks got the reaction time of a dead fish", 223 | "lwahtt heehell", 224 | "thatys u lb uf uckign worthless cunt", 225 | "GOGOGGOGOGOGOGOGGOGOGOGOGOGOGGOGOGOGOGOGOGOGOGOGOGOGOGOGOGOGOGOGOGO", 226 | "tjanmoks", 227 | "purples a worthless ass human being", 228 | "tp u dumbass cunt", 229 | "gme thgrall", 230 | "bebarbeabraberabre", 231 | "whot hehells iwnning htis game", 232 | "nmo iot snot", 233 | "ud fuckijg lose if i got muy wards off courier", 234 | "how come hes got a fuckign bloinka jnway", 235 | "clos eneouhg", 236 | "fkc of flamo", 237 | "congrats ornage ud umbass cunt dumbass", 238 | "@LiquidKorok (picks storm) Im okrko as shit", 239 | "aghility ehjro or sotrm", 240 | "cani get a fuckjig c0nfirm", 241 | "holy shit i got a shoutout", 242 | "what a useless team", 243 | "help b;uie - they wont stop fucking denyhing", 244 | "ive got a getting dive dand triple deny with luna aura issue", 245 | "waht tgeh hellw as ur plan", 246 | "RT @LodaBerg: 3 us east games later im quite exhausted :D GG WP Liquid.", 247 | "i dont bald dumbass", 248 | "it didnt show up ontehj mionaimap i dont thikn", 249 | "thast fukcing stpouid", 250 | "i aint physically capable of spamming all his spells 24/7 - my left arm huyrts", 251 | "zombies are so fuckinbg obomnxiu", 252 | "KLAMNOI", 253 | "oh my god there are people here", 254 | "what am i allowed to eat with my hands", 255 | "he didnt giv eme ocntolr whaqt th helel", 256 | "pinks fucking his moim ors meothing if iunderstood correctly", 257 | "im such a fucking sf pleyr giuys", 258 | "u gotta invest me", 259 | "i can tsolo kill pinka nymire", 260 | "are5 55w5e55 55gu555ci", 261 | "^+W^HA^T^HE^UCKC^", 262 | "`````````````000000mypc", 263 | "whay re us o bad myf uckigbn c ogd", 264 | "i enve won thi aslna ien myh lfie", 265 | "cocky ass mofos", 266 | "courie ornbs l dg&*", 267 | "star tht eufkcing gmae", 268 | "greys like mechanically the worst player ive eve rseen", 269 | "im always down for somthin new", 270 | "@TypicalGatsby i only go to dunkin and krispy kreme", 271 | "where are the utensils", 272 | "RT @Ancalima: @shitixmikesays sklep him", 273 | "thats all ornage", 274 | "whens the last time u said gj to me blue", 275 | "@LiquidKorok everuyone thionks theyr ufkcing korok", 276 | "hwos peopel do this to myc oureir", 277 | "conrgtasty", 278 | "im not crowing or waridng - uc an fuck ruself blue", 279 | "wasnt gale userf sugys", 280 | "ah well.", 281 | "shoutout to liche", 282 | "ur sog doda mn abd", 283 | "shoutout to moon", 284 | "if muy fingers wernt greasy iu would amicored those things naywa", 285 | "brwons the wrost player ive ever seen", 286 | "ur major disgusting blue", 287 | "@Liquidixmike88 @LiquidFLUFF ?", 288 | "fluff makes me crow 6 minute ward and dont give me pulls", 289 | "abort u dumb cunt", 290 | "im too busy microing to think though", 291 | "rekected they lsot the lana talreayd", 292 | "@MonolithDota @Aui_2000 @TeamLiquidnet yikes", 293 | "geeks", 294 | "who got hcees", 295 | "stfu wiill you", 296 | "@LocalBattles negative", 297 | "?", 298 | "u guys suck ass and ur not gunna win can u jus tleave", 299 | "am i a chickenhead blue", 300 | "?", 301 | "?", 302 | "omfg my teams trying to win", 303 | "thats a result ofu doing nto-hing", 304 | "why do shit when uj can just buy a midas.", 305 | "im real real real real real sick of u.", 306 | "?purples actually getitng pwnedfi refuse to believ eit guysy", 307 | "disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt disrupt", 308 | "dujmass, you got mag you momnkey", 309 | "purple is so ass", 310 | "?", 311 | "?", 312 | "flame purple on behalf of me", 313 | "we all lost to mason\nno shame in that", 314 | "purple sa huge fag", 315 | "disband\ncircle of life", 316 | "insult purple\ntell him i called him garbage", 317 | "insult purple\ni see u reading my msgs\nfucking insult purple", 318 | "MNSUYT GME TE MATUA CKIGN GME" 319 | ] 320 | --------------------------------------------------------------------------------