From 31b4764cd5233acb25f4d0027261fe83eaad6ec7 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 8 May 2017 23:15:33 +0200 Subject: rewrite the whole thing (wip) --- api.js | 73 +++++---- bot.js | 3 +- commands/vainsocial/about.js | 17 +- commands/vainsocial/guild_add.js | 58 ++----- commands/vainsocial/guild_create.js | 8 +- commands/vainsocial/guild_view.js | 18 +-- commands/vainsocial/match.js | 21 ++- commands/vainsocial/matches.js | 18 ++- commands/vainsocial/me.js | 30 +--- commands/vainsocial/user.js | 18 ++- package.json | 1 + responses.js | 300 ------------------------------------ strings.js | 48 ++++++ util.js | 85 ++++++---- views/guild.js | 45 ++++++ views/guild_add.js | 34 ++++ views/guild_create.js | 48 ++++++ views/match.js | 58 +++++++ views/matches.js | 81 ++++++++++ views/player.js | 108 +++++++++++++ views/register.js | 41 +++++ views/view.js | 39 +++++ 22 files changed, 695 insertions(+), 457 deletions(-) create mode 100644 strings.js create mode 100644 views/guild.js create mode 100644 views/guild_add.js create mode 100644 views/guild_create.js create mode 100644 views/match.js create mode 100644 views/matches.js create mode 100644 views/player.js create mode 100644 views/register.js create mode 100644 views/view.js 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 - `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; + } +}; -- cgit v1.3.1