summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorschneefux <schneefux+commit@schneefux.xyz>2017-12-30 13:11:06 +0100
committerschneefux <schneefux+commit@schneefux.xyz>2017-12-30 13:11:06 +0100
commitc6c5027865743328d0542246fef70e0fafe0f0f1 (patch)
tree9348d4afa900c170aa534f602b6f7a823292a831
parentabd33f47fbbcc92610b83341c61f5d1911eed590 (diff)
parent1881a2ecd48be772fd83b20068fe9946f1365eeb (diff)
downloaddiscordbot-develop.tar.gz
discordbot-develop.zip
Merge branch 'master' into developdevelop
-rw-r--r--.gitignore118
-rw-r--r--.gitmodules3
-rw-r--r--Dockerfile9
-rw-r--r--api.js294
-rw-r--r--bot.js103
-rw-r--r--commands/vainsocial/about.js37
-rw-r--r--commands/vainsocial/guild_add.js52
-rw-r--r--commands/vainsocial/guild_create.js70
-rw-r--r--commands/vainsocial/guild_member.js74
-rw-r--r--commands/vainsocial/guild_pin.js46
-rw-r--r--commands/vainsocial/guild_rm.js48
-rw-r--r--commands/vainsocial/guild_role.js46
-rw-r--r--commands/vainsocial/guild_update.js60
-rw-r--r--commands/vainsocial/guild_view.js46
-rw-r--r--commands/vainsocial/match.js68
-rw-r--r--commands/vainsocial/matches.js56
-rw-r--r--commands/vainsocial/me.js47
-rw-r--r--commands/vainsocial/user.js59
m---------joblib0
-rw-r--r--main.py193
-rw-r--r--package.json31
-rw-r--r--requirements.txt11
-rw-r--r--responses.js0
-rw-r--r--strings.js48
-rw-r--r--util.js149
-rw-r--r--views/guild.js35
-rw-r--r--views/guild_create.js49
-rw-r--r--views/guild_member.js35
-rw-r--r--views/guild_progress.js30
-rw-r--r--views/guild_update.js34
-rw-r--r--views/match.js53
-rw-r--r--views/matches.js80
-rw-r--r--views/player.js105
-rw-r--r--views/register.js46
-rw-r--r--views/simple.js24
-rw-r--r--views/view.js47
36 files changed, 1924 insertions, 282 deletions
diff --git a/.gitignore b/.gitignore
index a3401aa..6c64673 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,93 +1,61 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
+*.sqlite3
-# C extensions
-*.so
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
+# Coverage directory used by tools like istanbul
+coverage
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-.hypothesis/
+# nyc test coverage
+.nyc_output
-# Translations
-*.mo
-*.pot
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
-# Django stuff:
-*.log
-!/logs
-!run*.log
-local_settings.py
+# Bower dependency directory (https://bower.io/)
+bower_components
-# Flask stuff:
-instance/
-.webassets-cache
+# node-waf configuration
+.lock-wscript
-# Scrapy stuff:
-.scrapy
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
-# Sphinx documentation
-docs/_build/
+# Dependency directories
+node_modules/
+jspm_packages/
-# PyBuilder
-target/
+# Typescript v1 declaration files
+typings/
-# Jupyter Notebook
-.ipynb_checkpoints
+# Optional npm cache directory
+.npm
-# pyenv
-.python-version
+# Optional eslint cache
+.eslintcache
-# celery beat schedule file
-celerybeat-schedule
+# Optional REPL history
+.node_repl_history
-# dotenv
-.env
+# Output of 'npm pack'
+*.tgz
-# virtualenv
-.venv/
-venv/
-ENV/
+# Yarn Integrity file
+.yarn-integrity
-# Spyder project settings
-.spyderproject
+# dotenv environment variables file
+.env
-# Rope project settings
-.ropeproject
diff --git a/.gitmodules b/.gitmodules
index 52948d0..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "joblib"]
- path = joblib
- url = https://github.com/vainglorygame/joblib
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..326d1cf
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM node:7.7-alpine
+
+RUN mkdir -p /usr/src/app
+WORKDIR /usr/src/app
+
+COPY . /usr/src/app
+RUN npm install && npm cache clean
+
+CMD ["node", "bot.js"]
diff --git a/api.js b/api.js
new file mode 100644
index 0000000..12f58c3
--- /dev/null
+++ b/api.js
@@ -0,0 +1,294 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const request = require("request-promise"),
+ Promise = require("bluebird"),
+ WebSocket = require("ws"),
+ webstomp = require("webstomp-client"),
+ cacheManager = require("cache-manager"),
+ sleep = require("sleep-promise"),
+ Channel = require("async-csp").Channel;
+const api = module.exports;
+
+let cache = cacheManager.caching({
+ store: "memory",
+ ttl: 10 // s
+});
+
+const UPDATE_TIMEOUT = parseInt(process.env.UPDATE_TIMEOUT) || 30; // s
+
+const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.bot/bot/api",
+ API_MAP_URL = process.env.API_MAP_URL || "http://vainsocial.dev/masters/",
+ API_WS_URL = process.env.API_WS_URL || "ws://vainsocial.dev/ws",
+ API_BE_URL = process.env.API_BE_URL || "http://vainsocial.dev/bridge";
+
+const notif = webstomp.over(new WebSocket(API_WS_URL,
+ { perMessageDeflate: false }));
+
+(function connect() {
+ notif.connect("web", "web",
+ () => console.log("connected to queue"),
+ (err) => connect()
+ );
+})();
+
+module.exports.getMap = async (url) => {
+ return await request({
+ uri: API_MAP_URL + url,
+ json: true,
+ forever: true
+ });
+}
+
+module.exports.getFE = module.exports.get = async (url, params={}, ttl=60, cachekey=undefined) => {
+ if (cachekey == undefined) cachekey = url + JSON.stringify(params);
+ return await cache.wrap(cachekey, async () => await request({
+ uri: API_FE_URL + url,
+ qs: params,
+ json: true,
+ forever: true
+ }), { ttl: ttl });
+}
+
+// send a POST and optionally bust cache
+module.exports.postFE = module.exports.post = async (url, params={}, cachekey=undefined) => {
+ if (cachekey) cache.del(cachekey);
+ return await request.post(API_FE_URL + url, {
+ form: params,
+ json: true,
+ forever: true
+ });
+}
+
+// send a DELETE and optionally bust cache
+module.exports.deleteFE = module.exports.delete = async (url, params={}, cachekey=undefined) => {
+ if (cachekey) cache.del(cachekey);
+ return await request.delete(API_FE_URL + url, {
+ form: params,
+ json: true,
+ forever: true
+ });
+}
+
+// send a PUT and optionally bust cache
+module.exports.putFE = module.exports.put = async (url, params={}, cachekey=undefined) => {
+ if (cachekey) cache.del(cachekey);
+ return await request.put(API_FE_URL + url, {
+ form: params,
+ json: true,
+ forever: true
+ });
+}
+
+module.exports.postBE = module.exports.backend = async (url) => {
+ return await request.post({
+ uri: API_BE_URL + url,
+ json: true,
+ forever: true
+ });
+}
+
+function subscribe(topic, channel) {
+ return notif.subscribe("/topic/" + topic, (msg) => {
+ channel.put(msg.body);
+ msg.ack();
+ }, { ack: "client" });
+}
+
+// return id<->name mappings
+module.exports.getMappings = async () => {
+ return await cache.wrap("mappings", async () => {
+ let mapping = new Map();
+ await Promise.all([
+ Promise.map(
+ ["gamemode"], async (table) => {
+ mapping[table] = new Map();
+ (await api.getMap(table)).map(
+ (map) => mapping[table][map["id"]] = map["name"])
+ }
+ ),
+ // name <-> API name
+ Promise.map(
+ ["hero"], async (table) => {
+ mapping[table] = new Map();
+ (await api.getMap(table)).map(
+ (map) => mapping[table][map["api_name"]] = map["name"])
+ }
+ )
+ ]);
+ return mapping;
+ }, { ttl: 60 * 30 });
+}
+
+module.exports.mapGameMode = async (id) =>
+ (await api.getMappings())["gamemode"][id];
+
+module.exports.mapActor = async (api_name) =>
+ (await api.getMappings())["hero"][api_name];
+
+// return a set of IGN of supporters
+module.exports.getGamers = async () =>
+ (await api.getFE("/gamer", {}, 60 * 30)).map((gamer) => gamer.name);
+
+// be an async iterator
+// next() returns promises that are awaited until there is an update
+module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => {
+ const channel = new Channel(),
+ subscription = subscribe("player." + name, channel);
+ let subscribed = true;
+
+ function stop() {
+ if (!subscribed) return;
+ subscribed = false;
+ channel.close();
+ subscription.unsubscribe();
+ clearTimeout(timer);
+ }
+
+ // stop updates after timeout
+ const timer = setTimeout(() => stop(), timeout*1000);
+
+ return { next: async () => {
+ let msg;
+ do msg = await channel.take();
+ while([Channel.DONE, "search_fail", "search_success",
+ "stats_update", "matches_update", "matches_none"]
+ .indexOf(msg) == -1);
+ // bust caches
+ if (msg == "stats_update")
+ cache.del("player+" + name);
+ if (msg == "matches_update") {
+ cache.del("matches+" + name);
+ cache.del("player+" + name);
+ }
+ if (msg == "matches_none") stop(); // no new data
+ if (msg == "search_fail") {
+ stop();
+ throw { error: {
+ err: "No player found for the provided IGN."
+ } };
+ }
+ if (msg == Channel.DONE) {
+ subscribed = false;
+ subscription.unsubscribe();
+ return undefined;
+ }
+ return msg;
+ }, stop: stop };
+}
+
+// search an unknown player
+module.exports.searchPlayer = (name) =>
+ api.postBE("/player/" + name + "/search");
+
+// update a known player
+module.exports.updatePlayer = (name) =>
+ api.postBE("/player/" + name + "/update");
+
+// return player
+module.exports.getPlayer = async (name) => {
+ return await api.getFE("/player/" + name, {}, 60, "player+" + name);
+}
+
+// search or update a player
+module.exports.upsearchPlayer = async (name) => {
+ try {
+ await api.getPlayer(name);
+ await api.updatePlayer(name);
+ } catch (err) {
+ await api.searchPlayer(name);
+ }
+}
+
+// block until update is completely done
+module.exports.upsearchPlayerSync = async (name) => {
+ const waiter = await api.subscribeUpdates(name);
+ await api.upsearchPlayer(name);
+ while ([undefined, "matches_update", "matches_none"]
+ .indexOf(await waiter.next()) == -1);
+ waiter.stop();
+}
+
+// return matches
+module.exports.getMatches = async (name) => {
+ const data = await api.getFE("/player/" + name + "/matches/1.1.1.1", {},
+ 60 * 60, "matches+" + name);
+ return data.data;
+}
+
+// return single match
+module.exports.getMatch = async (id) => {
+ return await api.getFE("/match/" + id, {}, 60 * 60);
+}
+
+// return a guild
+module.exports.getGuild = async (token) => {
+ return await api.getFE("/guild", { user_token: token }, 60, "guild+" + token);
+}
+
+// return a guild
+module.exports.getGuildMembersByGuildName = async (name) => {
+ // TODO caching
+ return await api.getFE("/guild/" + name + "/members", { }, 0);
+}
+// TODO! cache guilds by guild id, not by user token
+
+// add user to guild
+module.exports.addToGuild = async (token, member) => {
+ const membership = await api.postFE("/guild/members", {
+ user_token: token,
+ member_name: member
+ }, "guild+" + token);
+ return membership;
+}
+
+// kick from guild
+module.exports.removeFromGuild = async (token, member) => {
+ const membership = await api.deleteFE("/guild/members/" + member, {
+ user_token: token
+ }, "guild+" + token);
+ return membership;
+}
+
+// change a role
+module.exports.changeRole = async (token, member, role) => {
+ const membership = await api.putFE("/guild/members/updateRole", {
+ user_token: token,
+ member_name: member,
+ new_role: role
+ }, "guild+" + token);
+ return membership;
+}
+
+// recalc fame, block until timeout or points update
+module.exports.calculateGuild = async (id, token) => {
+ const channel = new Channel(),
+ subscription = subscribe("global", channel);
+ await api.backend("/team/" + id + "/crunch");
+
+ // stop updates after timeout
+ setTimeout(() => channel.close(), UPDATE_TIMEOUT*1000);
+
+ let msg;
+ do msg = await channel.take();
+ while([Channel.DONE, "points_update"].indexOf(msg) == -1);
+
+ if (msg == "points_update") cache.del("guild+" + token);
+ subscription.unsubscribe();
+}
+
+// store Discord ID <-> IGN
+module.exports.setUser = async (token, name) => {
+ cache.del("user+" + token);
+ await api.post("/user", {
+ name: name,
+ user_token: token
+ });
+}
+
+// retrieve Discord ID -> IGN
+module.exports.getUser = async (token) => {
+ const user = await api.getFE("/user", { user_token: token }, 60, "user+" + token);
+ return user.name;
+}
diff --git a/bot.js b/bot.js
new file mode 100644
index 0000000..1efa1e4
--- /dev/null
+++ b/bot.js
@@ -0,0 +1,103 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const PREVIEW = process.env.PREVIEW != "false";
+
+const sqlite = require("sqlite"),
+ path = require("path"),
+ winston = require("winston"),
+ loggly = require("winston-loggly-bulk"),
+ Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ client = new Commando.Client({
+ owners: ["227440521704898561", "208974925199966208"],
+ // shutterfly, stormcaller
+ commandPrefix: (PREVIEW? "?" : "!"),
+ invite: "https://discord.gg/txTchJY",
+ unknownCommandResponse: false
+ }),
+ util = require("./util");
+
+const DISCORD_TOKEN = process.env.DISCORD_TOKEN,
+ LOGGLY_TOKEN = process.env.LOGGLY_TOKEN;
+
+const logger = new (winston.Logger)({
+ transports: [
+ new (winston.transports.Console)({
+ timestamp: true,
+ colorize: true
+ })
+ ]
+});
+
+if (LOGGLY_TOKEN)
+ logger.add(winston.transports.Loggly, {
+ inputToken: LOGGLY_TOKEN,
+ subdomain: "kvahuja",
+ tags: ["frontend", "discordbot"],
+ json: true
+ });
+
+client
+ .on("error", logger.error)
+ .on("warn", logger.warn)
+ .on("debug", logger.info)
+ .on("ready", () => {
+ logger.info(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`);
+ util.rotateGameStatus(client);
+ })
+ .on('disconnect', () => { logger.warn('Disconnected!'); })
+ .on('reconnecting', () => { logger.warn('Reconnecting...'); })
+ .on('commandError', (cmd, err) => {
+ if(err instanceof Commando.FriendlyError) return;
+ logger.error(`Error in command ${cmd.groupID}:${cmd.memberName}`, err);
+ })
+ .on('commandBlocked', (msg, reason) => {
+ logger.info(oneLine`
+ Command ${msg.command ? `${msg.command.groupID}:${msg.command.memberName}` : ''}
+ blocked; ${reason}
+ `);
+ })
+ .on('commandPrefixChange', (guild, prefix) => {
+ logger.info(oneLine`
+ Prefix ${prefix === '' ? 'removed' : `changed to ${prefix || 'the default'}`}
+ ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}.
+ `);
+ })
+ .on('commandStatusChange', (guild, command, enabled) => {
+ logger.info(oneLine`
+ Command ${command.groupID}:${command.memberName}
+ ${enabled ? 'enabled' : 'disabled'}
+ ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}.
+ `);
+ })
+ .on('groupStatusChange', (guild, group, enabled) => {
+ logger.info(oneLine`
+ Group ${group.id}
+ ${enabled ? 'enabled' : 'disabled'}
+ ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}.
+ `);
+ })
+
+ // response reaction interface
+ .on("messageReactionAdd", util.onNewReaction);
+
+client.setProvider(
+ sqlite.open(path.join(__dirname, "settings.sqlite3")).then(
+ db => new Commando.SQLiteProvider(db)));
+
+client.registry
+ .registerGroup('vainsocial', 'VainSocial')
+ .registerGroup('vainsocial-guild', 'VainSocial Guild management')
+ .registerDefaultTypes()
+ .registerDefaultGroups()
+ .registerDefaultCommands({ eval_: false, commandState: false })
+ .registerCommandsIn(path.join(__dirname, 'commands'));
+
+
+client.login(DISCORD_TOKEN);
+
+process.on("unhandledRejection", err => {
+ logger.error("Uncaught Promise Error", err);
+});
diff --git a/commands/vainsocial/about.js b/commands/vainsocial/about.js
new file mode 100644
index 0000000..e7941e3
--- /dev/null
+++ b/commands/vainsocial/about.js
@@ -0,0 +1,37 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ util = require("../../util");
+
+const PREVIEW = process.env.PREVIEW != "false",
+ ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/");
+
+module.exports = class ShowAboutCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "about",
+ group: "vainsocial",
+ memberName: "about",
+ description: "Shows invite links and developer contact details."
+ });
+ }
+ async run(msg) {
+ util.trackAction(msg, "about");
+ await msg.embed(util.vainsocialEmbed("About VainSocial", "", "about")
+ .setDescription(
+ `Built by the VainSocial development team using the MadGlory API.
+ Currently running on ${msg.client.guilds.size} servers.`)
+ .addField("Website",
+ ROOTURL + util.track("about"), true)
+ .addField("Bot invite link",
+ "https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot&permissions=52288", true)
+ .addField("Developer Discord invite",
+ "https://discord.gg/txTchJY", true)
+ .addField("Twitter",
+ "https://twitter.com/vainsocial", true)
+ );
+ }
+};
diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js
new file mode 100644
index 0000000..bf8f8f6
--- /dev/null
+++ b/commands/vainsocial/guild_add.js
@@ -0,0 +1,52 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ Promise = require("bluebird"),
+ api = require("../../api"),
+ util = require("../../util"),
+ GuildAddView = require("../../views/guild_progress");
+
+module.exports = class AddGuildMemberCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildadd",
+ aliases: ["vguild-add", "vgadd", "vga"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildadd",
+ description: "Register a member to your Guild.",
+ details: oneLine`
+Register IGNs to your Guild.
+`,
+ examples: ["vgadd StormCallerSr", "vgadd StormCallerSr shutterfly"],
+ argsType: "multiple"
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-add");
+ let playersStatus = {};
+ const playersWaiters = args.map((name) => api.subscribeUpdates(name)),
+ guildAddView = new GuildAddView(msg, playersStatus);
+ // create waiter dict & data dict
+ await Promise.each(playersWaiters, async (waiter, idx) => {
+ let success = false;
+ try {
+ playersStatus[args[idx]] = "Loading…";
+ await guildAddView.respond();
+ await api.upsearchPlayerSync(args[idx]);
+ success = true;
+ } catch (err) {
+ console.error(err);
+ playersStatus[args[idx]] = err.error.err;
+ success = false;
+ }
+ if (success) {
+ await api.addToGuild(msg.author.id, args[idx]);
+ playersStatus[args[idx]] = "Loaded.";
+ }
+ });
+ await guildAddView.respond("Your Guild members were added.");
+ }
+};
diff --git a/commands/vainsocial/guild_create.js b/commands/vainsocial/guild_create.js
new file mode 100644
index 0000000..a8caa6a
--- /dev/null
+++ b/commands/vainsocial/guild_create.js
@@ -0,0 +1,70 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ util = require("../../util"),
+ api = require("../../api"),
+ GuildCreateView = require("../../views/guild_create");
+
+module.exports = class RegisterGuildCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildcreate",
+ aliases: ["vguild-create", "vgcreate"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildcreate",
+ description: "Register a new Guild.",
+ details: oneLine`
+Create a Guild with your VainSocial profile as leader.
+`,
+ examples: ["vguild-create MyAmazingGuild MAG eu"],
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your Guild's name.",
+ type: "string",
+ min: 3
+ }, {
+ key: "tag",
+ label: "tag",
+ prompt: "Please specify your Guild's tag.",
+ type: "string",
+ min: 2,
+ max: 4
+ }, {
+ // TODO use enum
+ key: "region",
+ label: "region",
+ prompt: "Please specify your Guild's region.",
+ type: "string",
+ min: 2,
+ max: 10
+ } ]
+ });
+ }
+ // register a VainSocial Guild to a Discord account
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-create");
+ const guildCreateView = new GuildCreateView(msg, msg.author.id);
+ if (msg.guild.id != 283790513998659585)
+ return await guildCreateView.error(oneLine`
+Guilds creation is currently running in closed beta.
+If you want to participate, join our development Discord: https://discord.gg/txTchJY
+`);
+ try {
+ await api.post("/guild", {
+ shard_id: args.region,
+ name: args.name,
+ identifier: args.tag,
+ user_token: msg.author.id
+ });
+ } catch (err) {
+ console.error(err);
+ return await guildCreateView.error(err.error.err);
+ }
+ await guildCreateView.respond();
+ }
+};
diff --git a/commands/vainsocial/guild_member.js b/commands/vainsocial/guild_member.js
new file mode 100644
index 0000000..e72fff1
--- /dev/null
+++ b/commands/vainsocial/guild_member.js
@@ -0,0 +1,74 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ Promise = require("bluebird"),
+ api = require("../../api"),
+ util = require("../../util"),
+ GuildMemberView = require("../../views/guild_member");
+
+module.exports = class ViewGuildMemberCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildmember",
+ aliases: ["vguild-member", "vgm"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildmember",
+ description: "View a Guild member in detail.",
+ examples: ["vgm StormCallerSr"],
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify a Guild member's name.",
+ type: "string",
+ min: 2,
+ default: "?"
+ }, {
+ key: "guild",
+ label: "guild",
+ prompt: "Please specify a Guild's name.",
+ type: "string",
+ min: 2,
+ default: "?"
+ } ]
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-member");
+ const guildMemberView = new GuildMemberView(msg);
+ // get IGN or default
+ let ign;
+ try {
+ ign = await util.ignForUser(args.name, msg.author.id);
+ } catch (err) {
+ return await guildMemberView.error(strings.unknown(msg));
+ }
+ // get guild: name, server default, self
+ let guildName = args.guild, guild;
+ if (guildName == "?") {
+ guildName = msg.guild.settings.get("default-guild-name");
+ }
+
+ try {
+ let members;
+ // TODO.
+ if (guildName == undefined) members = (await api.getGuild(msg.author.id)).members;
+ else members = await api.getGuildMembersByGuildName(guildName);
+ console.log(guildName);
+ if (members.length == 0)
+ throw { error: { err: "Could not find that Guild." } };
+
+ const member = members.filter((m) => m.player.name == args.name)[0];
+ if (member == undefined)
+ throw { error: { err: "Player is not in the Guild." } };
+ const player = await api.getPlayer(member.player.name);
+ const matches = await api.getMatches(player.name);
+ await guildMemberView.respond(member, player, matches);
+ } catch (err) {
+ console.error(err);
+ return await guildMemberView.error(err.error.err);
+ }
+ }
+};
diff --git a/commands/vainsocial/guild_pin.js b/commands/vainsocial/guild_pin.js
new file mode 100644
index 0000000..986666e
--- /dev/null
+++ b/commands/vainsocial/guild_pin.js
@@ -0,0 +1,46 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ util = require("../../util"),
+ api = require("../../api"),
+ SimpleView = require("../../views/simple");
+
+module.exports = class PinGuildCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildpin",
+ aliases: ["vguild-pin", "vgpin"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildpin",
+ description: "Share your Guild on a server.",
+ examples: ["vgview"],
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your Guild's name.",
+ type: "string",
+ min: 2,
+ default: "?"
+ } ]
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-pin");
+ const pinView = new SimpleView(msg);
+ if (!msg.member.hasPermission('ADMINISTRATOR'))
+ return await pinView.error("You need to be server administrator to pin a GUild.");
+ let guild;
+ try {
+ guild = await api.getGuild(msg.author.id);
+ } catch (err) {
+ console.log(err);
+ return await guildOverviewView.error(err.error.err);
+ }
+ msg.guild.settings.set("default-guild-name", guild.name);
+ await pinView.respond(`Successfully pinned the Guild \`${guild.name}\` to your server.`);
+ }
+};
diff --git a/commands/vainsocial/guild_rm.js b/commands/vainsocial/guild_rm.js
new file mode 100644
index 0000000..e74aea4
--- /dev/null
+++ b/commands/vainsocial/guild_rm.js
@@ -0,0 +1,48 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ Promise = require("bluebird"),
+ api = require("../../api"),
+ util = require("../../util"),
+ GuildRmView = require("../../views/guild_progress");
+
+module.exports = class AddGuildMemberCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildrm",
+ aliases: ["vguild-rm", "vgrm"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildrm",
+ description: "Remove a member from your Guild.",
+ details: oneLine`
+Remove an IGN from your Guild.
+`,
+ examples: ["vgrm StormCallerSr", "vgrm StormCallerSr shutterfly"],
+ argsType: "multiple"
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-rm");
+ let playersStatus = {};
+ const guildRmView = new GuildRmView(msg, playersStatus);
+ try {
+ await Promise.each(args, async (name) => {
+ try {
+ playersStatus[name] = "Removing…";
+ await api.removeFromGuild(msg.author.id, name);
+ playersStatus[name] = "Removed.";
+ await guildRmView.respond();
+ } catch (err) {
+ playersStatus[name] = err.error.err;
+ }
+ });
+ } catch (err) {
+ console.error(err);
+ return await guildRmView.error(err.error.err);
+ }
+ await guildRmView.respond("Your Guild members were removed.");
+ }
+};
diff --git a/commands/vainsocial/guild_role.js b/commands/vainsocial/guild_role.js
new file mode 100644
index 0000000..50b88e4
--- /dev/null
+++ b/commands/vainsocial/guild_role.js
@@ -0,0 +1,46 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ Promise = require("bluebird"),
+ api = require("../../api"),
+ util = require("../../util"),
+ SimpleView = require("../../views/simple");
+
+module.exports = class RoleGuildMemberCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildrole",
+ aliases: ["vguild-role", "vgrole", "vgr"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildrole",
+ description: "Change a Guild member's role.",
+ examples: ["vgrole StormCallerSr Officer", "vgrole shutterfly Leader"],
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify a Guild member's name.",
+ type: "string",
+ min: 2
+ }, {
+ key: "role",
+ label: "role",
+ prompt: "Please specify the member's new role.",
+ type: "string"
+ } ]
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-role");
+ const simpleView = new SimpleView(msg);
+ try {
+ const member = await api.changeRole(msg.author.id, args.name, args.role);
+ await simpleView.respond(`Successfully changed ${args.name}'s role to ${args.role}.`);
+ } catch (err) {
+ console.log(err);
+ return await simpleView.error(err.error.err);
+ }
+ }
+};
diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js
new file mode 100644
index 0000000..a61647f
--- /dev/null
+++ b/commands/vainsocial/guild_update.js
@@ -0,0 +1,60 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ Promise = require("bluebird"),
+ api = require("../../api"),
+ util = require("../../util"),
+ GuildMembersProgressView = require("../../views/guild_progress");
+
+module.exports = class UpdateGuildCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildupdate",
+ aliases: ["vguild-update", "vgupdate"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildupdate",
+ description: "Update your Guild members' data.",
+ details: oneLine`
+Update the match history for all your Guild members.
+`,
+ examples: ["vgupdate"]
+ });
+ }
+ // internal / premium: immediately call backend player refresh
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-update");
+ // obj of ign: player
+ let playersStatus = {};
+ // collect an array of IGNs
+ let names, guild;
+ const guildUpdateView = new GuildMembersProgressView(msg, playersStatus);
+ try {
+ guild = await api.getGuild(msg.author.id);
+ names = guild.members.map((m) => m.player.name);
+ } catch (err) {
+ console.log(err);
+ return await guildUpdateView.error(err.error.err);
+ }
+ // update all the IGNs
+ const playersWaiters = names.map((name) => api.subscribeUpdates(name));
+ // create waiter dict & data dict
+ await Promise.each(playersWaiters, async (waiter, idx) => {
+ playersStatus[names[idx]] = "Loading…";
+ await guildUpdateView.respond();
+ await api.upsearchPlayerSync(names[idx]);
+ playersStatus[names[idx]] = "Loaded.";
+ await guildUpdateView.respond();
+ });
+ await guildUpdateView.respond("Your Guild's fame is being updated…");
+ try {
+ await api.calculateGuild(guild.id, msg.author.id);
+ } catch (err) {
+ console.log(err);
+ await guildUpdateView.error(err.error.err);
+ }
+ await guildUpdateView.respond("Your Guild was updated.");
+ }
+};
diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js
new file mode 100644
index 0000000..28ddad3
--- /dev/null
+++ b/commands/vainsocial/guild_view.js
@@ -0,0 +1,46 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ util = require("../../util"),
+ api = require("../../api"),
+ GuildOverviewView = require("../../views/guild");
+
+module.exports = class ViewGuildCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-guildview",
+ aliases: ["vguild-view", "vgview", "vgv"],
+ group: "vainsocial-guild",
+ memberName: "vainsocial-guildview",
+ description: "View members of a Guild.",
+ details: oneLine`
+Show a summary of your Guild.
+`,
+ examples: ["vgview"],
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your Guild's name.",
+ type: "string",
+ min: 2,
+ default: "?"
+ } ]
+ });
+ }
+ // show Guild details
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-guild-view");
+ const guildOverviewView = new GuildOverviewView(msg);
+ try {
+ const guild = await api.getGuild(msg.author.id);
+ await guildOverviewView.respond(guild);
+ } catch (err) {
+ console.log(err);
+ return await guildOverviewView.error(err.error.err);
+ }
+ }
+};
diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js
new file mode 100644
index 0000000..3acde84
--- /dev/null
+++ b/commands/vainsocial/match.js
@@ -0,0 +1,68 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ util = require("../../util"),
+ api = require("../../api"),
+ strings = require("../../strings"),
+ MatchView = require("../../views/match");
+
+module.exports = class ShowMatchCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-match",
+ aliases: ["vm"],
+ group: "vainsocial",
+ memberName: "vainsocial-match",
+ description: "Shows a user's match in detail.",
+ examples: ["vm shutterfly 1"],
+ argsType: "multiple",
+ argsCount: 2,
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your in game name (Case Sensitive).",
+ type: "string",
+ default: "?",
+ min: 3,
+ max: 16
+ }, {
+ key: "number",
+ label: "number",
+ prompt: "Please specify how far you want to go back in history. Use 1 or leave out for the latest match.",
+ type: "integer",
+ default: 1,
+ min: 1,
+ max: 10
+ } ]
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-match", args.name);
+ let ign;
+ try {
+ ign = await util.ignForUser(args.name, msg.author.id);
+ } catch (err) {
+ return await new MatchView(msg, undefined).error(strings.unknown(msg));
+ }
+
+ const matchView = new MatchView(msg);
+ try {
+ const participations = await api.getMatches(ign);
+ if (args.number > participations.length)
+ return await msg.say(strings.tooFewMatches(ign));
+ // wait for BE update
+ const waiter = api.subscribeUpdates(ign);
+ await api.upsearchPlayer(ign);
+
+ do await matchView.respond(participations[args.number-1].match_api_id);
+ while (await waiter.next() != undefined);
+ } catch (err) {
+ console.error(err);
+ return await matchView.error(err.error.err);
+ }
+ }
+};
diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js
new file mode 100644
index 0000000..7499725
--- /dev/null
+++ b/commands/vainsocial/matches.js
@@ -0,0 +1,56 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ util = require("../../util"),
+ api = require("../../api"),
+ strings = require("../../strings"),
+ MatchesView = require("../../views/matches");
+
+module.exports = class ShowMatchesCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-history",
+ aliases: ["vh"],
+ group: "vainsocial",
+ memberName: "vainsocial-matches",
+ description: "Shows a user's match history.",
+ examples: ["vh shutterfly"],
+ argsType: "single",
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your in game name (Case Sensitive).",
+ type: "string",
+ default: "?",
+ min: 3,
+ max: 16
+ } ]
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-matches", args.name);
+ let ign;
+ try {
+ ign = await util.ignForUser(args.name, msg.author.id);
+ } catch (err) {
+ return await strings.unknown(msg);
+ }
+
+ const matchesView = new MatchesView(msg, ign);
+ // wait for BE update
+ try {
+ const waiter = api.subscribeUpdates(ign);
+ await api.upsearchPlayer(ign);
+
+ do await matchesView.respond();
+ while (await waiter.next() != undefined);
+ } catch (err) {
+ console.log(err);
+ return await matchesView.error(err.error.err);
+ }
+ }
+};
diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js
new file mode 100644
index 0000000..8679a65
--- /dev/null
+++ b/commands/vainsocial/me.js
@@ -0,0 +1,47 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ api = require("../../api"),
+ util = require("../../util"),
+ RegisterView = require("../../views/register");
+
+module.exports = class RegisterUserCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-me",
+ aliases: ["vme", "vgme"],
+ group: "vainsocial",
+ memberName: "vainsocial-me",
+ description: "Register a users's in game name.",
+ details: oneLine`
+Store your in game name for quicker access to other commands and for Guild management.
+ `,
+ examples: ["vme shutterfly"],
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your in game name (Case Sensitive).",
+ type: "string",
+ min: 3,
+ max: 16
+ } ]
+ });
+ }
+ // register a Discord account at VainSocial
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-me", args.name);
+ const registerView = new RegisterView(msg, args.name);
+ try {
+ await api.upsearchPlayerSync(args.name);
+ await api.setUser(msg.author.id, args.name);
+ } catch (err) {
+ console.log(err);
+ return await registerView.error(err.error.err);
+ }
+ await registerView.respond();
+ }
+};
diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js
new file mode 100644
index 0000000..e251ad7
--- /dev/null
+++ b/commands/vainsocial/user.js
@@ -0,0 +1,59 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Commando = require("discord.js-commando"),
+ oneLine = require("common-tags").oneLine,
+ emoji = require("discord-emoji"),
+ util = require("../../util"),
+ strings = require("../../strings"),
+ api = require("../../api"),
+ PlayerView = require("../../views/player");
+
+module.exports = class ShowUserCommand extends Commando.Command {
+ constructor(client) {
+ super(client, {
+ name: "vainsocial-user",
+ aliases: ["v", "vu"],
+ group: "vainsocial",
+ memberName: "vainsocial-user",
+ description: "Shows a player\'s profile.",
+ details: oneLine`
+Display VainSocial lifetime statistics from Vainglory
+ `,
+ examples: ["vu shutterfly"],
+
+ args: [ {
+ key: "name",
+ label: "name",
+ prompt: "Please specify your in game name (Case Sensitive).",
+ type: "string",
+ default: "?",
+ min: 3,
+ max: 16
+ } ]
+ });
+ }
+ async run(msg, args) {
+ util.trackAction(msg, "vainsocial-user", args.name);
+ let ign;
+ try {
+ ign = await util.ignForUser(args.name, msg.author.id);
+ } catch (err) {
+ return await new PlayerView(msg, args.name).error(strings.unknown(msg));
+ }
+
+ const playerView = new PlayerView(msg, ign);
+ try {
+ // wait for BE update
+ const waiter = api.subscribeUpdates(ign);
+ await api.upsearchPlayer(ign);
+
+ do await playerView.respond();
+ while (await waiter.next() != undefined);
+ } catch (err) {
+ console.log(err);
+ return await playerView.error(err.error.err);
+ }
+ }
+};
diff --git a/joblib b/joblib
deleted file mode 160000
-Subproject 062f24ea27e7340deb5c46aa495a2467f2e42e7
diff --git a/main.py b/main.py
deleted file mode 100644
index f46fa73..0000000
--- a/main.py
+++ /dev/null
@@ -1,193 +0,0 @@
-#!/usr/bin/env python3
-
-import logging
-import os
-import asyncio
-import asyncpg
-import discord
-from discord.ext import commands
-import joblib.joblib
-
-source_db = {
- "host": os.environ.get("POSTGRESQL_SOURCE_HOST") or "localhost",
- "port": os.environ.get("POSTGRESQL_SOURCE_PORT") or 5433,
- "user": os.environ.get("POSTGRESQL_SOURCE_USER") or "vainraw",
- "password": os.environ.get("POSTGRESQL_SOURCE_PASSWORD") or "vainraw",
- "database": os.environ.get("POSTGRESQL_SOURCE_DB") or "vainsocial-raw"
-}
-
-dest_db = {
- "host": os.environ.get("POSTGRESQL_DEST_HOST") or "localhost",
- "port": os.environ.get("POSTGRESQL_DEST_PORT") or 5432,
- "user": os.environ.get("POSTGRESQL_DEST_USER") or "vainweb",
- "password": os.environ.get("POSTGRESQL_DEST_PASSWORD") or "vainweb",
- "database": os.environ.get("POSTGRESQL_DEST_DB") or "vainsocial-web"
-}
-
-
-bot = commands.Bot(
- command_prefix="!",
- description="Vainsocial Vainglory stats bot")
-
-queue = None
-pool = None
-
-async def connect(queuedb, db):
- logging.warning("connecting to database")
-
- global queue
- queue = joblib.joblib.JobQueue()
- await queue.connect(**queuedb)
- await queue.setup()
-
- global pool
- pool = await asyncpg.create_pool(
- min_size=1, **db)
-
-
-@bot.event
-async def on_ready():
- logging.warning("Logged in as %s (" +
- "https://discordapp.com/oauth2/authorize?&" +
- "client_id=%s&scope=bot)",
- bot.user.name, bot.user.id)
- await connect(source_db, dest_db)
- await bot.change_presence(
- game=discord.Game(
- name="alpha.vainsocial.com"))
-
-
-@bot.event
-async def on_command_error(error, ctx):
- logging.error(error)
- if ctx.invoked_subcommand:
- pages = bot.formatter.format_help_for(
- ctx, ctx.invoked_subcommand)
- for page in pages:
- await bot.send_message(
- ctx.message.channel, page)
- else:
- pages = bot.formatter.format_help_for(
- ctx, ctx.command)
- for page in pages:
- await bot.send_message(
- ctx.message.channel, page)
-
-
-@bot.command()
-async def vainsocial(name: str, region: str):
- """Retrieves a player's stats."""
- region = region.lower()
- if region not in ["na", "eu", "sg", "ea", "sa"]:
- await bot.say("That region is not supported.")
- return
-
- query = """
- SELECT
- player.name,
- player.shard_id,
- match.game_mode,
- roster.match_api_id,
- participant.hero, participant.winner,
- participant.kills, participant.deaths, participant.assists, participant.farm,
- player.skill_tier, player.played, player.wins,
- player.last_match_created_date::text
- FROM match, roster, participant, player where
- match.api_id=roster.match_api_id AND
- roster.api_id=participant.roster_api_id AND
- participant.player_api_id=player.api_id AND
- player.name=$1 AND player.last_match_created_date::text<>$2
- ORDER BY match.created_at DESC
- LIMIT 1
- """
- def emb(dct):
- data = dict(dct)
-
- modes = {
- "blitz_pvp_ranked": "Blitz",
- "casual_aral": "Battle Royale",
- "private": "private casual",
- "private_party_draft_match": "private draft",
- "private_party_blitz_match": "private Blitz",
- "private_party_aral_match": "private Battle Royale"
- }
- data["mode"] = modes.get(data["game_mode"]) or data["game_mode"]
- heroes = {
- "Sayoc": "Taka",
- "Hero009": "Krul",
- "Hero010": "Skaarf",
- "Hero016": "Rona"
- }
- data["hero"] = data["hero"].replace("*", "")
- data["hero"] = heroes.get(data["hero"]) or data["hero"]
- data["result"] = "won" if data["winner"] else "lost"
-
- emb = discord.Embed(
- title="%(name)s" % data,
- url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s" % data
- )
- emb.set_author(name="Vainsocial",
- url="https://alpha.vainsocial.com")
- emb.add_field(name="Profile",
- value=("%(wins)i wins / %(played)i games\n" +
- "https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s") % data)
- emb.add_field(name="Last match",
- value=("%(result)s %(mode)s as %(hero)s %(kills)i/%(deaths)i/%(assists)i\n" +
- "https://alpha.vainsocial.com/matches/%(match_api_id)s") % data)
-
- emb.set_footer(text="Vainsocial - Vainglory social stats service")
- emb.set_thumbnail(url="https://alpha.vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data)
- return emb
-
- async with pool.acquire() as con:
- await bot.type()
-
- data = await con.fetchrow(query, name, 'NULL')
- if data is None:
- in_cache = False # new user
- lmcd = "2017-02-01T00:00:00Z"
- msgid = await bot.say(
- "%s: please wait…" % name)
- else:
- in_cache = True # returning user
- lmcd = data["last_match_created_date"]
- msgid = await bot.say(embed=emb(data))
-
- logging.info("'%s' cached: %s", name, in_cache)
-
- payload = {
- "region": region,
- "params": {
- "filter[createdAt-start]": lmcd,
- "filter[playerNames]": name
- }
- }
- jobid = (await queue.request(jobtype="grab",
- payload=[payload],
- priority=[1]))[0]
-
- while True:
- # wait for grab job to finish
- status = await queue.status(jobid)
- if status == 'finished':
- break
- if status == 'failed':
- logging.warning("'%s': not found", name)
- if not in_cache:
- await bot.edit_message(msgid,
- "Could not find you.")
- return
- asyncio.sleep(0.5)
-
- while True:
- # wait for processor to update the player
- data = await con.fetchrow(query, name, lmcd)
- if data is not None:
- break
- asyncio.sleep(0.5)
-
- await bot.edit_message(msgid, embed=emb(data))
-
-
-logging.basicConfig(level=logging.INFO)
-bot.run(os.environ["VAINSOCIAL_DISCORDTOKEN"])
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..fc94066
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "discordbot",
+ "version": "2.0.0",
+ "description": "",
+ "main": "bot.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "schneefux",
+ "license": "UNLICENSED",
+ "dependencies": {
+ "async-csp": "^0.5.0",
+ "bluebird": "^3.5.0",
+ "bufferutil": "^2.0.0",
+ "cache-manager": "^2.4.0",
+ "discord-emoji": "^1.1.1",
+ "discord.js": "git+https://github.com/hydrabolt/discord.js.git",
+ "discord.js-commando": "git+https://github.com/Gawdl3y/discord.js-commando.git",
+ "erlpack": "github:hammerandchisel/erlpack",
+ "request": "^2.81.0",
+ "request-promise": "^4.2.0",
+ "sleep-promise": "^2.0.0",
+ "sqlite": "^2.5.0",
+ "universal-analytics": "^0.4.13",
+ "webstomp": "0.0.10",
+ "webstomp-client": "^1.0.6",
+ "winston": "^2.3.1",
+ "winston-loggly-bulk": "^1.4.2",
+ "ws": "^2.2.3"
+ }
+}
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 7c3bc62..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-aiohttp==1.0.5
-appdirs==1.4.2
-async-timeout==1.1.0
-asyncpg==0.9.0
-chardet==2.3.0
-discord.py==0.16.7
-multidict==2.1.4
-packaging==16.8
-pyparsing==2.1.10
-six==1.10.0
-websockets==3.2
diff --git a/responses.js b/responses.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/responses.js
diff --git a/strings.js b/strings.js
new file mode 100644
index 0000000..546dfc0
--- /dev/null
+++ b/strings.js
@@ -0,0 +1,48 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const emoji = require("discord-emoji"),
+ oneLine = require("common-tags").oneLine,
+ Promise = require("bluebird"),
+ api = require("./api"),
+ util = require("./util");
+const strings = module.exports;
+
+const MATCH_HISTORY_LEN = parseInt(process.env.MATCH_HISTORY_LEN) || 3,
+ PREVIEW = process.env.PREVIEW != "false",
+ ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/");
+
+// tell the user that they need to store their name
+module.exports.unknown = (msg) =>
+ `You're unknown to our service. Try ${util.usg(msg, "vme")}.`;
+
+module.exports.notFound = (ign) =>
+ `Could not find ${ign}.`;
+
+module.exports.noMatches = (ign) =>
+ `Could not find any casual/ranked matches for ${ign}.`;
+
+module.exports.tooFewMatches = (ign) =>
+ `Could not find that many casual/ranked matches for ${ign}.`;
+
+module.exports.loading = (ign) =>
+ `Loading data for ${ign}…`;
+
+module.exports.emojiCount = [ emoji.symbols.one, emoji.symbols.two, emoji.symbols.three,
+ emoji.symbols.four, emoji.symbols.five, emoji.symbols.six,
+ emoji.symbols.seven, emoji.symbols.eight, emoji.symbols.nine,
+ emoji.symbols.ten
+];
+
+module.exports.profile = "Profile";
+module.exports.lastMatch = "Last Match";
+
+// based on impact score float, return an Emoji
+module.exports.emojifyScore = (score) => {
+ if (score > 0.7) return emoji.people.heart_eyes;
+ if (score > 0.6) return emoji.people.blush;
+ if (score > 0.5) return emoji.people.yum;
+ if (score > 0.3) return emoji.people.relieved;
+ return emoji.people.upside_down;
+};
diff --git a/util.js b/util.js
new file mode 100644
index 0000000..c33d7b7
--- /dev/null
+++ b/util.js
@@ -0,0 +1,149 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const Discord = require("discord.js"),
+ ua = require("universal-analytics"),
+ api = require("./api"),
+ strings = require("./strings"),
+ Promise = require("bluebird"),
+ Channel = require("async-csp").Channel;
+const util = module.exports;
+
+const GOOGLEANALYTICS_ID = process.env.GOOGLEANALYTICS_ID,
+ REACTION_TIMEOUT = parseInt(process.env.REACTION_TIMEOUT) || 60, // s
+ IGN_ROTATE_TIMEOUT = parseInt(process.env.IGN_ROTATE_TIMEOUT) || 300,
+ PREVIEW = process.env.PREVIEW != "false",
+ ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/");
+
+const reactionsPipe = new Channel();
+
+// embed template
+module.exports.vainsocialEmbed = (title, link, command) => {
+ return new Discord.RichEmbed()
+ .setTitle(title)
+ .setColor("#55ADD3")
+ .setURL(ROOTURL + encodeURIComponent(link) + util.track(command))
+ .setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png",
+ ROOTURL + util.track(command))
+ .setFooter("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png")
+};
+
+// "playing…"
+module.exports.rotateGameStatus = (client) => {
+ (async function rotate() {
+ const gamers = await api.getGamers(),
+ idx = Math.floor(Math.random() * (gamers.length - 1)) + 1;
+ if (PREVIEW) await client.user.setGame(
+ `?v ${gamers[idx]} | preview.vainsocial.com`);
+ else await client.user.setGame(
+ `!v ${gamers[idx]} | vainsocial.com`);
+ setTimeout(rotate, IGN_ROTATE_TIMEOUT * 1000);
+ })();
+}
+
+// analytics url
+module.exports.track = (command) => {
+ return "?utm_source=discordbot&utm_medium=discord&utm_campaign=" + command;
+};
+
+// direct analytics
+module.exports.trackAction = (msg, action, ign="") => {
+ if (GOOGLEANALYTICS_ID == undefined) return;
+ const user = ua(GOOGLEANALYTICS_ID, msg.author.id,
+ { strictCidFormat: false });
+ user.pageview({
+ documentPath: action,
+ documentTitle: ign,
+ campaignSource: (msg.guild? msg.guild.id : 0),
+ campaignMedium: (msg.guild? msg.guild.name : "private")
+ }).send();
+};
+
+// return the shortest version of the usage help
+// just '?vm'
+module.exports.usg = (msg, cmd) => {
+ return msg.anyUsage(cmd, undefined, null);
+};
+
+// reaction -> pipe ->>> consumers
+// handle new reaction event
+module.exports.onNewReaction = (reaction) => {
+ if (reaction.users.array().length <= 1) return; // was me TODO
+ reactionsPipe.put(reaction);
+};
+
+// create an iterator that returns promises to await new reactions
+// the Promise result is the reaction name
+module.exports.awaitReactions = (message, emoji, timeout=REACTION_TIMEOUT) => {
+ const pipeOut = new Channel();
+ let reactions = [];
+ // stop listening after timeout
+ setTimeout(() => {
+ pipeOut.close();
+ Promise.map(reactions, (r) => r.remove());
+ }, timeout*1000);
+
+ // async in background
+ Promise.each(emoji, async (em) =>
+ reactions.push(await message.react(em)));
+
+ let reaction;
+ reactionsPipe.pipe(pipeOut);
+ return { next: async function() {
+ do {
+ reaction = await pipeOut.take();
+ if (reaction == Channel.DONE) return undefined;
+ } while (reaction.message.id != message.id ||
+ emoji.indexOf(reaction.emoji.name) == -1);
+ return reaction.emoji.name;
+ } }
+};
+
+// opts: object with key=emoji, value=func
+module.exports.reactionButtons = async (msg, opts) => {
+ if (msg.reactions.size > 0) return; // already added
+ const reactionWaiter = util.awaitReactions(msg, Object.keys(opts));
+ while (true) {
+ const rmoji = await reactionWaiter.next();
+ if (rmoji == undefined) break; // timeout
+ if (opts[rmoji]) await opts[rmoji]();
+ }
+}
+
+// respond or say text or embed
+module.exports.respond = async (msg, data, response) => {
+ if (response == undefined) {
+ if (typeof data === "string") response = await msg.say(data);
+ else response = await msg.embed(data);
+ } else {
+ if (typeof data === "string") {
+ if (data != response.content) response = await response.edit(data);
+ } else {
+ if (response.embeds.length == 0 ||
+ new Date(response.embeds[0].createdTimestamp).getTime()
+ != data.timestamp.getTime()) {
+ // TODO how2 edit embed properly?!
+ response = await msg.editResponse(response, {
+ type: "plain",
+ content: "",
+ options: { embed: data }
+ });
+ }
+ }
+ }
+ return response;
+}
+
+// array split generator
+module.exports.paginate = function* chunks(arr, pagesize) {
+ for (let c=0, len=arr.length; c<len; c+=pagesize)
+ yield arr.slice(c, c+pagesize);
+}
+
+// return ign, or associated ign, or undefined
+module.exports.ignForUser = async (name, user_token) => {
+ // "?" is not accepted as user input, but the default for empty args
+ if (name != "?") return name;
+ return await api.getUser(user_token);
+}
diff --git a/views/guild.js b/views/guild.js
new file mode 100644
index 0000000..15863b0
--- /dev/null
+++ b/views/guild.js
@@ -0,0 +1,35 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const View = require("./view"),
+ GuildMemberView = require("./guild_member"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ Promise = require("bluebird"),
+ oneLine = require("common-tags").oneLine;
+
+const GuildOverviewView = module.exports;
+
+// match detail view
+module.exports = class extends View {
+ text(members) {
+ // TODO remove when API supports server side sort
+ return members.sort((m1, m2) => m1.fame < m2.fame).map((m) =>
+ new GuildMemberView().text(m)).join("\n");
+ }
+
+ async embed(guild) {
+ const embed = util.vainsocialEmbed(`${guild.name} - ${guild.shard_id}`,
+ "", "vainsocial-guild-view")
+ .setDescription(await this.text(guild.members));
+ return embed;
+ }
+
+ async respond(guild, extra="") {
+ this.response = await util.respond(this.msg,
+ await this.embed(guild), this.response);
+ return this.response;
+ }
+}
diff --git a/views/guild_create.js b/views/guild_create.js
new file mode 100644
index 0000000..38720e1
--- /dev/null
+++ b/views/guild_create.js
@@ -0,0 +1,49 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const emoji = require("discord-emoji"),
+ View = require("./view"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const GuildCreateView = module.exports;
+
+// match detail view
+module.exports = class extends View {
+ constructor(msg, user_token) {
+ super(msg);
+ this.user_token = user_token;
+ }
+
+ async text() {
+ return `Guild created. You can now use ${util.usg(this.msg, "vgadd ign1 ign2 ignN")} to add members to your Guild.`;
+ }
+
+ async help() {
+ return `*${emoji.symbols.information_source} or ${util.usg(this.msg, "vgview")} to view your Guild*`
+ }
+
+ async buttons() {
+ let reactions = {};
+ reactions[emoji.symbols.information_source] = async () => {
+ util.trackAction(this.msg, "reaction-guildview");
+ await new GuildOverviewView(this.msg).respond(this.user_token);
+ };
+ return reactions;
+ }
+
+ // TODO move to super class
+ async respond() {
+ this.response = await util.respond(this.msg,
+ await this.text() + "\n" + await this.help(), this.response);
+ if (!this.hasButtons) {
+ await util.reactionButtons(this.response,
+ await this.buttons());
+ this.hasButtons = true;
+ }
+ return this.response;
+ };
+}
diff --git a/views/guild_member.js b/views/guild_member.js
new file mode 100644
index 0000000..d75ffe4
--- /dev/null
+++ b/views/guild_member.js
@@ -0,0 +1,35 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const View = require("./view"),
+ PlayerView = require("./player"),
+ MatchesView = require("./matches"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const GuildMemberView = module.exports;
+
+// combined fame + profile + last match
+module.exports = class extends View {
+ text(member) {
+ return `${member.player.name} | ${member.status} | ${member.fame} VS Fame`;
+ }
+
+ async embed(member, player, matches) {
+ const embed = util.vainsocialEmbed(`${member.player.name} - ${member.player.shard_id}`,
+ "", "vainsocial-guild-memberview")
+ .addField("Guild Profile", await this.text(member))
+ .addField("Player Profile", await new PlayerView().text(player))
+ .addField("Last Match", await new MatchesView().text(matches[0]));
+ return embed;
+ }
+
+ async respond(member, player, matches) {
+ this.response = await util.respond(this.msg,
+ await this.embed(member, player, matches), this.response);
+ return this.response;
+ }
+}
diff --git a/views/guild_progress.js b/views/guild_progress.js
new file mode 100644
index 0000000..b3fdeb3
--- /dev/null
+++ b/views/guild_progress.js
@@ -0,0 +1,30 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const View = require("./view"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const GuildMembersProgressView = module.exports;
+
+module.exports = class extends View {
+ constructor(msg, players) {
+ super(msg);
+ this.players = players;
+ }
+
+ // players: obj, key=ign, value=progress
+ async text(players) {
+ return Object.entries(players).map((tuple) =>
+ `${tuple[0]}: ${tuple[1]}`).join("\n");
+ }
+
+ async respond(extra="") {
+ this.response = await util.respond(this.msg,
+ await this.text(this.players) + "\n" + extra, this.response);
+ return this.response;
+ };
+}
diff --git a/views/guild_update.js b/views/guild_update.js
new file mode 100644
index 0000000..cb32dc3
--- /dev/null
+++ b/views/guild_update.js
@@ -0,0 +1,34 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const View = require("./view"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const GuildAddView = module.exports;
+
+// match detail view
+module.exports = class extends View {
+ constructor(msg, players) {
+ super(msg);
+ this.players = players;
+ }
+
+ // players: obj, key=ign, value=player
+ async text(players) {
+ return Object.entries(players).map((tuple) =>
+ (tuple[1] == undefined)?
+ `Loading ${tuple[0]}…`
+ : `Loaded ${tuple[0]}.`
+ ).join("\n");
+ }
+
+ async respond() {
+ this.response = await util.respond(this.msg,
+ await this.text(this.players), this.response);
+ return this.response;
+ };
+}
diff --git a/views/match.js b/views/match.js
new file mode 100644
index 0000000..2dc55ec
--- /dev/null
+++ b/views/match.js
@@ -0,0 +1,53 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const View = require("./view"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const PREVIEW = process.env.PREVIEW != "false",
+ ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/");
+
+const MatchView = module.exports;
+
+// match detail view
+module.exports = class extends View {
+ // return [[title, text], …] for rosters
+ async text(match) {
+ let resps = [];
+ for(let roster of match.rosters) {
+ let winstr = "Won";
+ if (!roster.winner) winstr = "Lost";
+ let rosterstr = `${roster.side} - \`${roster.hero_kills}\` Kills - ${winstr}`;
+ let teamstr = "";
+ for(let participant of roster.participants) {
+ const hero = await api.mapActor(participant.actor),
+ emojiScore = strings.emojifyScore(participant.stats.impact_score);
+ teamstr += `
+\`${hero}\`, [${participant.player.name}](${ROOTURL}player/${participant.player.name}${util.track("match-detail")}) \`T${Math.floor(participant.skill_tier/3+1)}\` | \`${participant.stats.kills}/${participant.stats.deaths}/${participant.stats.assists}\`, \`${Math.floor(participant.stats.farm)}\`, Score ${emojiScore} \`${Math.floor(100 * participant.stats.impact_score)}%\``;
+ }
+ resps.push([rosterstr, teamstr]);
+ }
+ return resps;
+ }
+
+ async embed(match) {
+ let embed = util.vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`,
+ "matches/" + match.api_id , "vainsocial-match")
+ .setTimestamp(new Date(match.created_at));
+ (await this.text(match)).forEach(([title, text]) => {
+ embed.addField(title, text, true);
+ });
+ return embed;
+ };
+
+ async respond(matchid) {
+ const match = await api.getMatch(matchid);
+ this.response = await util.respond(this.msg,
+ await this.embed(match), this.response);
+ return this.response;
+ };
+}
diff --git a/views/matches.js b/views/matches.js
new file mode 100644
index 0000000..a79654b
--- /dev/null
+++ b/views/matches.js
@@ -0,0 +1,80 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const emoji = require("discord-emoji"),
+ Promise = require("bluebird"),
+ View = require("./view"),
+ MatchView = require("./match"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings");
+
+const MATCH_HISTORY_LEN = parseInt(process.env.MATCH_HISTORY_LEN) || 3;
+
+const MatchesView = module.exports;
+
+// match detail view
+module.exports = class extends View {
+ constructor(msg, ign) {
+ super(msg);
+ this.ign = ign;
+ }
+
+ async text(participant) {
+ let winstr = "Won",
+ hero = await api.mapActor(participant.actor),
+ game_mode = await api.mapGameMode(participant.game_mode_id),
+ emojiScore = strings.emojifyScore(participant.stats.impact_score);
+ if (!participant.winner) winstr = "Lost";
+ return `
+${winstr} ${game_mode} with \`${hero}\`
+KDA, CS | \`${participant.stats.kills}/${participant.stats.deaths}/${participant.stats.assists}\`, \`${Math.round(participant.stats.farm)}\`
+Kill Participation | \`${Math.floor(100 * participant.stats.kill_participation)}%\`
+Score | ${emojiScore} \`${Math.floor(100 * participant.stats.impact_score)}%\`
+`;
+ }
+
+ async embed(matches) {
+ const matchesPart = matches.slice(0, MATCH_HISTORY_LEN);
+
+ // build embed
+ let embed = util.vainsocialEmbed(this.ign,
+ "player/" + this.ign, "vainsocial-matches")
+ .setDescription(`Last ${matchesPart.length} casual and ranked matches.\n`
+ + await this.help())
+ .setTimestamp(new Date(matchesPart[0].created_at));
+ await Promise.each(matchesPart, async (match, idx) =>
+ embed.addField(`Match ${idx + 1}`, await this.text(match))
+ );
+ return embed;
+ }
+
+ async help() {
+ return `*${emoji.symbols["1234"]} or ${util.usg(this.msg, "vm " + this.ign + " number")} for details*`
+ }
+
+ async buttons(matches) {
+ const matchesPart = matches.slice(0, MATCH_HISTORY_LEN);
+
+ let reactions = {};
+ matchesPart.forEach((m, idx) =>
+ reactions[strings.emojiCount[idx]] = async () => {
+ util.trackAction(this.msg, "reaction-match", m.match_api_id);
+ await new MatchView(this.msg).respond(m.match_api_id);
+ });
+ return reactions;
+ }
+
+ async respond() {
+ const matches = await api.getMatches(this.ign);
+ this.response = await util.respond(this.msg,
+ await this.embed(matches), this.response);
+ if (!this.hasButtons) {
+ await util.reactionButtons(this.response,
+ await this.buttons(matches), this.msg);
+ this.hasButtons = true;
+ }
+ return this.response;
+ }
+}
diff --git a/views/player.js b/views/player.js
new file mode 100644
index 0000000..a481c69
--- /dev/null
+++ b/views/player.js
@@ -0,0 +1,105 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const emoji = require("discord-emoji"),
+ View = require("./view"),
+ MatchView = require("./match"),
+ MatchesView = require("./matches"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const PREVIEW = process.env.PREVIEW != "false",
+ ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/");
+
+const PlayerView = module.exports;
+
+// player profile
+module.exports = class extends View {
+ constructor(msg, ign) {
+ super(msg);
+ this.ign = ign;
+ }
+
+ async text(player) {
+ const stats = oneLine`
+ Win Rate | \`${Math.round(100 *
+ player.currentSeries.reduce((t, s) => t + s.wins, 0) /
+ player.currentSeries.reduce((t, s) => t + s.played, 0)
+ )}%\`
+ `,
+ total_kda = oneLine`
+ Total KDA | \`${player.stats.kills}\` / \`${player.stats.deaths}\` / \`${player.stats.assists}\`
+ `;
+ let best_hero = "",
+ picks = "";
+ if (player.best_hero.length > 0)
+ best_hero = oneLine`
+ Best | \`${player.best_hero[0].name}\`
+ `;
+ if (player.picks.length > 0) {
+ const hero = await api.mapActor(player.picks[0].actor);
+ picks = oneLine`Favorite | \`${hero}\`, \`${player.picks[0].hero_pick} picks\``;
+ }
+ return `
+${stats}
+${total_kda}
+${best_hero}
+${picks}
+ `;
+ }
+
+ async embed(player, matches) {
+ return util.vainsocialEmbed(`${player.name} - ${player.shard_id}`, "player/" + player.name, "vainsocial-user")
+ .setThumbnail(ROOTURL + "images/game/skill_tiers/" +
+ matches[0].skill_tier + ".png")
+ .setDescription("")
+ .addField(strings.profile, await this.text(player), true)
+ .addField(strings.lastMatch, await new MatchesView().text(matches[0]) +
+ "\n" + await this.help(), true)
+ .setTimestamp(new Date(matches[0].created_at));
+ };
+
+ async help() {
+ return oneLine`
+*${emoji.symbols.information_source} or ${util.usg(this.msg, "vm " + this.ign)} for detail,
+${emoji.symbols["1234"]} or ${util.usg(this.msg, "vh " + this.ign)} for more*`;
+ }
+
+ async buttons(player, matches) {
+ let reactions = {};
+ reactions[emoji.symbols.information_source] = async () => {
+ util.trackAction(this.msg, "reaction-match", player.name);
+ await new MatchView(this.msg).respond(matches[0].match_api_id);
+ };
+ reactions[emoji.symbols["1234"]] = async () => {
+ util.trackAction(this.msg, "reaction-matches", player.name);
+ await new MatchesView(this.msg, player.name).respond();
+ };
+ return reactions;
+ }
+
+ async respond() {
+ let player, matches;
+ try {
+ [player, matches] = await Promise.all([
+ api.getPlayer(this.ign),
+ api.getMatches(this.ign)
+ ]);
+ } catch (err) {
+ this.response = await util.respond(this.msg,
+ strings.loading(this.ign), this.response);
+ return this.response;
+ }
+ this.response = await util.respond(this.msg,
+ await this.embed(player, matches), this.response);
+ if (!this.hasButtons) {
+ await util.reactionButtons(this.response,
+ await this.buttons(player, matches));
+ this.hasButtons = true;
+ }
+ return this.response;
+ }
+}
diff --git a/views/register.js b/views/register.js
new file mode 100644
index 0000000..a636e84
--- /dev/null
+++ b/views/register.js
@@ -0,0 +1,46 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const emoji = require("discord-emoji"),
+ View = require("./view"),
+ PlayerView = require("./player"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const RegisterView = module.exports;
+
+// user register view
+module.exports = class extends View {
+ constructor(msg, ign) {
+ super(msg);
+ this.ign = ign;
+ }
+
+ async text() {
+ return `You are now registered at VainSocial, ${this.msg.author.toString()}.`;
+ }
+ async help() {
+ return `*${emoji.symbols.repeat} or ${util.usg(this.msg, "v")} to view your profile, ${util.usg(this.msg, "vgcreate")} to create a Guild*`
+ }
+ async buttons() {
+ let reactions = {};
+ reactions[emoji.symbols.repeat] = async () => {
+ util.trackAction(this.msg, "reaction-player");
+ await new PlayerView(this.msg, this.ign).respond();
+ };
+ return reactions;
+ }
+ async respond() {
+ this.response = await util.respond(this.msg,
+ await this.text() + "\n" + await this.help(), this.response);
+ if (!this.hasButtons) {
+ await util.reactionButtons(this.response,
+ await this.buttons());
+ this.hasButtons = true;
+ }
+ return this.response;
+ };
+}
diff --git a/views/simple.js b/views/simple.js
new file mode 100644
index 0000000..5e8701a
--- /dev/null
+++ b/views/simple.js
@@ -0,0 +1,24 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const View = require("./view"),
+ util = require("../util"),
+ api = require("../api"),
+ strings = require("../strings"),
+ oneLine = require("common-tags").oneLine;
+
+const SimpleView = module.exports;
+
+// just respond with a text
+module.exports = class extends View {
+ async text(txt) {
+ return txt;
+ }
+
+ async respond(text) {
+ this.response = await util.respond(this.msg,
+ await this.text(text), this.response);
+ return this.response;
+ }
+}
diff --git a/views/view.js b/views/view.js
new file mode 100644
index 0000000..771e3db
--- /dev/null
+++ b/views/view.js
@@ -0,0 +1,47 @@
+#!/usr/bin/node
+/* jshint esnext:true */
+"use strict";
+
+const util = require("../util");
+
+module.exports = class {
+ constructor(msg) {
+ // command message
+ this.msg = msg;
+ // response (d'oh)
+ this.response = undefined;
+ this.hasButtons = false;
+ }
+ static async text() {
+ // return the markdown or an array of [title, markdown]
+ return "";
+ }
+ async embed() {
+ // return an embed using `text()` and `help()`
+ return util.vainsocialEmbed("title", "url");
+ }
+ async help() {
+ // return explanation for buttons
+ return "";
+ }
+ async buttons() {
+ // return buttons pointing to different views
+ // key: emoji, value: async func
+ return {};
+ }
+ async respond() {
+ // reply with embed + buttons
+ this.response = await util.respond(this.msg,
+ await this.embed(), this.response);
+ if (!this.hasButtons) {
+ await util.reactionButtons(this.response, await this.buttons(), this.msg);
+ this.hasButtons = true;
+ }
+ return this.response;
+ }
+ async error(text) {
+ // reply with error
+ this.response = await util.respond(this.msg, text, this.response);
+ return this.response;
+ }
+};