summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorschneefux <schneefux+commit@schneefux.xyz>2017-05-08 23:15:33 +0200
committerschneefux <schneefux+commit@schneefux.xyz>2017-05-08 23:15:33 +0200
commit31b4764cd5233acb25f4d0027261fe83eaad6ec7 (patch)
tree75e7c33b8284d711fb9208154a1d8db8b931a0f3
parentf3aa08e72c6ffdb942ea06e21ac19e28b8cd5550 (diff)
downloaddiscordbot-31b4764cd5233acb25f4d0027261fe83eaad6ec7.tar.gz
discordbot-31b4764cd5233acb25f4d0027261fe83eaad6ec7.zip
rewrite the whole thing (wip)
-rw-r--r--api.js73
-rw-r--r--bot.js3
-rw-r--r--commands/vainsocial/about.js17
-rw-r--r--commands/vainsocial/guild_add.js58
-rw-r--r--commands/vainsocial/guild_create.js8
-rw-r--r--commands/vainsocial/guild_view.js18
-rw-r--r--commands/vainsocial/match.js21
-rw-r--r--commands/vainsocial/matches.js18
-rw-r--r--commands/vainsocial/me.js30
-rw-r--r--commands/vainsocial/user.js18
-rw-r--r--package.json1
-rw-r--r--responses.js300
-rw-r--r--strings.js48
-rw-r--r--util.js85
-rw-r--r--views/guild.js45
-rw-r--r--views/guild_add.js34
-rw-r--r--views/guild_create.js48
-rw-r--r--views/match.js58
-rw-r--r--views/matches.js81
-rw-r--r--views/player.js108
-rw-r--r--views/register.js41
-rw-r--r--views/view.js39
22 files changed, 695 insertions, 457 deletions
diff --git a/api.js b/api.js
index 735308b..d303cab 100644
--- a/api.js
+++ b/api.js
@@ -7,7 +7,9 @@ const request = require("request-promise"),
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",
@@ -16,7 +18,7 @@ let cache = cacheManager.caching({
const UPDATE_TIMEOUT = parseInt(process.env.UPDATE_TIMEOUT) || 60; // s
-const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bot/api",
+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";
@@ -31,15 +33,15 @@ const notif = webstomp.over(new WebSocket(API_WS_URL,
);
})();
-function getMap(url) {
- return request({
+module.exports.getMap = async (url) => {
+ return await request({
uri: API_MAP_URL + url,
json: true,
forever: true
});
}
-async function getFE(url, params={}, ttl=60, cachekey=undefined) {
+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 () => {
try {
@@ -57,7 +59,7 @@ async function getFE(url, params={}, ttl=60, cachekey=undefined) {
}
// send a POST and optionally bust cache
-async function postFE(url, params={}, cachekey=undefined) {
+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,
@@ -66,34 +68,30 @@ async function postFE(url, params={}, cachekey=undefined) {
});
}
-function postBE(url) {
- return request.post({
+module.exports.postBE = module.exports.backend = async (url) => {
+ return await request.post({
uri: API_BE_URL + url,
json: true,
forever: true
});
}
-module.exports.get = getFE;
-module.exports.post = postFE;
-module.exports.backend = postBE;
-
function subscribe(topic, channel) {
return notif.subscribe("/topic/" + topic, (msg) => {
channel.put(msg.body);
msg.ack();
- }, {"ack": "client"});
+ }, { ack: "client" });
}
// return id<->name mappings
-async function getMappings() {
+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 getMap(table)).map(
+ (await api.getMap(table)).map(
(map) => mapping[table][map["id"]] = map["name"])
}
),
@@ -101,7 +99,7 @@ async function getMappings() {
Promise.map(
["hero"], async (table) => {
mapping[table] = new Map();
- (await getMap(table)).map(
+ (await api.getMap(table)).map(
(map) => mapping[table][map["api_name"]] = map["name"])
}
)
@@ -111,14 +109,14 @@ async function getMappings() {
}
module.exports.mapGameMode = async (id) =>
- (await getMappings())["gamemode"][id];
+ (await api.getMappings())["gamemode"][id];
module.exports.mapActor = async (api_name) =>
- (await getMappings())["hero"][api_name];
+ (await api.getMappings())["hero"][api_name];
// return a set of IGN of supporters
module.exports.getGamers = async () =>
- (await getFE("/gamer", {}, 60 * 30)).map((gamer) => gamer.name);
+ (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
@@ -130,7 +128,7 @@ module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => {
setTimeout(() => channel.close(), timeout*1000);
let msg;
- return { next: async function () {
+ return { next: async () => {
do msg = await channel.take();
while([Channel.DONE, "search_fail", "search_success",
"stats_update", "matches_update"].indexOf(msg) == -1);
@@ -151,38 +149,49 @@ module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => {
// search an unknown player
module.exports.searchPlayer = (name) =>
- postBE("/player/" + name + "/search");
+ api.postBE("/player/" + name + "/search");
// update a known player
module.exports.updatePlayer = (name) =>
- postBE("/player/" + name + "/update");
+ api.postBE("/player/" + name + "/update");
// return player
module.exports.getPlayer = (name) =>
- getFE("/player/" + name, {}, 60, "player+" + name);
+ api.getFE("/player/" + name, {}, 60, "player+" + name);
// search or update a player
module.exports.upsearchPlayer = async (name) => {
- if (await module.exports.getPlayer(name) == undefined)
- await module.exports.searchPlayer(name);
- else await module.exports.updatePlayer(name);
+ if (await api.getPlayer(name) == undefined)
+ await api.searchPlayer(name);
+ else await api.updatePlayer(name);
+}
+
+// block until update is completely done
+module.exports.upsearchPlayerSync = async (name) => {
+ const waiter = await api.subscribeUpdates(name);
+ await api.upsearchPlayer(name);
+ let matchesUpdated = false, msg;
+ while (await Promise.any([
+ waiter.next(),
+ sleep(2000)
+ ]) != undefined);
}
// return matches
module.exports.getMatches = async (name) => {
- const data = await getFE("/player/" + name + "/matches/1.1.1.1", {},
+ const data = await api.getFE("/player/" + name + "/matches/1.1.1.1", {},
60 * 60, "matches+" + name);
if (data == undefined) return [];
return data[0].data;
}
// return single match
-module.exports.getMatch = (id) =>
- getFE("/match/" + id, {}, 60 * 60);
+module.exports.getMatch = async (id) =>
+ await api.getFE("/match/" + id, {}, 60 * 60);
// return a guild
module.exports.getGuild = (token) =>
- getFE("/guild", { user_token: token }, 60, "guild+" + token);
+ api.getFE("/guild", { user_token: token }, 60, "guild+" + token);
// TODO! cache guilds by guild id, not by user token
// add user to guild
@@ -199,7 +208,7 @@ module.exports.addToGuild = async (token, member) => {
module.exports.calculateGuild = async (id, token) => {
const channel = new Channel(),
subscription = subscribe("global", channel);
- await module.exports.backend("/team/" + id + "/crunch");
+ await api.backend("/team/" + id + "/crunch");
// stop updates after timeout
setTimeout(() => channel.close(), UPDATE_TIMEOUT*1000);
@@ -215,7 +224,7 @@ module.exports.calculateGuild = async (id, token) => {
// store Discord ID <-> IGN
module.exports.setUser = async (token, name) => {
cache.del("user+" + token);
- await module.exports.post("/user", {
+ await api.post("/user", {
name: name,
user_token: token
});
@@ -223,4 +232,4 @@ module.exports.setUser = async (token, name) => {
// retrieve Discord ID -> IGN
module.exports.getUser = async (token) =>
- (await getFE("/user", { user_token: token }, 60, "user+" + token)).name;
+ (await api.getFE("/user", { user_token: token }, 60, "user+" + token)).name;
diff --git a/bot.js b/bot.js
index 5968392..1efa1e4 100644
--- a/bot.js
+++ b/bot.js
@@ -17,7 +17,6 @@ const sqlite = require("sqlite"),
invite: "https://discord.gg/txTchJY",
unknownCommandResponse: false
}),
- responses = require("./responses"),
util = require("./util");
const DISCORD_TOKEN = process.env.DISCORD_TOKEN,
@@ -46,7 +45,7 @@ client
.on("debug", logger.info)
.on("ready", () => {
logger.info(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`);
- responses.rotateGameStatus(client);
+ util.rotateGameStatus(client);
})
.on('disconnect', () => { logger.warn('Disconnected!'); })
.on('reconnecting', () => { logger.warn('Reconnecting...'); })
diff --git a/commands/vainsocial/about.js b/commands/vainsocial/about.js
index 22d7fb4..c452d87 100644
--- a/commands/vainsocial/about.js
+++ b/commands/vainsocial/about.js
@@ -4,7 +4,7 @@
const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
- responses = require("../../responses");
+ util = require("../../util");
module.exports = class ShowAboutCommand extends Commando.Command {
constructor(client) {
@@ -16,6 +16,19 @@ module.exports = class ShowAboutCommand extends Commando.Command {
});
}
async run(msg) {
- await responses.showAbout(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
index 531d3dd..0021360 100644
--- a/commands/vainsocial/guild_add.js
+++ b/commands/vainsocial/guild_add.js
@@ -6,7 +6,8 @@ const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
Promise = require("bluebird"),
api = require("../../api"),
- util = require("../../util");
+ util = require("../../util"),
+ GuildAddView = require("../../views/guild_add");
module.exports = class AddGuildMemberCommand extends Commando.Command {
constructor(client) {
@@ -26,48 +27,21 @@ Register IGNs to your Guild.
// register a VainSocial Guild to a Discord account
async run(msg, args) {
util.trackAction(msg, "vainsocial-guild-add");
- let response, total_progress = "";
-
- async function progress(part, final=false) {
- response = await util.respond(msg,
- total_progress + "\n" + part, response);
- if (final) total_progress += "\n" + part;
- }
-
- // for each IGN
- await Promise.each(args, async (user) => {
- await progress(`Adding ${user}…\n`);
- // make sure player exists in db
- let player = await api.getPlayer(user);
- if (player == undefined) {
- await progress(`Loading ${user}'s data into VainSocial…`);
- // if not, wait for backend to fetch name <-> api_id
- // - not interested in match history (hence no update),
- const waiter = api.subscribeUpdates(user);
- await api.searchPlayer(user);
-
- let success = false, notif;
- // wait until search success
- while (true) {
- notif = await waiter.next();
- if (notif == "stats_update") break;
- if (notif == undefined) {
- // give up
- await progress(`Ooops! Could not find ${user}.`, true);
- return;
- }
- }
- } else {
- await api.updatePlayer(user);
+ let playersData = {};
+ const playersWaiters = args.map((name) => api.subscribeUpdates(name)),
+ guildAddView = new GuildAddView(msg, playersData);
+ // create waiter dict & data dict
+ await Promise.map(playersWaiters, async (waiter, idx) => {
+ await api.upsearchPlayer(args[idx]);
+ let success = false;
+ while (await waiter.next() != undefined) {
+ playersData[args[idx]] = await api.getPlayer(args[idx]);
+ await guildAddView.respond();
+ success = true;
+ }
+ if (success) {
+ await api.addToGuild(msg.author.id, args[idx]);
}
-
- // all good, register to self guild
- await api.addToGuild(msg.author.id, user);
- await progress(`Added ${user}.`, true);
});
- await progress(oneLine`
-Guild members have been added.
-You can now use ${util.usg(msg, "vgview")} for an overview.
-`, true);
}
};
diff --git a/commands/vainsocial/guild_create.js b/commands/vainsocial/guild_create.js
index 1e5a368..ad8d0e7 100644
--- a/commands/vainsocial/guild_create.js
+++ b/commands/vainsocial/guild_create.js
@@ -4,8 +4,9 @@
const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
+ util = require("../../util"),
api = require("../../api"),
- util = require("../../util");
+ GuildCreateView = require("../../views/guild_create");
module.exports = class RegisterGuildCommand extends Commando.Command {
constructor(client) {
@@ -47,14 +48,13 @@ Create a Guild with your VainSocial profile as leader.
// register a VainSocial Guild to a Discord account
async run(msg, args) {
util.trackAction(msg, "vainsocial-guild-create");
+ // TODO error handling
await api.post("/guild", {
shard_id: args.region,
name: args.name,
identifier: args.tag,
user_token: msg.author.id
});
- await msg.reply(oneLine`
-You can now use ${util.usg(msg, "vgadd")} to add members to your Guild.
-`);
+ await new GuildCreateView(msg, msg.author.id).respond();
}
};
diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js
index 835b717..7deb66f 100644
--- a/commands/vainsocial/guild_view.js
+++ b/commands/vainsocial/guild_view.js
@@ -4,8 +4,8 @@
const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
- api = require("../../api"),
- util = require("../../util");
+ util = require("../../util"),
+ GuildOverviewView = require("../../views/guild");
module.exports = class ViewGuildCommand extends Commando.Command {
constructor(client) {
@@ -33,18 +33,6 @@ Show a summary of your Guild.
// show Guild details
async run(msg, args) {
util.trackAction(msg, "vainsocial-guild-view");
- // get this user's guild
- const guild = await api.getGuild(msg.author.id);
- if (guild == undefined) {
- await msg.reply("You are not registered in any guilds.");
- return;
- }
- // build response
- let response = "";
- response += `${guild.name} (${guild.shard_id})\n`;
- guild.members.forEach((member) => {
- response += `${member.player.name}: ${member.fame} VainSocial Fame\n`;
- });
- await msg.say(response);
+ await new GuildOverviewView(msg, msg.author.id).respond();
}
};
diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js
index ba24baf..c3d2906 100644
--- a/commands/vainsocial/match.js
+++ b/commands/vainsocial/match.js
@@ -4,7 +4,10 @@
const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
- responses = require("../../responses");
+ util = require("../../util"),
+ api = require("../../api"),
+ strings = require("../../strings"),
+ MatchView = require("../../views/match");
module.exports = class ShowMatchCommand extends Commando.Command {
constructor(client) {
@@ -38,6 +41,20 @@ module.exports = class ShowMatchCommand extends Commando.Command {
});
}
async run(msg, args) {
- await responses.showMatch(msg, args);
+ util.trackAction(msg, "vainsocial-match", args.name);
+ const ign = await util.ignForUser(args.name, msg.author.id);
+ if (ign == undefined) return await msg.say(strings.unknown(msg));
+
+ const participations = await api.getMatches(ign);
+ if (args.number > participations.length)
+ return await msg.say(strings.tooFewMatches(ign));
+ const matchView = new MatchView(msg,
+ participations[args.number-1].match_api_id);
+ // wait for BE update
+ const waiter = api.subscribeUpdates(ign);
+ await api.upsearchPlayer(ign);
+
+ do await matchView.respond();
+ while (await waiter.next() != undefined);
}
};
diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js
index 7eaa513..0197b8b 100644
--- a/commands/vainsocial/matches.js
+++ b/commands/vainsocial/matches.js
@@ -4,7 +4,10 @@
const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
- responses = require("../../responses");
+ util = require("../../util"),
+ api = require("../../api"),
+ strings = require("../../strings"),
+ MatchesView = require("../../views/matches");
module.exports = class ShowMatchesCommand extends Commando.Command {
constructor(client) {
@@ -29,6 +32,17 @@ module.exports = class ShowMatchesCommand extends Commando.Command {
});
}
async run(msg, args) {
- await responses.showMatches(msg, args);
+ util.trackAction(msg, "vainsocial-matches", args.name);
+ const ign = await util.ignForUser(args.name, msg.author.id);
+ if (ign == undefined) return await strings.unknown(msg);
+
+ // peek
+ const matchesView = new MatchesView(msg, ign);
+ // wait for BE update
+ const waiter = api.subscribeUpdates(ign);
+ await api.upsearchPlayer(ign);
+
+ do await matchesView.respond();
+ while (await waiter.next() != undefined);
}
};
diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js
index 76619bb..e17afea 100644
--- a/commands/vainsocial/me.js
+++ b/commands/vainsocial/me.js
@@ -32,31 +32,9 @@ Store your in game name for quicker access to other commands and for Guild manag
}
// register a Discord account at VainSocial
async run(msg, args) {
- const ign = args.name;
- util.trackAction(msg, "vainsocial-me", ign);
- const player = await api.getPlayer(ign);
- // TODO refactor and build a blocking function via processor
- // TODO respond loading
- if (player == undefined) {
- const waiter = api.subscribeUpdates(ign);
- await api.searchPlayer(ign);
-
- let success = false, notif;
- // wait until search success
- while (true) {
- notif = await waiter.next();
- if (notif == "stats_update") break;
- if (notif == undefined) {
- // give up
- await progress(`Ooops! Could not find ${user}.`, true);
- return;
- }
- }
- }
- await api.setUser(msg.author.id, ign);
- await msg.reply(oneLine`
-You are now able to use ${util.usg(msg, "v")} to access your profile faster.
-You now create a Guild with ${util.usg(msg, "vgcreate")}.
-`);
+ util.trackAction(msg, "vainsocial-me", args.name);
+ await api.upsearchPlayer(ign);
+ await api.subscribeUpdates(ign).next();
+ await new RegisterView(msg).respond();
}
};
diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js
index 43aecac..bfcc1a0 100644
--- a/commands/vainsocial/user.js
+++ b/commands/vainsocial/user.js
@@ -4,7 +4,11 @@
const Commando = require("discord.js-commando"),
oneLine = require("common-tags").oneLine,
- responses = require("../../responses");
+ 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) {
@@ -31,6 +35,16 @@ Display VainSocial lifetime statistics from Vainglory
});
}
async run(msg, args) {
- await responses.showUser(msg, args);
+ util.trackAction(msg, "vainsocial-user", args.name);
+ const ign = await util.ignForUser(args.name, msg.author.id);
+ if (ign == undefined) return await msg.say(strings.unknown(msg));
+
+ const playerView = new PlayerView(msg, ign);
+ // wait for BE update
+ const waiter = api.subscribeUpdates(ign);
+ await api.upsearchPlayer(ign);
+
+ do await playerView.respond();
+ while (await waiter.next() != undefined);
}
};
diff --git a/package.json b/package.json
index b5962a4..fc94066 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"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",
diff --git a/responses.js b/responses.js
index 32ce136..e69de29 100644
--- a/responses.js
+++ b/responses.js
@@ -1,300 +0,0 @@
-#!/usr/bin/node
-/* jshint esnext:true */
-"use strict";
-
-const Commando = require("discord.js-commando"),
- Promise = require("bluebird"),
- emoji = require("discord-emoji"),
- oneLine = require("common-tags").oneLine,
- util = require("./util"),
- api = require("./api");
-
-const PREVIEW = process.env.PREVIEW != "false",
- MATCH_HISTORY_LEN = parseInt(process.env.MATCH_HISTORY_LEN) || 3,
- IGN_ROTATE_TIMEOUT = parseInt(process.env.IGN_ROTATE_TIMEOUT) || 300,
- ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/");
-
-// based on impact score float, return an Emoji
-function 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;
-}
-
-// return a match overview string
-async function formatMatch(participant) {
- let winstr = "Won",
- hero = await api.mapActor(participant.actor),
- game_mode = await api.mapGameMode(participant.game_mode_id);
- 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 | ${emojifyScore(participant.stats.impact_score)} \`${Math.floor(100 * participant.stats.impact_score)}%\`
-`;
-}
-
-// return [[title, text], …] for rosters
-async function formatMatchDetail(match) {
- let strings = [];
- 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);
- 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 ${emojifyScore(participant.stats.impact_score)} \`${Math.floor(100 * participant.stats.impact_score)}%\``;
- }
- strings.push([rosterstr, teamstr]);
- }
- return strings;
-}
-
-// return a profile string
-async function formatPlayer(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}
- `;
-}
-
-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);
- })();
-}
-
-// about
-module.exports.showAbout = async (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)
- );
-}
-
-// show player profile and last match
-module.exports.showUser = async (msg, args) => {
- let responded = false,
- response,
- ign = args.name,
- reactionsAdded = false;
- util.trackAction(msg, "vainsocial-user", ign);
-
- // shorthand
- // "?" is not accepted as user input, but the default for empty
- if (ign == "?") {
- ign = await api.getUser(msg.author.id);
- if (ign == undefined) {
- await msg.reply(util.formatSorryUnknown(msg));
- return;
- }
- }
-
- const waiter = api.subscribeUpdates(ign);
- await api.upsearchPlayer(ign);
- do {
- const [player, matches] = await Promise.all([
- api.getPlayer(ign),
- api.getMatches(ign)
- ]);
- if (player == undefined || matches.length == 0) {
- response = await util.respond(msg,
- "Loading your data…", response);
- continue;
- }
-
- const moreHelp = oneLine`
-*${emoji.symbols.information_source} or ${util.usg(msg, "vm " + ign)} for detail,
-${emoji.symbols["1234"]} or ${util.usg(msg, "vh " + ign)} for more*`
-
- const embed = util.vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign, "vainsocial-user")
- .setThumbnail(ROOTURL + "images/game/skill_tiers/" +
- matches[0].skill_tier + ".png")
- .setDescription("")
- .addField("Profile", await formatPlayer(player), true)
- .addField("Last match", await formatMatch(matches[0]) + moreHelp, true)
- .setTimestamp(new Date(matches[0].created_at));
- response = await util.respond(msg, embed, response);
-
- if (!reactionsAdded) {
- // build reaction buttton bar
- reactionsAdded = true;
- let reactionWaiter = util.awaitReactions(response,
- [emoji.symbols.information_source, emoji.symbols["1234"]]);
- (async () => {
- while (true) {
- let rmoji = await reactionWaiter.next();
- if (rmoji == undefined) break; // timeout
- if (rmoji == emoji.symbols.information_source) {
- util.trackAction(msg, "reaction-match", ign);
- await respondMatch(msg, ign, matches[0].match_api_id);
- }
- if (rmoji == emoji.symbols["1234"]) {
- util.trackAction(msg, "reaction-matches", ign);
- await respondMatches(msg, ign);
- }
- }
- })(); // async in background
- }
- } while (await waiter.next() != undefined);
- if (!reactionsAdded)
- await util.respond(msg, `Could not find \`${ign}\`.`, response);
-}
-
-// show match in detail
-async function respondMatch(msg, ign, id, response=undefined) {
- const match = await api.getMatch(id);
-
- let embed = util.vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`,
- "player/" + ign + "/match/" + id, "vainsocial-match")
- .setTimestamp(new Date(match.created_at));
- (await formatMatchDetail(match)).forEach(([title, text]) => {
- embed.addField(title, text, true);
- });
- return await util.respond(msg, embed, response);
-}
-
-module.exports.showMatch = async (msg, args) => {
- const index = args.number;
- let responded = false,
- ign = args.name,
- response;
- util.trackAction(msg, "vainsocial-match", ign);
-
- // shorthand
- if (ign == "?") {
- ign = await api.getUser(msg.author.id);
- if (ign == undefined) {
- await msg.reply(util.formatSorryUnknown(msg));
- return;
- }
- }
-
- const waiter = await api.subscribeUpdates(ign);
- await api.upsearchPlayer(ign);
- do {
- const matches = await api.getMatches(ign);
- if (index > matches.length) {
- response = await util.respond(msg, "Not enough matches yet.",
- response);
- continue;
- }
- const id = matches[index - 1].match_api_id;
- response = await respondMatch(msg, ign, id, response);
- responded = true;
- } while (await waiter.next() != undefined);
- if (!responded)
- await util.respond(msg, `Could not find \`${ign}\`.`, response);
-}
-
-// show match history
-async function respondMatches(msg, ign, response=undefined) {
- const count = [ 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
- ];
- const match_data = await api.getMatches(ign),
- matches = match_data.slice(0, MATCH_HISTORY_LEN),
- matches_num = matches.length;
-
- // not enough data
- if (matches_num == 0) return await util.respond(msg,
- "No match history yet.", response);
-
- // build embed
- let embed = util.vainsocialEmbed(ign, "player/" + ign, "vainsocial-matches")
- .setDescription(`
-Last ${matches_num} casual and ranked matches.
-*${emoji.symbols["1234"]} or ${util.usg(msg, "vm " + ign + " number")} for details*
- `)
- .setTimestamp(new Date(matches[0].created_at));
- await Promise.each(matches, async (match, idx) =>
- embed.addField(`Match ${idx + 1}`, await formatMatch(match))
- );
- response = await util.respond(msg, embed, response);
-
- // reaction button bar
- const reactionWaiter = util.awaitReactions(response,
- count.slice(0, matches_num));
- (async () => {
- while (true) {
- let rmoji = await reactionWaiter.next();
- if (rmoji == undefined) break; // timeout
- let idx = count.indexOf(rmoji);
- await respondMatch(msg, ign, matches[idx].match_api_id);
- util.trackAction(msg, "reaction-match", ign);
- }
- })(); // async in background
- return response;
-}
-
-module.exports.showMatches = async (msg, args) => {
- let ign = args.name,
- responded = false,
- response;
- util.trackAction(msg, "vainsocial-matches", ign);
-
- // shorthand
- if (ign == "?") {
- ign = await api.getUser(msg.author.id);
- if (ign == undefined) {
- await msg.reply(util.formatSorryUnknown(msg));
- return;
- }
- }
-
- const waiter = await api.subscribeUpdates(ign);
- await api.upsearchPlayer(ign);
- do {
- response = await respondMatches(msg, ign, response);
- responded = true;
- } while (await waiter.next() != undefined);
- if (!responded)
- await util.respond(msg, `Could not find \`${ign}\`.`,
- response);
-}
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
index 0e09b7a..a20cbaa 100644
--- a/util.js
+++ b/util.js
@@ -4,11 +4,15 @@
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/");
@@ -19,12 +23,25 @@ module.exports.vainsocialEmbed = (title, link, command) => {
return new Discord.RichEmbed()
.setTitle(title)
.setColor("#55ADD3")
- .setURL(ROOTURL + link + module.exports.track(command))
+ .setURL(ROOTURL + link + util.track(command))
.setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png",
- ROOTURL + module.exports.track(command))
+ 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;
@@ -64,7 +81,7 @@ module.exports.awaitReactions = (message, emoji, timeout=REACTION_TIMEOUT) => {
// stop listening after timeout
setTimeout(() => {
pipeOut.close();
- Promise.map(reactions, async (r) => r.remove());
+ Promise.map(reactions, (r) => r.remove());
}, timeout*1000);
// async in background
@@ -73,46 +90,58 @@ module.exports.awaitReactions = (message, emoji, timeout=REACTION_TIMEOUT) => {
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;
- }
- }
+ 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);
- }
+ if (typeof data === "string") response = await msg.say(data);
+ else response = await msg.embed(data);
} else {
if (typeof data === "string") {
- if (data != response.content)
- await response.edit(data);
+ if (data != response.content) response = await response.edit(data);
} else {
- if (response.embeds.length ==0 ||
+ if (response.embeds.length == 0 ||
new Date(response.embeds[0].createdTimestamp).getTime()
- != data.timestamp.getTime())
+ != data.timestamp.getTime()) {
// TODO how2 edit embed properly?!
- await msg.editResponse(response, {
+ 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);
+}
-// tell the user that they need to store their name
-module.exports.formatSorryUnknown = (msg) =>
- `You're unknown to our service. Try ${module.exports.usg(msg, "help vme")}.`;
+// 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
+ (name != "?")? name : await api.getUser(user_token);
diff --git a/views/guild.js b/views/guild.js
new file mode 100644
index 0000000..1a4cb78
--- /dev/null
+++ b/views/guild.js
@@ -0,0 +1,45 @@
+#!/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 GuildOverviewView = module.exports;
+
+// match detail view
+module.exports = class extends View {
+ constructor(msg, user_token) {
+ super(msg);
+ this.user_token = user_token;
+ }
+
+ async text(members) {
+ // TODO remove when API supports order by fame
+ members = members.sort((m1, m2) => m1.fame < m2.fame);
+ return members.map((member) =>
+ `${member.player.name} ${member.fame}`).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() {
+ const guild = await api.getGuild(this.user_token);
+ if (guild == undefined) {
+ this.response = await util.respond(this.msg,
+ strings.notRegistered, this.response);
+ return this.response;
+ }
+ this.response = await util.respond(this.msg,
+ await this.embed(guild), this.response);
+ return this.response;
+ };
+}
diff --git a/views/guild_add.js b/views/guild_add.js
new file mode 100644
index 0000000..cb32dc3
--- /dev/null
+++ b/views/guild_add.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/guild_create.js b/views/guild_create.js
new file mode 100644
index 0000000..9720191
--- /dev/null
+++ b/views/guild_create.js
@@ -0,0 +1,48 @@
+#!/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 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.user_token).respond();
+ };
+ return reactions;
+ }
+
+ // TODO move to super class
+ async respond() {
+ this.response = await util.respond(this.msg,
+ await this.text(), this.response);
+ if (!this.hasButtons) {
+ await util.reactionButtons(this.response,
+ await this.buttons());
+ this.hasButtons = true;
+ }
+ return this.response;
+ };
+}
diff --git a/views/match.js b/views/match.js
new file mode 100644
index 0000000..8a04045
--- /dev/null
+++ b/views/match.js
@@ -0,0 +1,58 @@
+#!/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 {
+ constructor(msg, matchid) {
+ super(msg);
+ this.matchid = matchid;
+ }
+
+ // return [[title, text], …] for rosters
+ static 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() {
+ const match = await api.getMatch(this.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..279bf9c
--- /dev/null
+++ b/views/matches.js
@@ -0,0 +1,81 @@
+#!/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 () => {
+ console.log("react");
+ util.trackAction(this.msg, "reaction-match", m.match_api_id);
+ await new MatchView(this.msg, m.match_api_id).respond();
+ });
+ 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..ec5d38b
--- /dev/null
+++ b/views/player.js
@@ -0,0 +1,108 @@
+#!/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, matches[0].match_api_id).respond();
+ };
+ 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() {
+ const [player, matches] = await Promise.all([
+ api.getPlayer(this.ign),
+ api.getMatches(this.ign)
+ ]);
+ if (player == undefined) {
+ this.response = await util.respond(this.msg,
+ strings.loading(this.ign), this.response);
+ return this.response;
+ }
+ if (matches.length == 0) {
+ 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..0e5ff71
--- /dev/null
+++ b/views/register.js
@@ -0,0 +1,41 @@
+#!/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 RegisterView = module.exports;
+
+// user register view
+module.exports = class extends View {
+ async text() {
+ return `You are now registered at VainSocial, ${this.msg.author.mention}.`;
+ }
+ 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");
+ const ign = await util.ignForUser(undefined, this.msg.author.id);
+ await new PlayerView(this.msg, ign).respond();
+ };
+ return reactions;
+ }
+ async respond() {
+ await api.setUser(msg.author.id, ign);
+ this.response = await util.respond(this.msg,
+ await this.text(), 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/view.js b/views/view.js
new file mode 100644
index 0000000..3d3ae38
--- /dev/null
+++ b/views/view.js
@@ -0,0 +1,39 @@
+#!/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);
+ await util.reactionButtons(this.response, await this.buttons(), this.msg);
+ return this.response;
+ }
+};