diff options
| author | schneefux <schneefux+commit@schneefux.xyz> | 2017-05-04 22:33:17 +0200 |
|---|---|---|
| committer | schneefux <schneefux+commit@schneefux.xyz> | 2017-05-04 22:33:17 +0200 |
| commit | 5045641b065bfea9a0db39dd4814b2cf2269e723 (patch) | |
| tree | 08f3a90d952fb5a5655a105437d0b9de6572a6a6 | |
| parent | 188e3997092a57be11574e5e121af5882dfb40ec (diff) | |
| download | discordbot-5045641b065bfea9a0db39dd4814b2cf2269e723.tar.gz discordbot-5045641b065bfea9a0db39dd4814b2cf2269e723.zip | |
fame support (wip)
| -rw-r--r-- | api.js | 25 | ||||
| -rw-r--r-- | bot.js | 6 | ||||
| -rw-r--r-- | commands/vainsocial/guild_add.js | 80 | ||||
| -rw-r--r-- | commands/vainsocial/guild_create.js | 66 | ||||
| -rw-r--r-- | commands/vainsocial/guild_view.js | 58 | ||||
| -rw-r--r-- | commands/vainsocial/me.js | 26 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | responses.js | 214 | ||||
| -rw-r--r-- | util.js | 118 |
9 files changed, 419 insertions, 175 deletions
@@ -2,7 +2,7 @@ /* jshint esnext:true */ "use strict"; -const request = require("request-promise-native"), +const request = require("request-promise"), Promise = require("bluebird"), WebSocket = require("ws"), webstomp = require("webstomp-client"), @@ -16,7 +16,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/bots/api", +const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/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"; @@ -113,14 +113,9 @@ module.exports.subscribeUpdates = function(name, timeout=UPDATE_TIMEOUT) { let msg; return { next: async function () { - if (this._first) { - this._first = false; - await postBE("/player/" + name + "/update"); - return true; - } do { msg = await channel.take(); - } while([Channel.DONE, "initial", "search_fail", + } while([Channel.DONE, "search_fail", "search_success", "stats_update", "matches_update"].indexOf(msg) == -1); // bust caches if (["stats_update"].indexOf(msg) != -1) @@ -133,8 +128,18 @@ module.exports.subscribeUpdates = function(name, timeout=UPDATE_TIMEOUT) { subscription.unsubscribe(); return undefined; } - return true; - }, _first: true }; + return msg; + } }; +} + +// search an unknown player +module.exports.searchPlayer = async function(name) { + return await postBE("/player/" + name + "/search"); +} + +// update a known player +module.exports.updatePlayer = async function(name) { + return await postBE("/player/" + name + "/update"); } // return player @@ -17,7 +17,8 @@ const sqlite = require("sqlite"), invite: "https://discord.gg/txTchJY", unknownCommandResponse: false }), - responses = require("./responses"); + responses = require("./responses"), + util = require("./util"); const DISCORD_TOKEN = process.env.DISCORD_TOKEN, LOGGLY_TOKEN = process.env.LOGGLY_TOKEN; @@ -81,7 +82,7 @@ client }) // response reaction interface - .on("messageReactionAdd", responses.onNewReaction); + .on("messageReactionAdd", util.onNewReaction); client.setProvider( sqlite.open(path.join(__dirname, "settings.sqlite3")).then( @@ -89,6 +90,7 @@ client.setProvider( client.registry .registerGroup('vainsocial', 'VainSocial') + .registerGroup('vainsocial-guild', 'VainSocial Guild management') .registerDefaultTypes() .registerDefaultGroups() .registerDefaultCommands({ eval_: false, commandState: false }) diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js new file mode 100644 index 0000000..7f6aac7 --- /dev/null +++ b/commands/vainsocial/guild_add.js @@ -0,0 +1,80 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + Promise = require("bluebird"), + request = require("request-promise"), + api = require("../../api"), + util = require("../../util"); + +const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bot/api"; + +module.exports = class AddGuildMemberCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildadd", + aliases: ["vguild-add", "vgadd", "vga"], + group: "vainsocial-guild", + memberName: "vainsocial-guildadd", + description: "Register a member to your Guild.", + details: oneLine` +Register IGNs to your Guild. +`, + examples: ["vgadd StormCallerSr", "vgadd StormCallerSr shutterfly"], + argsType: "multiple" + }); + } + // 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 + 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 == "search_success") break; + if (notif == undefined) { + // give up + await progress(`Ooops! Could not find ${user}.`, true); + return; + } + } + } else { + await api.updatePlayer(user); + } + + // all good, register to self guild + await request.post(API_FE_URL + "/guild/members", { + forever: true, + form: { + user_token: msg.author.id, + member_name: 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 new file mode 100644 index 0000000..9114703 --- /dev/null +++ b/commands/vainsocial/guild_create.js @@ -0,0 +1,66 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + request = require("request-promise"), + util = require("../../util"); + +const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bot/api"; + +module.exports = class RegisterGuildCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildcreate", + aliases: ["vguild-create", "vgcreate"], + group: "vainsocial-guild", + memberName: "vainsocial-guildcreate", + description: "Register a new Guild.", + details: oneLine` +Create a Guild with your VainSocial profile as leader. +`, + examples: ["vguild-create MyAmazingGuild MAG eu"], + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your Guild's name.", + type: "string", + min: 3 + }, { + key: "tag", + label: "tag", + prompt: "Please specify your Guild's tag.", + type: "string", + min: 3, + max: 4 + }, { + // TODO use enum + key: "region", + label: "region", + prompt: "Please specify your Guild's region.", + type: "string", + min: 2, + max: 10 + } ] + }); + } + // register a VainSocial Guild to a Discord account + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-create"); + console.log(API_FE_URL + "/guild"); + await request.post(API_FE_URL + "/guild", { + forever: true, + form: { + 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. +`); + } +}; diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js new file mode 100644 index 0000000..c459590 --- /dev/null +++ b/commands/vainsocial/guild_view.js @@ -0,0 +1,58 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + request = require("request-promise"), + util = require("../../util"); + +const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bot/api"; + +module.exports = class ViewGuildCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildview", + aliases: ["vguild-view", "vgview", "vgv"], + group: "vainsocial-guild", + memberName: "vainsocial-guildview", + description: "View members of a Guild.", + details: oneLine` +Show a summary of your Guild. +`, + examples: ["vgview"], + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your Guild's name.", + type: "string", + min: 3, + default: "?" + } ] + }); + } + // show Guild details + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-view"); + // get this user's guild + const guild = await request(API_FE_URL + "/guild", { + forever: true, + json: true, + qs: { + user_token: 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); + } +}; diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index 4de7c8f..02818be 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -4,18 +4,21 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, - responses = require("../../responses"); + request = require("request-promise"), + util = require("../../util"); -module.exports = class RememberUserCommand extends Commando.Command { +const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bot/api"; + +module.exports = class RegisterUserCommand extends Commando.Command { constructor(client) { super(client, { name: "vainsocial-me", aliases: ["vme"], group: "vainsocial", memberName: "vainsocial-me", - description: "Remembers a users's in game name.", + description: "Register a users's in game name.", details: oneLine` -Store your in game name for quicker access to other commands. +Store your in game name for quicker access to other commands and for Guild management. `, examples: ["vme shutterfly"], @@ -29,7 +32,20 @@ Store your in game name for quicker access to other commands. } ] }); } + // register a Discord account at VainSocial async run(msg, args) { - await responses.rememberUser(msg, args); + const ign = args.name; + util.trackAction(msg, "vainsocial-me", ign); + await request.post(API_FE_URL + "/user", { + forever: true, + form: { + name: ign, + user_token: msg.author.id + } + }); + await msg.reply(oneLine` +You are now able to use ${util.usg(msg, "v")} to access your profile faster. +You now have access to Guild management features. +`); } }; diff --git a/package.json b/package.json index 40fbca2..b5962a4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "erlpack": "github:hammerandchisel/erlpack", "request": "^2.81.0", "request-promise": "^4.2.0", - "request-promise-native": "^1.0.3", "sqlite": "^2.5.0", "universal-analytics": "^0.4.13", "webstomp": "0.0.10", diff --git a/responses.js b/responses.js index 692d1f4..49302a4 100644 --- a/responses.js +++ b/responses.js @@ -3,58 +3,17 @@ "use strict"; const Commando = require("discord.js-commando"), - Discord = require("discord.js"), Promise = require("bluebird"), - ua = require("universal-analytics"), emoji = require("discord-emoji"), oneLine = require("common-tags").oneLine, - Channel = require("async-csp").Channel, + 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, - REACTION_TIMEOUT = parseInt(process.env.REACTION_TIMEOUT) || 60, // s - GOOGLEANALYTICS_ID = process.env.GOOGLEANALYTICS_ID, ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/"); -const reactionsPipe = new Channel(); - -// embed template -function vainsocialEmbed(title, link, command) { - return new Discord.RichEmbed() - .setTitle(title) - .setColor("#55ADD3") - .setURL(ROOTURL + link + track(command)) - .setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png", - ROOTURL + track(command)) - .setFooter("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png") -} - -// analytics url -function track(command) { - return "?utm_source=discordbot&utm_medium=discord&utm_campaign=" + command; -} - -// direct analytics -function trackAction(msg, action, ign="") { - if (GOOGLEANALYTICS_ID == undefined) return; - const user = ua(GOOGLEANALYTICS_ID, msg.author.id, - { strictCidFormat: false }); - user.pageview({ - documentPath: action, - documentTitle: ign, - campaignSource: msg.guild.id, - campaignMedium: msg.guild.name - }).send(); -} - -// return the shortest version of the usage help -// just '?vm' -function usg(msg, cmd) { - return msg.anyUsage(cmd, undefined, null); -} - // based on impact score float, return an Emoji function emojifyScore(score) { if (score > 0.7) return emoji.people.heart_eyes; @@ -89,7 +48,7 @@ async function formatMatchDetail(match) { for(let participant of roster.participants) { const hero = await api.mapActor(participant.actor); teamstr += ` -\`${hero}\`, [${participant.player.name}](${ROOTURL}player/${participant.player.name}${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)}%\``; +\`${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]); } @@ -139,78 +98,15 @@ module.exports.rotateGameStatus = (client) => { })(); } -// reaction -> pipe ->>> consumers -// handle new reaction event -module.exports.onNewReaction = (reaction) => { - if (reaction.users.array().length <= 1) return; // was me TODO - reactionsPipe.put(reaction); -} - -// create an iterator that returns promises to await new reactions -// the Promise result is the reaction name -function awaitReactions(message, emoji, timeout=REACTION_TIMEOUT) { - const pipeOut = new Channel(); - let reactions = []; - // stop listening after timeout - setTimeout(() => { - pipeOut.close(); - Promise.map(reactions, async (r) => r.remove()); - }, timeout*1000); - - // async in background - Promise.each(emoji, async (em) => - reactions.push(await message.react(em))); - - let reaction; - reactionsPipe.pipe(pipeOut); - return { - next: async function() { - do { - reaction = await pipeOut.take(); - if (reaction == Channel.DONE) - return undefined; - } while (reaction.message.id != message.id || - emoji.indexOf(reaction.emoji.name) == -1); - return reaction.emoji.name; - } - } -} - -// respond or say text or embed -async function respond(msg, data, response) { - if (response == undefined) { - if (typeof data === "string") { - response = await msg.say(data); - } else { - response = await msg.embed(data); - } - } else { - if (typeof data === "string") { - if (data != response.content) - await response.edit(data); - } else { - if (new Date(response.embeds[0].createdTimestamp).getTime() - != data.timestamp.getTime()) - // TODO how2 edit embed properly?! - await msg.editResponse(response, { - type: "plain", - content: "", - options: { embed: data } - }); - } - } - return response; -} - // about module.exports.showAbout = async (msg) => { - trackAction(msg, "about"); - await msg.embed(vainsocialEmbed("About VainSocial", "", "about") + 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 + track("about"), true) + 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", @@ -222,20 +118,8 @@ Currently running on ${msg.client.guilds.size} servers.`) // return the sqlite stored ign for this user function nameByUser(msg) { - return msg.guild.settings.get("remember+" + msg.author.id); -} - -// tell the user that they need to store their name -function formatSorryUnknown(msg) { - return `You're unknown to our service. Try ${usg(msg, "help vme")}.`; -} - -module.exports.rememberUser = async (msg, args) => { - const ign = args.name; - trackAction(msg, "vainsocial-me", ign); - await msg.guild.settings.set("remember+" + msg.author.id, ign); - await msg.reply( -`You are now able to use ${usg(msg, "v")} to access your profile faster.`); + throw "nope that's not possible yet"; + //return msg.guild.settings.get("remember+" + msg.author.id); } // show player profile and last match @@ -244,84 +128,90 @@ module.exports.showUser = async (msg, args) => { response, ign = args.name, reactionsAdded = false; - trackAction(msg, "vainsocial-user", ign); + util.trackAction(msg, "vainsocial-user", ign); // shorthand // "?" is not accepted as user input, but the default for empty if (ign == "?") { ign = nameByUser(msg); if (ign == undefined) { - await msg.reply(formatSorryUnknown(msg)); + await msg.reply(util.formatSorryUnknown(msg)); return; } } + // TODO refactor the update flow a bit + // trigger backend + if (await api.getPlayer(ign) == undefined) + await api.searchPlayer(ign); + else await api.updatePlayer(ign); + const waiter = api.subscribeUpdates(ign); - while (await waiter.next() != undefined) { + do { const [player, data] = await Promise.all([ api.getPlayer(ign), api.getMatches(ign) ]); if (player == undefined) { - response = await respond(msg, + response = await util.respond(msg, "Loading your data…", response); continue; } if (data == undefined || data[0].data.length == 0) { - response = await respond(msg, + response = await util.respond(msg, "No match history for you yet", response); continue; } const matches = data[0]; const moreHelp = oneLine` -*${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, -${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` +*${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 = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign, "vainsocial-user") + const embed = util.vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign, "vainsocial-user") .setThumbnail(ROOTURL + "images/game/skill_tiers/" + matches.data[0].skill_tier + ".png") .setDescription("") .addField("Profile", await formatPlayer(player), true) .addField("Last match", await formatMatch(matches.data[0]) + moreHelp, true) .setTimestamp(new Date(matches.data[0].created_at)); - response = await respond(msg, embed, response); + response = await util.respond(msg, embed, response); if (!reactionsAdded) { reactionsAdded = true; - let reactionWaiter = awaitReactions(response, + 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) { - trackAction(msg, "reaction-match", ign); + util.trackAction(msg, "reaction-match", ign); await respondMatch(msg, ign, matches.data[0].match_api_id); } if (rmoji == emoji.symbols["1234"]) { - trackAction(msg, "reaction-matches", ign); + util.trackAction(msg, "reaction-matches", ign); await respondMatches(msg, ign); } } })(); // async in background } - } + } while (await waiter.next() != undefined); if (!reactionsAdded) - await respond(msg, `Could not find \`${ign}\`.`, response); + 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 = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, + 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 respond(msg, embed, response); + return await util.respond(msg, embed, response); } module.exports.showMatch = async (msg, args) => { @@ -329,37 +219,42 @@ module.exports.showMatch = async (msg, args) => { let responded = false, ign = args.name, response; - trackAction(msg, "vainsocial-match", ign); + util.trackAction(msg, "vainsocial-match", ign); // shorthand if (ign == "?") { ign = nameByUser(msg); if (ign == undefined) { - await msg.reply(formatSorryUnknown(msg)); + await msg.reply(util.formatSorryUnknown(msg)); return; } } + // trigger backend + if (await api.getPlayer(ign) == undefined) + await api.searchPlayer(ign); + else await api.updatePlayer(ign); + const waiter = api.subscribeUpdates(ign); - while (await waiter.next() != undefined) { + do { const data = await api.getMatches(ign); if (data == undefined || data[0].data.length == 0) { - response = await respond(msg, "No matches for you yet", + response = await util.respond(msg, "No matches for you yet", response); continue; } const matches = data[0]; if (index - 1 > matches.data.length) { - response = await respond(msg, "Not enough matches yet", + response = await util.respond(msg, "Not enough matches yet", response); continue; } let id = matches.data[index -1].match_api_id; response = await respondMatch(msg, ign, id, response); responded = true; - } + } while (await waiter.next() != undefined); if (!responded) - await respond(msg, `Could not find \`${ign}\`.`, response); + await util.respond(msg, `Could not find \`${ign}\`.`, response); } // show match history @@ -373,18 +268,18 @@ async function respondMatches(msg, ign, response=undefined) { matches = data[0].data.slice(0, MATCH_HISTORY_LEN), // TODO matches_num = matches.length; - let embed = vainsocialEmbed(ign, "player/" + ign, "vainsocial-matches") + let embed = util.vainsocialEmbed(ign, "player/" + ign, "vainsocial-matches") .setDescription(` Last ${matches_num} casual and ranked matches. -*${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign + " number")} for details* +*${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 respond(msg, embed, response); + response = await util.respond(msg, embed, response); - const reactionWaiter = awaitReactions(response, + const reactionWaiter = util.awaitReactions(response, count.slice(0, matches_num)); (async () => { while (true) { @@ -392,7 +287,7 @@ Last ${matches_num} casual and ranked matches. if (rmoji == undefined) break; // timeout let idx = count.indexOf(rmoji); await respondMatch(msg, ign, matches[idx].match_api_id); - trackAction(msg, "reaction-match", ign); + util.trackAction(msg, "reaction-match", ign); } })(); // async in background return response; @@ -402,30 +297,35 @@ module.exports.showMatches = async (msg, args) => { let ign = args.name, responded = false, response; - trackAction(msg, "vainsocial-matches", ign); + util.trackAction(msg, "vainsocial-matches", ign); // shorthand if (ign == "?") { ign = nameByUser(msg); if (ign == undefined) { - await msg.reply(formatSorryUnknown(msg)); + await msg.reply(util.formatSorryUnknown(msg)); return; } } + // trigger backend + if (await api.getPlayer(ign) == undefined) + await api.searchPlayer(ign); + else await api.updatePlayer(ign); + const waiter = api.subscribeUpdates(ign); - while (await waiter.next() != undefined) { + do { const data = await api.getMatches(ign); if (data == undefined || data[0].data.length == 0) { - response = await respond(msg, "No matches for you yet", + response = await util.respond(msg, "No matches for you yet", response); continue; } const matches = data[0]; response = await respondMatches(msg, ign, response); responded = true; - } + } while (await waiter.next() != undefined); if (!responded) - await respond(msg, `Could not find \`${ign}\`.`, + await util.respond(msg, `Could not find \`${ign}\`.`, response); } @@ -0,0 +1,118 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Discord = require("discord.js"), + ua = require("universal-analytics"), + Promise = require("bluebird"), + Channel = require("async-csp").Channel; + +const GOOGLEANALYTICS_ID = process.env.GOOGLEANALYTICS_ID, + REACTION_TIMEOUT = parseInt(process.env.REACTION_TIMEOUT) || 60, // s + PREVIEW = process.env.PREVIEW != "false", + ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/"); + +const reactionsPipe = new Channel(); + +// embed template +module.exports.vainsocialEmbed = (title, link, command) => { + return new Discord.RichEmbed() + .setTitle(title) + .setColor("#55ADD3") + .setURL(ROOTURL + link + module.exports.track(command)) + .setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png", + ROOTURL + module.exports.track(command)) + .setFooter("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png") +}; + +// analytics url +module.exports.track = (command) => { + return "?utm_source=discordbot&utm_medium=discord&utm_campaign=" + command; +}; + +// direct analytics +module.exports.trackAction = (msg, action, ign="") => { + if (GOOGLEANALYTICS_ID == undefined) return; + const user = ua(GOOGLEANALYTICS_ID, msg.author.id, + { strictCidFormat: false }); + user.pageview({ + documentPath: action, + documentTitle: ign, + campaignSource: msg.guild.id, + campaignMedium: msg.guild.name + }).send(); +}; + +// return the shortest version of the usage help +// just '?vm' +module.exports.usg = (msg, cmd) => { + return msg.anyUsage(cmd, undefined, null); +}; + +// reaction -> pipe ->>> consumers +// handle new reaction event +module.exports.onNewReaction = (reaction) => { + if (reaction.users.array().length <= 1) return; // was me TODO + reactionsPipe.put(reaction); +}; + +// create an iterator that returns promises to await new reactions +// the Promise result is the reaction name +module.exports.awaitReactions = (message, emoji, timeout=REACTION_TIMEOUT) => { + const pipeOut = new Channel(); + let reactions = []; + // stop listening after timeout + setTimeout(() => { + pipeOut.close(); + Promise.map(reactions, async (r) => r.remove()); + }, timeout*1000); + + // async in background + Promise.each(emoji, async (em) => + reactions.push(await message.react(em))); + + let reaction; + reactionsPipe.pipe(pipeOut); + return { + next: async function() { + do { + reaction = await pipeOut.take(); + if (reaction == Channel.DONE) + return undefined; + } while (reaction.message.id != message.id || + emoji.indexOf(reaction.emoji.name) == -1); + return reaction.emoji.name; + } + } +}; + +// respond or say text or embed +module.exports.respond = async (msg, data, response) => { + if (response == undefined) { + if (typeof data === "string") { + response = await msg.say(data); + } else { + response = await msg.embed(data); + } + } else { + if (typeof data === "string") { + if (data != response.content) + await response.edit(data); + } else { + if (new Date(response.embeds[0].createdTimestamp).getTime() + != data.timestamp.getTime()) + // TODO how2 edit embed properly?! + await msg.editResponse(response, { + type: "plain", + content: "", + options: { embed: data } + }); + } + } + return response; +}; + +// tell the user that they need to store their name +module.exports.formatSorryUnknown = (msg) => { + return `You're unknown to our service. Try ${util.usg(msg, "help vme")}.`; +}; |
