From 840a7e6eedbf8791f455d7085a8180f586d42266 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 14 Mar 2017 17:46:23 +0100 Subject: attempt search without region, use new joblib --- joblib | 2 +- main.py | 38 +++++++++++++++----------------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/joblib b/joblib index 542df39..7ced841 160000 --- a/joblib +++ b/joblib @@ -1 +1 @@ -Subproject commit 542df395ce186ccc28d2c4122b7b12024ade8f47 +Subproject commit 7ced841aa44a7dd7bd2d3a68ceb989ef488ce441 diff --git a/main.py b/main.py index f46fa73..2f5d733 100644 --- a/main.py +++ b/main.py @@ -57,31 +57,9 @@ async def on_ready(): name="alpha.vainsocial.com")) -@bot.event -async def on_command_error(error, ctx): - logging.error(error) - if ctx.invoked_subcommand: - pages = bot.formatter.format_help_for( - ctx, ctx.invoked_subcommand) - for page in pages: - await bot.send_message( - ctx.message.channel, page) - else: - pages = bot.formatter.format_help_for( - ctx, ctx.command) - for page in pages: - await bot.send_message( - ctx.message.channel, page) - - @bot.command() -async def vainsocial(name: str, region: str): +async def vainsocial(name: str, region: str = None): """Retrieves a player's stats.""" - region = region.lower() - if region not in ["na", "eu", "sg", "ea", "sa"]: - await bot.say("That region is not supported.") - return - query = """ SELECT player.name, @@ -153,6 +131,20 @@ async def vainsocial(name: str, region: str): lmcd = data["last_match_created_date"] msgid = await bot.say(embed=emb(data)) + if not in_cache: + if region is None: + await bot.edit_message(msgid, + "Can't find you. What is your region?") + return + region = region.lower() + if region not in ["na", "eu", "sg", "ea", "sa"]: + await bot.edit_message(msgid, + "That region is not supported.") + return + else: + if region is None: + region = data["shard_id"] + logging.info("'%s' cached: %s", name, in_cache) payload = { -- cgit v1.3.1 From a134ef3c7367269a1baafca86b1b69acdc06587b Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 14 Mar 2017 17:51:11 +0100 Subject: alias to !v or !vain --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 2f5d733..63d44d8 100644 --- a/main.py +++ b/main.py @@ -57,7 +57,7 @@ async def on_ready(): name="alpha.vainsocial.com")) -@bot.command() +@bot.command(aliases=["v", "vain"]) async def vainsocial(name: str, region: str = None): """Retrieves a player's stats.""" query = """ -- cgit v1.3.1 From d10b8e4df90e1324be7cd31aa7654f3572d54cb9 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 15 Mar 2017 16:06:15 +0100 Subject: add about command --- main.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/main.py b/main.py index 63d44d8..d071350 100644 --- a/main.py +++ b/main.py @@ -57,6 +57,24 @@ async def on_ready(): name="alpha.vainsocial.com")) +@bot.command() +async def about(): + """Print invite links.""" + emb = discord.Embed( + title="Vainsocial Discord bot", + description="Built by the Vainsocial development team using the MadGlory API." + ) + emb.add_field(name="Website", + value="https://alpha.vainsocial.com") + emb.add_field(name="Bot invite link", + value="https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot") + emb.add_field(name="Developer Discord", + value="https://discord.gg/txTchJY") + emb.add_field(name="Twitter", + value="https://twitter.com/vainsocial") + await bot.say(embed=emb) + + @bot.command(aliases=["v", "vain"]) async def vainsocial(name: str, region: str = None): """Retrieves a player's stats.""" -- cgit v1.3.1 From f60fb8e7f90f21598a62f2300218ab2b9b8cae7b Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 15 Mar 2017 16:19:30 +0100 Subject: add server counter --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index d071350..bae83fa 100644 --- a/main.py +++ b/main.py @@ -62,7 +62,7 @@ async def about(): """Print invite links.""" emb = discord.Embed( title="Vainsocial Discord bot", - description="Built by the Vainsocial development team using the MadGlory API." + description="Built by the Vainsocial development team using the MadGlory API. Currently running on %i servers." % (len(bot.servers),) ) emb.add_field(name="Website", value="https://alpha.vainsocial.com") -- cgit v1.3.1 From 362e508c62d82376a932539199efb08469b1d8b4 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 15 Mar 2017 16:50:29 +0100 Subject: add tracking args --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index bae83fa..e429cdb 100644 --- a/main.py +++ b/main.py @@ -65,7 +65,7 @@ async def about(): description="Built by the Vainsocial development team using the MadGlory API. Currently running on %i servers." % (len(bot.servers),) ) emb.add_field(name="Website", - value="https://alpha.vainsocial.com") + value="https://alpha.vainsocial.com/?src=discordbot") emb.add_field(name="Bot invite link", value="https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot") emb.add_field(name="Developer Discord", @@ -120,16 +120,16 @@ async def vainsocial(name: str, region: str = None): emb = discord.Embed( title="%(name)s" % data, - url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s" % data + url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?src=discordbot" % data ) emb.set_author(name="Vainsocial", url="https://alpha.vainsocial.com") emb.add_field(name="Profile", value=("%(wins)i wins / %(played)i games\n" + - "https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s") % data) + "https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?src=discordbot") % data) emb.add_field(name="Last match", value=("%(result)s %(mode)s as %(hero)s %(kills)i/%(deaths)i/%(assists)i\n" + - "https://alpha.vainsocial.com/matches/%(match_api_id)s") % data) + "https://alpha.vainsocial.com/matches/%(match_api_id)s/?src=discordbot") % data) emb.set_footer(text="Vainsocial - Vainglory social stats service") emb.set_thumbnail(url="https://alpha.vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data) -- cgit v1.3.1 From e66f60fb9f2c0cdeb023feba23ac4f99a9405e01 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 15 Mar 2017 16:53:26 +0100 Subject: map sg -> sea --- main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.py b/main.py index e429cdb..adada9d 100644 --- a/main.py +++ b/main.py @@ -155,6 +155,8 @@ async def vainsocial(name: str, region: str = None): "Can't find you. What is your region?") return region = region.lower() + if region == "sea": + region = "sg" if region not in ["na", "eu", "sg", "ea", "sa"]: await bot.edit_message(msgid, "That region is not supported.") -- cgit v1.3.1 From 40d8e60a5dc4017289e3eeee453a6da9e737e87f Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 16 Mar 2017 20:15:18 +0100 Subject: update tracking params --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index adada9d..c8c5576 100644 --- a/main.py +++ b/main.py @@ -65,7 +65,7 @@ async def about(): description="Built by the Vainsocial development team using the MadGlory API. Currently running on %i servers." % (len(bot.servers),) ) emb.add_field(name="Website", - value="https://alpha.vainsocial.com/?src=discordbot") + value="https://alpha.vainsocial.com/?utm_source=discord&utm_medium=vainsocial") emb.add_field(name="Bot invite link", value="https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot") emb.add_field(name="Developer Discord", @@ -120,16 +120,16 @@ async def vainsocial(name: str, region: str = None): emb = discord.Embed( title="%(name)s" % data, - url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?src=discordbot" % data + url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial" % data ) emb.set_author(name="Vainsocial", url="https://alpha.vainsocial.com") emb.add_field(name="Profile", value=("%(wins)i wins / %(played)i games\n" + - "https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?src=discordbot") % data) + "https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial") % data) emb.add_field(name="Last match", value=("%(result)s %(mode)s as %(hero)s %(kills)i/%(deaths)i/%(assists)i\n" + - "https://alpha.vainsocial.com/matches/%(match_api_id)s/?src=discordbot") % data) + "https://alpha.vainsocial.com/matches/%(match_api_id)s/?utm_source=discord&utm_medium=vainsocial") % data) emb.set_footer(text="Vainsocial - Vainglory social stats service") emb.set_thumbnail(url="https://alpha.vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data) -- cgit v1.3.1 From 7cb25bd6866369df6421efb354fad58dd59535a0 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 17 Mar 2017 18:16:57 +0100 Subject: move repos --- .gitmodules | 2 +- joblib | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 52948d0..47695ea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "joblib"] path = joblib - url = https://github.com/vainglorygame/joblib + url = https://gitlab.com/vainglorygame/joblib.git diff --git a/joblib b/joblib index 7ced841..542df39 160000 --- a/joblib +++ b/joblib @@ -1 +1 @@ -Subproject commit 7ced841aa44a7dd7bd2d3a68ceb989ef488ce441 +Subproject commit 542df395ce186ccc28d2c4122b7b12024ade8f47 -- cgit v1.3.1 From 90ea3116b667a6a944458564ea5e0b114c9d5ab8 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 17 Mar 2017 19:13:41 +0100 Subject: committed wrong joblib ver --- joblib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joblib b/joblib index 542df39..7ced841 160000 --- a/joblib +++ b/joblib @@ -1 +1 @@ -Subproject commit 542df395ce186ccc28d2c4122b7b12024ade8f47 +Subproject commit 7ced841aa44a7dd7bd2d3a68ceb989ef488ce441 -- cgit v1.3.1 From 5b2c6e3f290379f1124758d77c55db8b79c2df8b Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 19 Mar 2017 15:52:22 +0100 Subject: show last match updated date --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index c8c5576..304fcc2 100644 --- a/main.py +++ b/main.py @@ -120,6 +120,7 @@ async def vainsocial(name: str, region: str = None): emb = discord.Embed( title="%(name)s" % data, + description="Last match registered (GMT): %(last_match_created_date)s" % data, url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial" % data ) emb.set_author(name="Vainsocial", -- cgit v1.3.1 From 1d180c4a2c43a0984512b97603df16eed502267b Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 19 Mar 2017 16:34:20 +0100 Subject: downgrade joblib to prevent concurrency issues --- joblib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joblib b/joblib index 7ced841..a5cc4c3 160000 --- a/joblib +++ b/joblib @@ -1 +1 @@ -Subproject commit 7ced841aa44a7dd7bd2d3a68ceb989ef488ce441 +Subproject commit a5cc4c3ea48908166f43dabaf5d54046d2014d93 -- cgit v1.3.1 From ca785cb1fad8ac9b53bce829e7876a5d9c086385 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 20 Mar 2017 17:59:52 +0100 Subject: use named links --- main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 304fcc2..693fdd7 100644 --- a/main.py +++ b/main.py @@ -65,13 +65,13 @@ async def about(): description="Built by the Vainsocial development team using the MadGlory API. Currently running on %i servers." % (len(bot.servers),) ) emb.add_field(name="Website", - value="https://alpha.vainsocial.com/?utm_source=discord&utm_medium=vainsocial") + value="[vainsocial.com](https://alpha.vainsocial.com/?utm_source=discord&utm_medium=vainsocial)") emb.add_field(name="Bot invite link", - value="https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot") + value="[discordapp.com](https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot)") emb.add_field(name="Developer Discord", - value="https://discord.gg/txTchJY") + value="[txTchJY](https://discord.gg/txTchJY)") emb.add_field(name="Twitter", - value="https://twitter.com/vainsocial") + value="[twitter/vainsocial](https://twitter.com/vainsocial)") await bot.say(embed=emb) @@ -127,10 +127,10 @@ async def vainsocial(name: str, region: str = None): url="https://alpha.vainsocial.com") emb.add_field(name="Profile", value=("%(wins)i wins / %(played)i games\n" + - "https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial") % data) + "[View on vainsocial.com](https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial)") % data) emb.add_field(name="Last match", value=("%(result)s %(mode)s as %(hero)s %(kills)i/%(deaths)i/%(assists)i\n" + - "https://alpha.vainsocial.com/matches/%(match_api_id)s/?utm_source=discord&utm_medium=vainsocial") % data) + "[View on vainsocial.com](https://alpha.vainsocial.com/matches/%(match_api_id)s/?utm_source=discord&utm_medium=vainsocial)") % data) emb.set_footer(text="Vainsocial - Vainglory social stats service") emb.set_thumbnail(url="https://alpha.vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data) -- cgit v1.3.1 From 6dccd6c2973b1cde3ac66f24e290b9ed107e94a5 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 20 Mar 2017 18:04:27 +0100 Subject: print region --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 693fdd7..c74ea4d 100644 --- a/main.py +++ b/main.py @@ -119,7 +119,7 @@ async def vainsocial(name: str, region: str = None): data["result"] = "won" if data["winner"] else "lost" emb = discord.Embed( - title="%(name)s" % data, + title="%(name)s (%(shard_id)s)" % data, description="Last match registered (GMT): %(last_match_created_date)s" % data, url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial" % data ) -- cgit v1.3.1 From 6905c585e8bff61dad66594e28ef17d574c665da Mon Sep 17 00:00:00 2001 From: Vainsocial root Date: Tue, 21 Mar 2017 07:23:41 +0000 Subject: request only ranked, casual --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 304fcc2..78b196f 100644 --- a/main.py +++ b/main.py @@ -172,7 +172,8 @@ async def vainsocial(name: str, region: str = None): "region": region, "params": { "filter[createdAt-start]": lmcd, - "filter[playerNames]": name + "filter[playerNames]": name, + "filter[gameMode]": "casual,ranked" } } jobid = (await queue.request(jobtype="grab", -- cgit v1.3.1 From 8adfdfe94e994821594678625fb3dd6cb31b9d98 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 21 Mar 2017 09:01:24 +0100 Subject: roll back joblib to support pooling and notifications --- joblib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joblib b/joblib index a5cc4c3..049d73f 160000 --- a/joblib +++ b/joblib @@ -1 +1 @@ -Subproject commit a5cc4c3ea48908166f43dabaf5d54046d2014d93 +Subproject commit 049d73f217dfc881b89be23bc23d33d995bdea42 -- cgit v1.3.1 From d9a4daf9070beb801b76799b615c4f7b0493abab Mon Sep 17 00:00:00 2001 From: Vainsocial root Date: Fri, 24 Mar 2017 18:17:35 +0000 Subject: use release url --- main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index 95f83d1..5f0e076 100644 --- a/main.py +++ b/main.py @@ -54,7 +54,7 @@ async def on_ready(): await connect(source_db, dest_db) await bot.change_presence( game=discord.Game( - name="alpha.vainsocial.com")) + name="vainsocial.com")) @bot.command() @@ -65,11 +65,11 @@ async def about(): description="Built by the Vainsocial development team using the MadGlory API. Currently running on %i servers." % (len(bot.servers),) ) emb.add_field(name="Website", - value="[vainsocial.com](https://alpha.vainsocial.com/?utm_source=discord&utm_medium=vainsocial)") + value="[vainsocial.com](https://vainsocial.com/?utm_source=discord&utm_medium=vainsocial)") emb.add_field(name="Bot invite link", value="[discordapp.com](https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot)") emb.add_field(name="Developer Discord", - value="[txTchJY](https://discord.gg/txTchJY)") + value="[discord.me/vainsocial](https://discord.me/vainsocial)") emb.add_field(name="Twitter", value="[twitter/vainsocial](https://twitter.com/vainsocial)") await bot.say(embed=emb) @@ -121,19 +121,19 @@ async def vainsocial(name: str, region: str = None): emb = discord.Embed( title="%(name)s (%(shard_id)s)" % data, description="Last match registered (GMT): %(last_match_created_date)s" % data, - url="https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial" % data + url="https://vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial" % data ) emb.set_author(name="Vainsocial", - url="https://alpha.vainsocial.com") + url="https://vainsocial.com") emb.add_field(name="Profile", value=("%(wins)i wins / %(played)i games\n" + - "[View on vainsocial.com](https://alpha.vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial)") % data) + "[View on vainsocial.com](https://vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial)") % data) emb.add_field(name="Last match", value=("%(result)s %(mode)s as %(hero)s %(kills)i/%(deaths)i/%(assists)i\n" + - "[View on vainsocial.com](https://alpha.vainsocial.com/matches/%(match_api_id)s/?utm_source=discord&utm_medium=vainsocial)") % data) + "[View on vainsocial.com](https://vainsocial.com/matches/%(match_api_id)s/?utm_source=discord&utm_medium=vainsocial)") % data) emb.set_footer(text="Vainsocial - Vainglory social stats service") - emb.set_thumbnail(url="https://alpha.vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data) + emb.set_thumbnail(url="https://vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data) return emb async with pool.acquire() as con: -- cgit v1.3.1 From 7eb8795a2d80b0c83b2b38711cebfd27a2ae7d3b Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 25 Mar 2017 16:21:58 +0100 Subject: rewrite using updater --- main.py | 155 ++++++++++++++++++++++++++++------------------------------------ 1 file changed, 67 insertions(+), 88 deletions(-) diff --git a/main.py b/main.py index 5f0e076..81dcaa3 100644 --- a/main.py +++ b/main.py @@ -2,21 +2,15 @@ import logging import os +import http +import json import asyncio import asyncpg import discord from discord.ext import commands -import joblib.joblib - -source_db = { - "host": os.environ.get("POSTGRESQL_SOURCE_HOST") or "localhost", - "port": os.environ.get("POSTGRESQL_SOURCE_PORT") or 5433, - "user": os.environ.get("POSTGRESQL_SOURCE_USER") or "vainraw", - "password": os.environ.get("POSTGRESQL_SOURCE_PASSWORD") or "vainraw", - "database": os.environ.get("POSTGRESQL_SOURCE_DB") or "vainsocial-raw" -} +from socketIO_client import SocketIO, LoggingNamespace -dest_db = { +db = { "host": os.environ.get("POSTGRESQL_DEST_HOST") or "localhost", "port": os.environ.get("POSTGRESQL_DEST_PORT") or 5432, "user": os.environ.get("POSTGRESQL_DEST_USER") or "vainweb", @@ -26,24 +20,11 @@ dest_db = { bot = commands.Bot( - command_prefix="!", + command_prefix="?", description="Vainsocial Vainglory stats bot") -queue = None pool = None -async def connect(queuedb, db): - logging.warning("connecting to database") - - global queue - queue = joblib.joblib.JobQueue() - await queue.connect(**queuedb) - await queue.setup() - - global pool - pool = await asyncpg.create_pool( - min_size=1, **db) - @bot.event async def on_ready(): @@ -51,7 +32,10 @@ async def on_ready(): "https://discordapp.com/oauth2/authorize?&" + "client_id=%s&scope=bot)", bot.user.name, bot.user.id) - await connect(source_db, dest_db) + logging.warning("connecting to database") + global pool + pool = await asyncpg.create_pool( + min_size=2, **db) await bot.change_presence( game=discord.Game( name="vainsocial.com")) @@ -76,7 +60,7 @@ async def about(): @bot.command(aliases=["v", "vain"]) -async def vainsocial(name: str, region: str = None): +async def vainsocial(name: str): """Retrieves a player's stats.""" query = """ SELECT @@ -88,11 +72,11 @@ async def vainsocial(name: str, region: str = None): participant.kills, participant.deaths, participant.assists, participant.farm, player.skill_tier, player.played, player.wins, player.last_match_created_date::text - FROM match, roster, participant, player where + FROM match, roster, participant, player WHERE match.api_id=roster.match_api_id AND roster.api_id=participant.roster_api_id AND participant.player_api_id=player.api_id AND - player.name=$1 AND player.last_match_created_date::text<>$2 + player.name=$1 ORDER BY match.created_at DESC LIMIT 1 """ @@ -139,68 +123,63 @@ async def vainsocial(name: str, region: str = None): async with pool.acquire() as con: await bot.type() - data = await con.fetchrow(query, name, 'NULL') - if data is None: - in_cache = False # new user - lmcd = "2017-02-01T00:00:00Z" - msgid = await bot.say( - "%s: please wait…" % name) - else: - in_cache = True # returning user - lmcd = data["last_match_created_date"] - msgid = await bot.say(embed=emb(data)) - - if not in_cache: - if region is None: - await bot.edit_message(msgid, - "Can't find you. What is your region?") - return - region = region.lower() - if region == "sea": - region = "sg" - if region not in ["na", "eu", "sg", "ea", "sa"]: - await bot.edit_message(msgid, - "That region is not supported.") - return - else: - if region is None: - region = data["shard_id"] - - logging.info("'%s' cached: %s", name, in_cache) - - payload = { - "region": region, - "params": { - "filter[createdAt-start]": lmcd, - "filter[playerNames]": name, - "filter[gameMode]": "casual,ranked" - } - } - jobid = (await queue.request(jobtype="grab", - payload=[payload], - priority=[1]))[0] - - while True: - # wait for grab job to finish - status = await queue.status(jobid) - if status == 'finished': - break - if status == 'failed': - logging.warning("'%s': not found", name) - if not in_cache: - await bot.edit_message(msgid, - "Could not find you.") - return - asyncio.sleep(0.5) - - while True: - # wait for processor to update the player - data = await con.fetchrow(query, name, lmcd) - if data is not None: + bot_response = await bot.say("Loading…") + # TODO this is shitty. Shouldn't need globals to keep track of updates + # also, this is buggy. + # TODO: Use a Python-friendly alternative to socketio + global do_update, wait_for_update + wait_for_update = True # continue polling + do_update = True # poll immediately + + async def request_update(): + global wait_for_update + # request an update via Vainsocial API + api_con = http.client.HTTPConnection("localhost", 8080) + api_con.request("HEAD", "/api/player/name/" + name) + api_resp = api_con.getresponse() + logging.info("%s: API responded with status %i", + name, api_resp.status) + if api_resp.status == 404: + wait_for_update = False + await bot.edit_message(bot_response, + "Could not find you.") + api_con.close() + + def update_available(*args): + global do_update, wait_for_update + # TODO here is the problem ^ don't work in closures :( + if args[0] in ["process_finished", "compile_finished"]: + do_update = True + if args[0] in ["grab_failed", "process_finished", + "compile_finished"]: + wait_for_update = False + + io = SocketIO("localhost", 8080, LoggingNamespace) + io.on(name, update_available) + + asyncio.ensure_future(request_update()) + + has_embed = False + for _ in range(10): # try for 10s + if do_update: + logging.info("%s: updating bot response", + name) + do_update = False + data = await con.fetchrow(query, name) + if data is not None: + has_embed = True + await bot.edit_message(bot_response, + embed=emb(data)) + else: + logging.info("%s: no data in db, waiting for API", + name) + if not wait_for_update: break - asyncio.sleep(0.5) + io.wait(seconds=1) - await bot.edit_message(msgid, embed=emb(data)) + if has_embed: + await bot.edit_message(bot_response, "Up to date.") + # if not, 404 logging.basicConfig(level=logging.INFO) -- cgit v1.3.1 From 7a69ec99048c4f08b6623527c0271d7fb01c0072 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 25 Mar 2017 18:20:16 +0100 Subject: wait for 'done' event --- main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 81dcaa3..f1b94ab 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# TODO one request blocks the whole event loop. import logging import os @@ -150,8 +151,7 @@ async def vainsocial(name: str): # TODO here is the problem ^ don't work in closures :( if args[0] in ["process_finished", "compile_finished"]: do_update = True - if args[0] in ["grab_failed", "process_finished", - "compile_finished"]: + if args[0] == "done": wait_for_update = False io = SocketIO("localhost", 8080, LoggingNamespace) @@ -160,7 +160,7 @@ async def vainsocial(name: str): asyncio.ensure_future(request_update()) has_embed = False - for _ in range(10): # try for 10s + for _ in range(30): # try for s if do_update: logging.info("%s: updating bot response", name) @@ -175,6 +175,7 @@ async def vainsocial(name: str): name) if not wait_for_update: break + io.wait(seconds=1) if has_embed: -- cgit v1.3.1 From daecd8249d8e4c6b50dfe2541b9787c954b1d1e7 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 25 Mar 2017 19:19:33 +0100 Subject: do not block main loop --- main.py | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/main.py b/main.py index f1b94ab..db36495 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -# TODO one request blocks the whole event loop. import logging import os +import concurrent.futures import http -import json import asyncio import asyncpg import discord @@ -19,12 +18,15 @@ db = { "database": os.environ.get("POSTGRESQL_DEST_DB") or "vainsocial-web" } +TIMEOUT = 120 # s until giving up on API bot = commands.Bot( command_prefix="?", description="Vainsocial Vainglory stats bot") pool = None +threadpool = concurrent.futures.ThreadPoolExecutor( + max_workers=10) @bot.event @@ -125,15 +127,10 @@ async def vainsocial(name: str): await bot.type() bot_response = await bot.say("Loading…") - # TODO this is shitty. Shouldn't need globals to keep track of updates - # also, this is buggy. - # TODO: Use a Python-friendly alternative to socketio - global do_update, wait_for_update - wait_for_update = True # continue polling - do_update = True # poll immediately + vainsocial.wait_for_update = True # continue polling + vainsocial.do_update = True # poll immediately async def request_update(): - global wait_for_update # request an update via Vainsocial API api_con = http.client.HTTPConnection("localhost", 8080) api_con.request("HEAD", "/api/player/name/" + name) @@ -141,46 +138,43 @@ async def vainsocial(name: str): logging.info("%s: API responded with status %i", name, api_resp.status) if api_resp.status == 404: - wait_for_update = False + vainsocial.wait_for_update = False await bot.edit_message(bot_response, "Could not find you.") api_con.close() def update_available(*args): - global do_update, wait_for_update - # TODO here is the problem ^ don't work in closures :( if args[0] in ["process_finished", "compile_finished"]: - do_update = True + # do partial update + vainsocial.do_update = True if args[0] == "done": - wait_for_update = False + vainsocial.wait_for_update = False + io = SocketIO("localhost", 8080, LoggingNamespace) io.on(name, update_available) asyncio.ensure_future(request_update()) + asyncio.ensure_future( + bot.loop.run_in_executor(threadpool, io.wait, TIMEOUT)) has_embed = False - for _ in range(30): # try for s - if do_update: - logging.info("%s: updating bot response", - name) - do_update = False + for _ in range(TIMEOUT): + if vainsocial.do_update: + logging.info("%s: updating bot response", name) + vainsocial.do_update = False data = await con.fetchrow(query, name) - if data is not None: + if data: has_embed = True await bot.edit_message(bot_response, embed=emb(data)) - else: - logging.info("%s: no data in db, waiting for API", - name) - if not wait_for_update: + if not vainsocial.wait_for_update: break - - io.wait(seconds=1) + await asyncio.sleep(0.1) if has_embed: await bot.edit_message(bot_response, "Up to date.") - # if not, 404 + # if not, it was a 404 logging.basicConfig(level=logging.INFO) -- cgit v1.3.1 From aac10c20daf635f8880b919c46540d080642b143 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 25 Mar 2017 19:22:00 +0100 Subject: remove joblib dependency --- .gitmodules | 3 --- joblib | 1 - 2 files changed, 4 deletions(-) delete mode 160000 joblib diff --git a/.gitmodules b/.gitmodules index 47695ea..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "joblib"] - path = joblib - url = https://gitlab.com/vainglorygame/joblib.git diff --git a/joblib b/joblib deleted file mode 160000 index 049d73f..0000000 --- a/joblib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 049d73f217dfc881b89be23bc23d33d995bdea42 -- cgit v1.3.1 From 98a9fcc38b62fa12049003b1913ac5a81578f474 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 25 Mar 2017 20:54:34 +0100 Subject: update requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 7c3bc62..427dc0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,8 @@ discord.py==0.16.7 multidict==2.1.4 packaging==16.8 pyparsing==2.1.10 +requests==2.13.0 six==1.10.0 +socketIO-client==0.7.2 +websocket-client==0.40.0 websockets==3.2 -- cgit v1.3.1 From bf068ebf9e86f527474e30bdaf4a74503f11d608 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 25 Mar 2017 20:55:32 +0100 Subject: accidentally changed prefix --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index db36495..0b81fa8 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ db = { TIMEOUT = 120 # s until giving up on API bot = commands.Bot( - command_prefix="?", + command_prefix="!", description="Vainsocial Vainglory stats bot") pool = None -- cgit v1.3.1 From 291cf5046f1497c49b0266d0d818882a2b96d5d4 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 26 Mar 2017 23:13:32 +0200 Subject: fix timeout occuring too early, kill websocket when it's not needed anymore --- main.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 0b81fa8..a80dc92 100644 --- a/main.py +++ b/main.py @@ -154,12 +154,13 @@ async def vainsocial(name: str): io = SocketIO("localhost", 8080, LoggingNamespace) io.on(name, update_available) - asyncio.ensure_future(request_update()) - asyncio.ensure_future( + socket_waiter = asyncio.ensure_future( bot.loop.run_in_executor(threadpool, io.wait, TIMEOUT)) + asyncio.ensure_future(request_update()) has_embed = False - for _ in range(TIMEOUT): + poll_frequency = 5 # Hz + for _ in range(TIMEOUT*poll_frequency): if vainsocial.do_update: logging.info("%s: updating bot response", name) vainsocial.do_update = False @@ -170,7 +171,9 @@ async def vainsocial(name: str): embed=emb(data)) if not vainsocial.wait_for_update: break - await asyncio.sleep(0.1) + await asyncio.sleep(1.0/poll_frequency) + + socket_waiter.cancel() # stop listening on websocket if has_embed: await bot.edit_message(bot_response, "Up to date.") -- cgit v1.3.1 From 0564241327ecc6eb08cd716f510ebb0afbb3eeb3 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 11 Apr 2017 21:21:22 +0200 Subject: draft: rewrite in node --- .gitignore | 129 +++++++++++------------------ api.js | 73 ++++++++++++++++ bot.js | 66 +++++++++++++++ commands/vainsocial/match.js | 41 +++++++++ commands/vainsocial/matches.js | 34 ++++++++ commands/vainsocial/user.js | 33 ++++++++ main.py | 184 ----------------------------------------- package.json | 27 ++++++ requirements.txt | 14 ---- responses.js | 156 ++++++++++++++++++++++++++++++++++ settings.sqlite3 | Bin 0 -> 8192 bytes 11 files changed, 478 insertions(+), 279 deletions(-) create mode 100644 api.js create mode 100644 bot.js create mode 100644 commands/vainsocial/match.js create mode 100644 commands/vainsocial/matches.js create mode 100644 commands/vainsocial/user.js delete mode 100644 main.py create mode 100644 package.json delete mode 100644 requirements.txt create mode 100644 responses.js create mode 100644 settings.sqlite3 diff --git a/.gitignore b/.gitignore index a3401aa..2788ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,93 +1,60 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: +# Logs +logs *.log -!/logs -!run*.log -local_settings.py +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# Flask stuff: -instance/ -.webassets-cache +# Runtime data +pids +*.pid +*.seed +*.pid.lock -# Scrapy stuff: -.scrapy +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov -# Sphinx documentation -docs/_build/ +# Coverage directory used by tools like istanbul +coverage -# PyBuilder -target/ +# nyc test coverage +.nyc_output -# Jupyter Notebook -.ipynb_checkpoints +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt -# pyenv -.python-version +# Bower dependency directory (https://bower.io/) +bower_components -# celery beat schedule file -celerybeat-schedule +# node-waf configuration +.lock-wscript -# dotenv -.env +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm -# virtualenv -.venv/ -venv/ -ENV/ +# Optional eslint cache +.eslintcache -# Spyder project settings -.spyderproject +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env -# Rope project settings -.ropeproject +.sqlite3 diff --git a/api.js b/api.js new file mode 100644 index 0000000..fef1ffe --- /dev/null +++ b/api.js @@ -0,0 +1,73 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const request = require("request-promise-native"), + WebSocket = require("ws"), + webstomp = require("webstomp-client"), + Channel = require("async-csp").Channel; + +const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bots/api", + API_WS_URL = process.env.API_WS_URL || "ws://vainsocial.dev/ws", + API_BE_URL = process.env.API_BE_URL || "http://vainsocial.dev/bridge"; + +const ws = new WebSocket(API_WS_URL, { perMessageDeflate: false }), + notif = webstomp.over(ws); + +function connect() { + notif.connect("web", "web", + () => console.log("connected to queue"), + (err) => console.error("error connecting to queue", err)); + keepalive(); +} + +function keepalive() { + ws.ping("", false, true); + setTimeout(this, 60000); +} + +ws.on("ready", connect); + +// TODO use keepalive / connection pool +async function getFE(url) { + return await request({ + uri: API_FE_URL + url, + json: true + }); +} + +async function postBE(url) { + return await request.post({ + uri: API_BE_URL + url, + json: true + }); +} + +function subscribe(topic, channel) { + notif.subscribe("/topic/" + topic, async (msg) => { + await channel.put(msg.body); + msg.ack(); + }, {"ack": "client"}); +} + +module.exports.searchPlayer = async function (name, timeout=30) { + let channel = new Channel(); + + subscribe("player." + name, channel); + await postBE("/player/" + name + "/search"); + setTimeout(() => channel.close(), timeout*1000); + channel.put(""); // initial fetch + + let generator = async () => { + let msg = await channel.take(); + if (msg == "search_fail") { + throw "not found"; + } + if (msg == Channel.DONE) { + throw "exhausted"; + } + return await getFE("/player/" + name); + } + + return { next: generator }; +} diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..c3826c1 --- /dev/null +++ b/bot.js @@ -0,0 +1,66 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const sqlite = require("sqlite"), + path = require("path"), + Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + client = new Commando.Client({ + owner: "227440521704898561", // shutterfly + commandPrefix: "?" + }); + +const DISCORDTOKEN = process.env.DISCORDTOKEN || "Mjg5ODM2OTAwOTg5MDA5OTIw.C6SLKA.j8UETpPHztDV45xicf11hwpwNK8"; + +client + .on("error", console.error) + .on("warn", console.warn) + .on("debug", console.log) + .on("ready", () => { + console.log(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); + }) + .on('disconnect', () => { console.warn('Disconnected!'); }) + .on('reconnecting', () => { console.warn('Reconnecting...'); }) + .on('commandError', (cmd, err) => { + if(err instanceof Commando.FriendlyError) return; + console.error(`Error in command ${cmd.groupID}:${cmd.memberName}`, err); + }) + .on('commandBlocked', (msg, reason) => { + console.log(oneLine` + Command ${msg.command ? `${msg.command.groupID}:${msg.command.memberName}` : ''} + blocked; ${reason} + `); + }) + .on('commandPrefixChange', (guild, prefix) => { + console.log(oneLine` + Prefix ${prefix === '' ? 'removed' : `changed to ${prefix || 'the default'}`} + ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. + `); + }) + .on('commandStatusChange', (guild, command, enabled) => { + console.log(oneLine` + Command ${command.groupID}:${command.memberName} + ${enabled ? 'enabled' : 'disabled'} + ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. + `); + }) + .on('groupStatusChange', (guild, group, enabled) => { + console.log(oneLine` + Group ${group.id} + ${enabled ? 'enabled' : 'disabled'} + ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. + `); + }); + +client.setProvider( + sqlite.open(path.join(__dirname, "settings.sqlite3")).then( + db => new Commando.SQLiteProvider(db))); + +client.registry + .registerGroup('vainsocial', 'VainSocial') + .registerDefaults() + .registerCommandsIn(path.join(__dirname, 'commands')); + + +client.login(DISCORDTOKEN); diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js new file mode 100644 index 0000000..14b06a2 --- /dev/null +++ b/commands/vainsocial/match.js @@ -0,0 +1,41 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + responses = require("../../responses"); + +module.exports = class ShowMatchCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-match", + aliases: ["vm"], + group: "vainsocial", + memberName: "vainsocial-match", + description: "Show a user's match in detail.", + details: oneLine` + todo + `, + examples: ["vm shutterfly 1"], + argsType: "multiple", + argsCount: 2, + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your in game name (Case Sensitive).", + type: "string" + }, { + key: "number", + label: "number", + prompt: "Please specify how far you want to go back in history. Use 1 or leave out for the latest match.", + type: "integer", + default: 1 + } ] + }); + } + async run(msg, args) { + await responses.showMatch(msg, args); + } +}; diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js new file mode 100644 index 0000000..2660d73 --- /dev/null +++ b/commands/vainsocial/matches.js @@ -0,0 +1,34 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + responses = require("../../responses"); + +module.exports = class ShowMatchesCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-history", + aliases: ["vh"], + group: "vainsocial", + memberName: "vainsocial-matches", + description: "Show a user's match history.", + details: oneLine` + todo + `, + examples: ["vh shutterfly"], + argsType: "single", + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your in game name (Case Sensitive).", + type: "string" + } ] + }); + } + async run(msg, args) { + await responses.showMatches(msg, args); + } +}; diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js new file mode 100644 index 0000000..f544a5e --- /dev/null +++ b/commands/vainsocial/user.js @@ -0,0 +1,33 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + responses = require("../../responses"); + +module.exports = class ShowUserCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-user", + aliases: ["v", "vu"], + group: "vainsocial", + memberName: "vainsocial-user", + description: "Show a player\'s profile.", + details: oneLine` + Display VainSocial lifetime statistics from Vainglory + `, + examples: ["vu shutterfly"], + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your in game name (Case Sensitive).", + type: "string" + } ] + }); + } + async run(msg, args) { + await responses.showUser(msg, args); + } +}; diff --git a/main.py b/main.py deleted file mode 100644 index a80dc92..0000000 --- a/main.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import os -import concurrent.futures -import http -import asyncio -import asyncpg -import discord -from discord.ext import commands -from socketIO_client import SocketIO, LoggingNamespace - -db = { - "host": os.environ.get("POSTGRESQL_DEST_HOST") or "localhost", - "port": os.environ.get("POSTGRESQL_DEST_PORT") or 5432, - "user": os.environ.get("POSTGRESQL_DEST_USER") or "vainweb", - "password": os.environ.get("POSTGRESQL_DEST_PASSWORD") or "vainweb", - "database": os.environ.get("POSTGRESQL_DEST_DB") or "vainsocial-web" -} - -TIMEOUT = 120 # s until giving up on API - -bot = commands.Bot( - command_prefix="!", - description="Vainsocial Vainglory stats bot") - -pool = None -threadpool = concurrent.futures.ThreadPoolExecutor( - max_workers=10) - - -@bot.event -async def on_ready(): - logging.warning("Logged in as %s (" + - "https://discordapp.com/oauth2/authorize?&" + - "client_id=%s&scope=bot)", - bot.user.name, bot.user.id) - logging.warning("connecting to database") - global pool - pool = await asyncpg.create_pool( - min_size=2, **db) - await bot.change_presence( - game=discord.Game( - name="vainsocial.com")) - - -@bot.command() -async def about(): - """Print invite links.""" - emb = discord.Embed( - title="Vainsocial Discord bot", - description="Built by the Vainsocial development team using the MadGlory API. Currently running on %i servers." % (len(bot.servers),) - ) - emb.add_field(name="Website", - value="[vainsocial.com](https://vainsocial.com/?utm_source=discord&utm_medium=vainsocial)") - emb.add_field(name="Bot invite link", - value="[discordapp.com](https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot)") - emb.add_field(name="Developer Discord", - value="[discord.me/vainsocial](https://discord.me/vainsocial)") - emb.add_field(name="Twitter", - value="[twitter/vainsocial](https://twitter.com/vainsocial)") - await bot.say(embed=emb) - - -@bot.command(aliases=["v", "vain"]) -async def vainsocial(name: str): - """Retrieves a player's stats.""" - query = """ - SELECT - player.name, - player.shard_id, - match.game_mode, - roster.match_api_id, - participant.hero, participant.winner, - participant.kills, participant.deaths, participant.assists, participant.farm, - player.skill_tier, player.played, player.wins, - player.last_match_created_date::text - FROM match, roster, participant, player WHERE - match.api_id=roster.match_api_id AND - roster.api_id=participant.roster_api_id AND - participant.player_api_id=player.api_id AND - player.name=$1 - ORDER BY match.created_at DESC - LIMIT 1 - """ - def emb(dct): - data = dict(dct) - - modes = { - "blitz_pvp_ranked": "Blitz", - "casual_aral": "Battle Royale", - "private": "private casual", - "private_party_draft_match": "private draft", - "private_party_blitz_match": "private Blitz", - "private_party_aral_match": "private Battle Royale" - } - data["mode"] = modes.get(data["game_mode"]) or data["game_mode"] - heroes = { - "Sayoc": "Taka", - "Hero009": "Krul", - "Hero010": "Skaarf", - "Hero016": "Rona" - } - data["hero"] = data["hero"].replace("*", "") - data["hero"] = heroes.get(data["hero"]) or data["hero"] - data["result"] = "won" if data["winner"] else "lost" - - emb = discord.Embed( - title="%(name)s (%(shard_id)s)" % data, - description="Last match registered (GMT): %(last_match_created_date)s" % data, - url="https://vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial" % data - ) - emb.set_author(name="Vainsocial", - url="https://vainsocial.com") - emb.add_field(name="Profile", - value=("%(wins)i wins / %(played)i games\n" + - "[View on vainsocial.com](https://vainsocial.com/players/%(shard_id)s/%(name)s/?utm_source=discord&utm_medium=vainsocial)") % data) - emb.add_field(name="Last match", - value=("%(result)s %(mode)s as %(hero)s %(kills)i/%(deaths)i/%(assists)i\n" + - "[View on vainsocial.com](https://vainsocial.com/matches/%(match_api_id)s/?utm_source=discord&utm_medium=vainsocial)") % data) - - emb.set_footer(text="Vainsocial - Vainglory social stats service") - emb.set_thumbnail(url="https://vainsocial.com/images/game/skill_tiers/%(skill_tier)s.png" % data) - return emb - - async with pool.acquire() as con: - await bot.type() - - bot_response = await bot.say("Loading…") - vainsocial.wait_for_update = True # continue polling - vainsocial.do_update = True # poll immediately - - async def request_update(): - # request an update via Vainsocial API - api_con = http.client.HTTPConnection("localhost", 8080) - api_con.request("HEAD", "/api/player/name/" + name) - api_resp = api_con.getresponse() - logging.info("%s: API responded with status %i", - name, api_resp.status) - if api_resp.status == 404: - vainsocial.wait_for_update = False - await bot.edit_message(bot_response, - "Could not find you.") - api_con.close() - - def update_available(*args): - if args[0] in ["process_finished", "compile_finished"]: - # do partial update - vainsocial.do_update = True - if args[0] == "done": - vainsocial.wait_for_update = False - - - io = SocketIO("localhost", 8080, LoggingNamespace) - io.on(name, update_available) - - socket_waiter = asyncio.ensure_future( - bot.loop.run_in_executor(threadpool, io.wait, TIMEOUT)) - asyncio.ensure_future(request_update()) - - has_embed = False - poll_frequency = 5 # Hz - for _ in range(TIMEOUT*poll_frequency): - if vainsocial.do_update: - logging.info("%s: updating bot response", name) - vainsocial.do_update = False - data = await con.fetchrow(query, name) - if data: - has_embed = True - await bot.edit_message(bot_response, - embed=emb(data)) - if not vainsocial.wait_for_update: - break - await asyncio.sleep(1.0/poll_frequency) - - socket_waiter.cancel() # stop listening on websocket - - if has_embed: - await bot.edit_message(bot_response, "Up to date.") - # if not, it was a 404 - - -logging.basicConfig(level=logging.INFO) -bot.run(os.environ["VAINSOCIAL_DISCORDTOKEN"]) diff --git a/package.json b/package.json new file mode 100644 index 0000000..ddf79e4 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "discordbot", + "version": "2.0.0", + "description": "", + "main": "bot.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "schneefux", + "license": "UNLICENSED", + "dependencies": { + "async-csp": "^0.5.0", + "bufferutil": "^3.0.0", + "discord-emoji": "^1.1.1", + "discord.js": "^11.0.0", + "discord.js-commando": "^0.9.0", + "erlpack": "github:hammerandchisel/erlpack", + "request": "^2.81.0", + "request-promise": "^4.2.0", + "request-promise-native": "^1.0.3", + "sqlite": "^2.5.0", + "uws": "^0.14.1", + "webstomp": "0.0.10", + "webstomp-client": "^1.0.6", + "ws": "^2.2.3" + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 427dc0d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -aiohttp==1.0.5 -appdirs==1.4.2 -async-timeout==1.1.0 -asyncpg==0.9.0 -chardet==2.3.0 -discord.py==0.16.7 -multidict==2.1.4 -packaging==16.8 -pyparsing==2.1.10 -requests==2.13.0 -six==1.10.0 -socketIO-client==0.7.2 -websocket-client==0.40.0 -websockets==3.2 diff --git a/responses.js b/responses.js new file mode 100644 index 0000000..f54ea89 --- /dev/null +++ b/responses.js @@ -0,0 +1,156 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + Discord = require("discord.js"), + emoji = require("discord-emoji"), + oneLine = require("common-tags").oneLine, + api = require("./api"); + +function vainsocialEmbed(title, link) { + return new Discord.RichEmbed() + .setTitle(title) + .setURL("https://vainsocial.com/" + link) + .setColor("#55ADD3") + .setAuthor("VainSocial", null, "https://vainsocial.com") + .setFooter("VainSocial - Vainglory social stats service") + ; +} + +// returns the shortest version of the usage help +// just '?vm' +function usg(msg, cmd) { + return msg.anyUsage(cmd, undefined, null); +} + +// TODO, obviously + +// show player profile and last match +module.exports.showUser = async (msg, args) => { + let ign = args.name, + iterator = await api.searchPlayer(ign), + response, data; + while (true) { + try { + data = await iterator.next(); + } catch (err) { + if (err.statusCode == 404) { + await msg.reply("Could not find `" + ign + "`"); + break; + } + if (err == "exhausted") + break; + } + let winstr = "won"; + if (data.last_result[0].winner == false) winstr = "lost"; + + let embed = vainsocialEmbed(ign, "player/" + ign) + .setThumbnail("https://vainsocial.com/images/game/skill_tiers/" + + data.skill_tier + ".png") + .setDescription("") + .addField("Profile", ` + (Player stats will be here) + `) + .addField("Last match", oneLine` + Played ${data.last_result[0].game_mode_id} with ${data.last_result[0].actor}, ${winstr} + *${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for more* + `) + .setTimestamp(new Date(data.last_match_created_date)) + ; + + if (response == undefined) { + response = await msg.replyEmbed(embed); + await response.react(emoji.symbols.information_source); + msg.client.on("messageReactionAdd", async (react) => { + if (react.message != response) return; + if (react.emoji != emoji.symbols.information_source) return; + await showMatch(msg, { + name: ign, + id: data.last_result[0].match_api_id + }); + }); + } else { + response = await msg.editResponse(response, { + type: "plain", + content: "", + options: { embed: embed } + }); + } + } +} + +// show match in detail +let showMatch = module.exports.showMatch = async (msg, args) => { + let response, + ign = args.name, + id = args.id, + index = args.number, + match; + if (id != null) match = {}; + else match = {}; // fetch match from API + let embed = vainsocialEmbed("A Match", "match/" + 12345) + .setDescription("A lot of stuff happened here. This was ranked or casual?") + .addField("Left team", ` + Killed some heroes from the right team and someone died + `) + .addField("Right team", ` + Did good work too. Poor minions. + `) + //.setTimestamp(new Date(data.last_match_created_date)) + ; + response = await msg.editResponse(response, { + type: "plain", + content: "", + options: { embed: embed } + }); +} + +// show match history +module.exports.showMatches = async (msg, args) => { + let ign = args.name, + response; + let 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 + ]; + let embed = vainsocialEmbed(ign, "player/" + ign) + .setDescription(` + Last 1337 casual and ranked matches. + *${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign)} number for details* + `) + .addField("Match 1", ` + Blablabla. Blabla. + `) + .addField("Match 2", ` + Ooooh did a Minion just walk into our base? + `) + ; + response = await msg.editResponse(response, { + type: "plain", + content: "", + options: { embed: embed } + }); + + msg.client.on("messageReactionAdd", async (react) => { + if (react.message != response) return; + if (react.count == 1) return; // was me + let idx = count.indexOf(react.emoji.name); + if (idx == -1) return; + await showMatch(msg, { + name: ign, + number: idx + }); + }); + + for (let num of count) + await response.react(num) +} diff --git a/settings.sqlite3 b/settings.sqlite3 new file mode 100644 index 0000000..15ce44e Binary files /dev/null and b/settings.sqlite3 differ -- cgit v1.3.1 From 795126408fc717a11d832f412470597377d71449 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 15 Apr 2017 17:07:20 +0200 Subject: implement basic functionality --- api.js | 70 +++++++++------ bot.js | 9 +- commands/vainsocial/match.js | 8 +- commands/vainsocial/matches.js | 4 +- commands/vainsocial/user.js | 4 +- package.json | 2 +- responses.js | 187 +++++++++++++++++++++++++++++------------ 7 files changed, 198 insertions(+), 86 deletions(-) diff --git a/api.js b/api.js index fef1ffe..37ed6ae 100644 --- a/api.js +++ b/api.js @@ -11,63 +11,77 @@ const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bots/api", API_WS_URL = process.env.API_WS_URL || "ws://vainsocial.dev/ws", API_BE_URL = process.env.API_BE_URL || "http://vainsocial.dev/bridge"; -const ws = new WebSocket(API_WS_URL, { perMessageDeflate: false }), - notif = webstomp.over(ws); +const notif = webstomp.over(new WebSocket(API_WS_URL, + { perMessageDeflate: false })); -function connect() { +(function connect() { notif.connect("web", "web", () => console.log("connected to queue"), - (err) => console.error("error connecting to queue", err)); - keepalive(); -} - -function keepalive() { - ws.ping("", false, true); - setTimeout(this, 60000); -} - -ws.on("ready", connect); + (err) => connect() + ); +})(); // TODO use keepalive / connection pool -async function getFE(url) { - return await request({ +function getFE(url) { + return request({ uri: API_FE_URL + url, json: true }); } -async function postBE(url) { - return await request.post({ +function postBE(url) { + return request.post({ uri: API_BE_URL + url, json: true }); } function subscribe(topic, channel) { - notif.subscribe("/topic/" + topic, async (msg) => { - await channel.put(msg.body); + return notif.subscribe("/topic/" + topic, (msg) => { + channel.put(msg.body); msg.ack(); }, {"ack": "client"}); } -module.exports.searchPlayer = async function (name, timeout=30) { - let channel = new Channel(); - - subscribe("player." + name, channel); - await postBE("/player/" + name + "/search"); - setTimeout(() => channel.close(), timeout*1000); - channel.put(""); // initial fetch +// be an async generator +// next() returns player data whenever an update is available +module.exports.searchPlayer = async function (name, timeout=60) { + let channel = new Channel(), + subscription = subscribe("player." + name, channel); + await postBE("/player/" + name + "/update"); + // stop updates after timeout + setTimeout(() => { + channel.close(); + subscription.unsubscribe(); + }, timeout*1000); + channel.put("initial"); // initial fetch let generator = async () => { - let msg = await channel.take(); + let msg; + while (true) { + msg = await channel.take(); + if (["initial", "search_fail", "stats_update", + "matches_update"].indexOf(msg) != -1) + break; + } if (msg == "search_fail") { throw "not found"; } if (msg == Channel.DONE) { throw "exhausted"; } - return await getFE("/player/" + name); + return getFE("/player/" + name); } return { next: generator }; } + +// return matches +module.exports.searchMatches = async function (name) { + return getFE("/player/" + name + "/matches/1.1.1.1"); +} + +// return single match +module.exports.searchMatch = async function (id) { + return getFE("/match/" + id); +} diff --git a/bot.js b/bot.js index c3826c1..bb2e9b2 100644 --- a/bot.js +++ b/bot.js @@ -19,6 +19,7 @@ client .on("debug", console.log) .on("ready", () => { console.log(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); + client.user.setGame("?v shutterflySEA | vainsocial.com"); }) .on('disconnect', () => { console.warn('Disconnected!'); }) .on('reconnecting', () => { console.warn('Reconnecting...'); }) @@ -59,8 +60,14 @@ client.setProvider( client.registry .registerGroup('vainsocial', 'VainSocial') - .registerDefaults() + .registerDefaultTypes() + .registerDefaultGroups() + .registerDefaultCommands({ eval_: false, commandState: false }) .registerCommandsIn(path.join(__dirname, 'commands')); client.login(DISCORDTOKEN); + +process.on("unhandledRejection", err => { + console.error("Uncaught Promise Error: \n" + err.stack); +}); diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js index 14b06a2..c8167ea 100644 --- a/commands/vainsocial/match.js +++ b/commands/vainsocial/match.js @@ -25,13 +25,17 @@ module.exports = class ShowMatchCommand extends Commando.Command { key: "name", label: "name", prompt: "Please specify your in game name (Case Sensitive).", - type: "string" + type: "string", + min: 3, + max: 16 }, { key: "number", label: "number", prompt: "Please specify how far you want to go back in history. Use 1 or leave out for the latest match.", type: "integer", - default: 1 + default: 1, + min: 1, + max: 10 } ] }); } diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js index 2660d73..3f2de2f 100644 --- a/commands/vainsocial/matches.js +++ b/commands/vainsocial/matches.js @@ -24,7 +24,9 @@ module.exports = class ShowMatchesCommand extends Commando.Command { key: "name", label: "name", prompt: "Please specify your in game name (Case Sensitive).", - type: "string" + type: "string", + min: 3, + max: 16 } ] }); } diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js index f544a5e..1f41c12 100644 --- a/commands/vainsocial/user.js +++ b/commands/vainsocial/user.js @@ -23,7 +23,9 @@ module.exports = class ShowUserCommand extends Commando.Command { key: "name", label: "name", prompt: "Please specify your in game name (Case Sensitive).", - type: "string" + type: "string", + min: 3, + max: 16 } ] }); } diff --git a/package.json b/package.json index ddf79e4..6630525 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "async-csp": "^0.5.0", "bufferutil": "^3.0.0", "discord-emoji": "^1.1.1", - "discord.js": "^11.0.0", + "discord.js": "git+https://github.com/hydrabolt/discord.js.git", "discord.js-commando": "^0.9.0", "erlpack": "github:hammerandchisel/erlpack", "request": "^2.81.0", diff --git a/responses.js b/responses.js index f54ea89..9945861 100644 --- a/responses.js +++ b/responses.js @@ -14,7 +14,7 @@ function vainsocialEmbed(title, link) { .setURL("https://vainsocial.com/" + link) .setColor("#55ADD3") .setAuthor("VainSocial", null, "https://vainsocial.com") - .setFooter("VainSocial - Vainglory social stats service") + .setFooter("VainSocial") ; } @@ -24,52 +24,119 @@ function usg(msg, cmd) { return msg.anyUsage(cmd, undefined, null); } -// TODO, obviously +// 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 +function formatMatch(participant) { + let winstr = "Won"; + if (!participant.winner) winstr = "Lost"; + return ` +${winstr} ${participant.game_mode_id} with \`${participant.actor.replace(/\*/g, "")}\` +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 +function formatMatchDetail(match) { + let strings = []; + for(let roster of match.rosters) { + let rosterstr = `${roster.side} - \`${roster.hero_kills}\` Kills`; + let teamstr = ""; + for(let participant of roster.participants) { + teamstr += ` +\`${participant.actor.replace(/\*/g, "")}\`, [${participant.player.name}](https://vainsocial.com/player/${participant.player.name}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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 +function formatPlayer(player) { + let 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}\` + `, + best_hero = "", + picks = ""; + if (player.best_hero.length > 0) + best_hero = oneLine` + Best | \`${player.best_hero[0].name}\` + `; + if (player.picks.length > 0) + picks = oneLine` + Favorite | \`${player.picks[0].name}\`, \`${player.picks[0].hero_pick} picks\` + `; + return ` +${stats} +${total_kda} +${best_hero} +${picks} + `; +} // show player profile and last match module.exports.showUser = async (msg, args) => { let ign = args.name, iterator = await api.searchPlayer(ign), - response, data; + response, player; while (true) { try { - data = await iterator.next(); + player = await iterator.next(); } catch (err) { - if (err.statusCode == 404) { - await msg.reply("Could not find `" + ign + "`"); + if (err == "not found") { + await msg.say("Could not find player `" + ign + "`."); break; } if (err == "exhausted") break; + if (err.statusCode == 404) + continue; } - let winstr = "won"; - if (data.last_result[0].winner == false) winstr = "lost"; + let matches = (await api.searchMatches(ign)).data; + let matchstr = "not available"; + if (matches.length > 0) matchstr = formatMatch(matches[0]); - let embed = vainsocialEmbed(ign, "player/" + ign) + let embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) .setThumbnail("https://vainsocial.com/images/game/skill_tiers/" + - data.skill_tier + ".png") + player.skill_tier + ".png") .setDescription("") - .addField("Profile", ` - (Player stats will be here) - `) - .addField("Last match", oneLine` - Played ${data.last_result[0].game_mode_id} with ${data.last_result[0].actor}, ${winstr} - *${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for more* - `) - .setTimestamp(new Date(data.last_match_created_date)) + .addField("Profile", formatPlayer(player), true) + .addField("Last match", matchstr + ` +*${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more* + `, true) + .setTimestamp(new Date(player.last_match_created_date)) ; if (response == undefined) { - response = await msg.replyEmbed(embed); - await response.react(emoji.symbols.information_source); + response = await msg.embed(embed); msg.client.on("messageReactionAdd", async (react) => { if (react.message != response) return; - if (react.emoji != emoji.symbols.information_source) return; - await showMatch(msg, { - name: ign, - id: data.last_result[0].match_api_id - }); + if (react.users.array().length <= 1) return; // was me + if (react.emoji.name == emoji.symbols.information_source) + await showMatch(msg, { + name: ign, + id: matches[0].match_api_id + }); + if (react.emoji.name == emoji.symbols["1234"]) + await showMatches(msg, { name: ign }); }); + await response.react(emoji.symbols.information_source); + await response.react(emoji.symbols["1234"]); } else { response = await msg.editResponse(response, { type: "plain", @@ -85,19 +152,26 @@ let showMatch = module.exports.showMatch = async (msg, args) => { let response, ign = args.name, id = args.id, - index = args.number, - match; - if (id != null) match = {}; - else match = {}; // fetch match from API - let embed = vainsocialEmbed("A Match", "match/" + 12345) - .setDescription("A lot of stuff happened here. This was ranked or casual?") - .addField("Left team", ` - Killed some heroes from the right team and someone died - `) - .addField("Right team", ` - Did good work too. Poor minions. - `) - //.setTimestamp(new Date(data.last_match_created_date)) + index = args.number; + + let match; + try { + if (id == undefined) + id = (await api.searchMatches(ign)).data[index - 1].match_api_id; + match = await api.searchMatch(id); + } catch (err) { + await msg.say(oneLine` + Ooops! I don't have any data for you yet. + Please take a look at your profile first! + ${usg(msg, "v " + ign)} + `); // TODO! + return; + } + let embed = vainsocialEmbed(`${match.game_mode}, \`${match.duration}\` minutes`, "match/" + id) + .setTimestamp(new Date(match.created_at)) + formatMatchDetail(match).forEach(([title, text]) => { + embed.addField(title, text, true); + }); ; response = await msg.editResponse(response, { type: "plain", @@ -107,7 +181,7 @@ let showMatch = module.exports.showMatch = async (msg, args) => { } // show match history -module.exports.showMatches = async (msg, args) => { +let showMatches = module.exports.showMatches = async (msg, args) => { let ign = args.name, response; let count = [ @@ -122,18 +196,27 @@ module.exports.showMatches = async (msg, args) => { emoji.symbols.nine, emoji.symbols.ten ]; + let data, matches, matches_num; + try { + data = await api.searchMatches(ign); + } catch (err) { + await msg.say(oneLine` + Ooops! I don't have any data for you yet. + Please take a look at your profile first! + ${usg(msg, "v " + ign)} + `); // TODO! + return; + } + matches = data.data.slice(0, 3); + matches_num = matches.length; + let embed = vainsocialEmbed(ign, "player/" + ign) .setDescription(` - Last 1337 casual and ranked matches. - *${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign)} number for details* - `) - .addField("Match 1", ` - Blablabla. Blabla. - `) - .addField("Match 2", ` - Ooooh did a Minion just walk into our base? - `) - ; + Last ${matches_num} casual and ranked matches. + *${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign + " number")} for details* + `); + matches.forEach((match, idx) => + embed.addField(`Match ${idx + 1}`, formatMatch(match))); response = await msg.editResponse(response, { type: "plain", content: "", @@ -142,15 +225,15 @@ module.exports.showMatches = async (msg, args) => { msg.client.on("messageReactionAdd", async (react) => { if (react.message != response) return; - if (react.count == 1) return; // was me + if (react.users.array().length <= 1) return; // was me let idx = count.indexOf(react.emoji.name); if (idx == -1) return; await showMatch(msg, { name: ign, - number: idx + id: matches[idx].match_api_id }); }); - for (let num of count) + for (let num of count.slice(0, matches_num)) await response.react(num) } -- cgit v1.3.1 From 4bd55e56b17a3b59b5a291b1bef66c74e3c6c608 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 15 Apr 2017 17:10:51 +0200 Subject: remove my token --- bot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.js b/bot.js index bb2e9b2..91eeca6 100644 --- a/bot.js +++ b/bot.js @@ -11,7 +11,7 @@ const sqlite = require("sqlite"), commandPrefix: "?" }); -const DISCORDTOKEN = process.env.DISCORDTOKEN || "Mjg5ODM2OTAwOTg5MDA5OTIw.C6SLKA.j8UETpPHztDV45xicf11hwpwNK8"; +const DISCORD_TOKEN = process.env.DISCORD_TOKEN; client .on("error", console.error) @@ -66,7 +66,7 @@ client.registry .registerCommandsIn(path.join(__dirname, 'commands')); -client.login(DISCORDTOKEN); +client.login(DISCORD_TOKEN); process.on("unhandledRejection", err => { console.error("Uncaught Promise Error: \n" + err.stack); -- cgit v1.3.1 From 1e759435cf539a8a422d91e07519c3aa1c5b838e Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 15 Apr 2017 17:11:15 +0200 Subject: add dockerfile --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9c3debd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:7.7-alpine + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY . /usr/src/app +RUN npm install && npm cache clean + +CMD ["node", "worker.js"] -- cgit v1.3.1 From ab3e74687ae926c48f3e13e3eb0703af8e0d3961 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 15 Apr 2017 17:17:43 +0200 Subject: fix dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9c3debd..326d1cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,4 @@ WORKDIR /usr/src/app COPY . /usr/src/app RUN npm install && npm cache clean -CMD ["node", "worker.js"] +CMD ["node", "bot.js"] -- cgit v1.3.1 From 7a6150f68888df9c110349dbae7173229037d470 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 16 Apr 2017 22:46:12 +0200 Subject: use participant skill tier on player profile --- .gitignore | 1 - responses.js | 10 +++++++--- settings.sqlite3 | Bin 8192 -> 0 bytes 3 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 settings.sqlite3 diff --git a/.gitignore b/.gitignore index 2788ca5..00cbbdf 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,3 @@ typings/ # dotenv environment variables file .env -.sqlite3 diff --git a/responses.js b/responses.js index 9945861..b0a9941 100644 --- a/responses.js +++ b/responses.js @@ -108,12 +108,16 @@ module.exports.showUser = async (msg, args) => { continue; } let matches = (await api.searchMatches(ign)).data; - let matchstr = "not available"; - if (matches.length > 0) matchstr = formatMatch(matches[0]); + let matchstr = "not available", + skill_tier = -1; + if (matches.length > 0) { + matchstr = formatMatch(matches[0]); + skill_tier = matches[0].skill_tier; + } let embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) .setThumbnail("https://vainsocial.com/images/game/skill_tiers/" + - player.skill_tier + ".png") + skill_tier + ".png") .setDescription("") .addField("Profile", formatPlayer(player), true) .addField("Last match", matchstr + ` diff --git a/settings.sqlite3 b/settings.sqlite3 deleted file mode 100644 index 15ce44e..0000000 Binary files a/settings.sqlite3 and /dev/null differ -- cgit v1.3.1 From 85bd3e9dd2d51ccc7db498fb7b470b1d7f6e2523 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 16 Apr 2017 22:47:23 +0200 Subject: use different playing status --- bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.js b/bot.js index 91eeca6..8b18d20 100644 --- a/bot.js +++ b/bot.js @@ -19,7 +19,7 @@ client .on("debug", console.log) .on("ready", () => { console.log(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); - client.user.setGame("?v shutterflySEA | vainsocial.com"); + client.user.setGame("?v shutterfly | vainsocial.com"); }) .on('disconnect', () => { console.warn('Disconnected!'); }) .on('reconnecting', () => { console.warn('Reconnecting...'); }) -- cgit v1.3.1 From 8ec0c0b3df0ba12688321d1f198cc4635e11a0e9 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 16 Apr 2017 22:53:45 +0200 Subject: get lmcd from last match --- responses.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/responses.js b/responses.js index b0a9941..18743be 100644 --- a/responses.js +++ b/responses.js @@ -109,10 +109,12 @@ module.exports.showUser = async (msg, args) => { } let matches = (await api.searchMatches(ign)).data; let matchstr = "not available", - skill_tier = -1; + skill_tier = -1, + last_match_date = new Date(); if (matches.length > 0) { matchstr = formatMatch(matches[0]); skill_tier = matches[0].skill_tier; + last_match_date = matches[0].created_at; } let embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) @@ -123,7 +125,7 @@ module.exports.showUser = async (msg, args) => { .addField("Last match", matchstr + ` *${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more* `, true) - .setTimestamp(new Date(player.last_match_created_date)) + .setTimestamp(last_match_date) ; if (response == undefined) { -- cgit v1.3.1 From 892c5bf4791c49b8f5cdf0f72add7ed6c3034c6e Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 16 Apr 2017 22:55:04 +0200 Subject: let lmcd be timestamp --- responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.js b/responses.js index 18743be..f12e643 100644 --- a/responses.js +++ b/responses.js @@ -114,7 +114,7 @@ module.exports.showUser = async (msg, args) => { if (matches.length > 0) { matchstr = formatMatch(matches[0]); skill_tier = matches[0].skill_tier; - last_match_date = matches[0].created_at; + last_match_date = new Date(matches[0].created_at); } let embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) -- cgit v1.3.1 From 3194e700332987e5cdd3e026bad930fc87dfd245 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 16 Apr 2017 22:56:41 +0200 Subject: small response style improvements --- responses.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/responses.js b/responses.js index f12e643..5800650 100644 --- a/responses.js +++ b/responses.js @@ -173,7 +173,7 @@ let showMatch = module.exports.showMatch = async (msg, args) => { `); // TODO! return; } - let embed = vainsocialEmbed(`${match.game_mode}, \`${match.duration}\` minutes`, "match/" + id) + let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, "match/" + id) .setTimestamp(new Date(match.created_at)) formatMatchDetail(match).forEach(([title, text]) => { embed.addField(title, text, true); @@ -218,8 +218,8 @@ let showMatches = module.exports.showMatches = async (msg, args) => { let embed = vainsocialEmbed(ign, "player/" + ign) .setDescription(` - Last ${matches_num} casual and ranked matches. - *${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign + " number")} for details* +Last ${matches_num} casual and ranked matches. +*${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign + " number")} for details* `); matches.forEach((match, idx) => embed.addField(`Match ${idx + 1}`, formatMatch(match))); -- cgit v1.3.1 From 7aa7a45e3f46fd1bc3a965c0c4a40bded288d7a8 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 17 Apr 2017 19:02:23 +0200 Subject: it's done --- api.js | 113 ++++++++++---- bot.js | 17 +- commands/about.js | 21 +++ commands/vainsocial/match.js | 3 - package.json | 2 + responses.js | 362 +++++++++++++++++++++++++++---------------- 6 files changed, 346 insertions(+), 172 deletions(-) create mode 100644 commands/about.js diff --git a/api.js b/api.js index 37ed6ae..a13eb8c 100644 --- a/api.js +++ b/api.js @@ -3,11 +3,19 @@ "use strict"; const request = require("request-promise-native"), + Promise = require("bluebird"), WebSocket = require("ws"), webstomp = require("webstomp-client"), + cacheManager = require("cache-manager"), Channel = require("async-csp").Channel; +let cache = cacheManager.caching({ + store: "memory", + ttl: 10 // s +}); + const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bots/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"; @@ -22,6 +30,13 @@ const notif = webstomp.over(new WebSocket(API_WS_URL, })(); // TODO use keepalive / connection pool +function getMap(url) { + return request({ + uri: API_MAP_URL + url, + json: true + }); +} + function getFE(url) { return request({ uri: API_FE_URL + url, @@ -43,45 +58,85 @@ function subscribe(topic, channel) { }, {"ack": "client"}); } -// be an async generator -// next() returns player data whenever an update is available -module.exports.searchPlayer = async function (name, timeout=60) { - let channel = new Channel(), +// return id<->name mappings +async function getMappings() { + return await cache.wrap("mappings", async () => { + let mapping = new Map(); + await Promise.map( + ["gamemode"], async (table) => { + mapping[table] = new Map(); + (await getMap(table)).map( + (map) => mapping[table]["id"] = map["name"]) + } + ); + return mapping; + }); +} + +module.exports.mapGameMode = async function(id) { + return (await getMappings())["gamemode"][id]; +} + +// be an async iterator +// next() returns promises that are awaited until there is an update +module.exports.subscribeUpdates = function(name, timeout=60) { + const channel = new Channel(), subscription = subscribe("player." + name, channel); - await postBE("/player/" + name + "/update"); + // stop updates after timeout - setTimeout(() => { - channel.close(); - subscription.unsubscribe(); - }, timeout*1000); - channel.put("initial"); // initial fetch - - let generator = async () => { - let msg; - while (true) { - msg = await channel.take(); - if (["initial", "search_fail", "stats_update", - "matches_update"].indexOf(msg) != -1) - break; + setTimeout(() => channel.close(), timeout*1000); + + let msg; + return { next: async function () { + if (this._first) { + this._first = false; + await postBE("/player/" + name + "/update"); + return true; } - if (msg == "search_fail") { - throw "not found"; + do { + msg = await channel.take(); + } while([Channel.DONE, "initial", "search_fail", + "stats_update", "matches_update"].indexOf(msg) == -1); + // bust caches + if (["stats_update"].indexOf(msg) != -1) + cache.del("player+" + name); + if (["matches_update"].indexOf(msg) != -1) { + cache.del("matches+" + name); + cache.del("player+" + name); } - if (msg == Channel.DONE) { - throw "exhausted"; + if ([Channel.DONE, "search_fail"].indexOf(msg) != -1) { + subscription.unsubscribe(); + return undefined; } - return getFE("/player/" + name); - } + return true; + }, _first: true }; +} - return { next: generator }; +// return player +module.exports.getPlayer = async function(name) { + return await cache.wrap("player+" + name, async () => { + try { + return await getFE("/player/" + name); + } catch (err) { + return undefined; + } + }, { ttl: 60 }); } // return matches -module.exports.searchMatches = async function (name) { - return getFE("/player/" + name + "/matches/1.1.1.1"); +module.exports.getMatches = async function(name) { + return await cache.wrap("matches+" + name, async () => { + try { + return await getFE("/player/" + name + "/matches/1.1.1.1"); + } catch (err) { + return undefined; + } + }, { ttl: 60 }); } // return single match -module.exports.searchMatch = async function (id) { - return getFE("/match/" + id); +module.exports.getMatch = async function(id) { + return await cache.wrap("match+" + id, async () => + getFE("/match/" + id), + { ttl: 60 * 60 }); } diff --git a/bot.js b/bot.js index 8b18d20..a7bd8e5 100644 --- a/bot.js +++ b/bot.js @@ -2,14 +2,20 @@ /* jshint esnext:true */ "use strict"; +const PREVIEW = process.env.PREVIEW || true; + const sqlite = require("sqlite"), path = require("path"), Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, client = new Commando.Client({ - owner: "227440521704898561", // shutterfly - commandPrefix: "?" - }); + owners: ["227440521704898561", "208974925199966208"], + // shutterfly, stormcaller + commandPrefix: (PREVIEW? "?" : "!"), + invite: "https://discord.gg/txTchJY", + unknownCommandResponse: false + }), + responses = require("./responses"); const DISCORD_TOKEN = process.env.DISCORD_TOKEN; @@ -52,7 +58,10 @@ client ${enabled ? 'enabled' : 'disabled'} ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. `); - }); + }) + + // response reaction interface + .on("messageReactionAdd", responses.onNewReaction); client.setProvider( sqlite.open(path.join(__dirname, "settings.sqlite3")).then( diff --git a/commands/about.js b/commands/about.js new file mode 100644 index 0000000..3e8be67 --- /dev/null +++ b/commands/about.js @@ -0,0 +1,21 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + responses = require("../responses"); + +module.exports = class ShowMatchCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "about", + group: "util", + memberName: "about", + description: "Shows invite links and developer contact details." + }); + } + async run(msg) { + await responses.showAbout(msg); + } +}; diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js index c8167ea..8f4a5a4 100644 --- a/commands/vainsocial/match.js +++ b/commands/vainsocial/match.js @@ -14,9 +14,6 @@ module.exports = class ShowMatchCommand extends Commando.Command { group: "vainsocial", memberName: "vainsocial-match", description: "Show a user's match in detail.", - details: oneLine` - todo - `, examples: ["vm shutterfly 1"], argsType: "multiple", argsCount: 2, diff --git a/package.json b/package.json index 6630525..13ee525 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "license": "UNLICENSED", "dependencies": { "async-csp": "^0.5.0", + "bluebird": "^3.5.0", "bufferutil": "^3.0.0", + "cache-manager": "^2.4.0", "discord-emoji": "^1.1.1", "discord.js": "git+https://github.com/hydrabolt/discord.js.git", "discord.js-commando": "^0.9.0", diff --git a/responses.js b/responses.js index 5800650..15e7ec7 100644 --- a/responses.js +++ b/responses.js @@ -4,21 +4,38 @@ const Commando = require("discord.js-commando"), Discord = require("discord.js"), + Promise = require("bluebird"), emoji = require("discord-emoji"), oneLine = require("common-tags").oneLine, + Channel = require("async-csp").Channel, api = require("./api"); +const PREVIEW = process.env.PREVIEW || true, + MATCH_HISTORY_LEN = parseInt(process.env.MATCH_HISTORY_LEN) || 3; + +const reactionsPipe = new Channel(); + +// embed template function vainsocialEmbed(title, link) { - return new Discord.RichEmbed() + let embed = new Discord.RichEmbed() .setTitle(title) - .setURL("https://vainsocial.com/" + link) - .setColor("#55ADD3") - .setAuthor("VainSocial", null, "https://vainsocial.com") - .setFooter("VainSocial") - ; + .setColor("#55ADD3"); + if (PREVIEW) { + embed + .setURL("https://preview.vainsocial.com/" + link) + .setAuthor("VainSocial preview", null, + "https://preview.vainsocial.com") + .setFooter("VainSocial preview"); + } else { + embed + .setURL("https://vainsocial.com/" + link) + .setAuthor("VainSocial", null, "https://vainsocial.com") + .setFooter("VainSocial"); + } + return embed; } -// returns the shortest version of the usage help +// return the shortest version of the usage help // just '?vm' function usg(msg, cmd) { return msg.anyUsage(cmd, undefined, null); @@ -34,11 +51,12 @@ function emojifyScore(score) { } // return a match overview string -function formatMatch(participant) { - let winstr = "Won"; +async function formatMatch(participant) { + let winstr = "Won", + game_mode = await api.mapGameMode(participant.game_mode_id); if (!participant.winner) winstr = "Lost"; return ` -${winstr} ${participant.game_mode_id} with \`${participant.actor.replace(/\*/g, "")}\` +${winstr} ${game_mode} with \`${participant.actor.replace(/\*/g, "")}\` 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)}%\` @@ -46,7 +64,7 @@ Score | ${emojifyScore(participant.stats.impact_score)} \`${Math.floor(100 * par } // return [[title, text], …] for rosters -function formatMatchDetail(match) { +async function formatMatchDetail(match) { let strings = []; for(let roster of match.rosters) { let rosterstr = `${roster.side} - \`${roster.hero_kills}\` Kills`; @@ -89,157 +107,229 @@ ${picks} `; } +// 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=60) { + const pipeOut = new Channel(); + // stop listening after timeout + setTimeout(() => pipeOut.close(), timeout*1000); + + Promise.each(emoji, async (em) => + message.react(em)); // async in background + + 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) => { + await msg.embed(vainsocialEmbed("About VainSocial", "") + .setDescription( +`Built by the VainSocial development team using the MadGlory API. +Currently running on ${msg.client.guilds.size} servers.`) + .addField("Website", + "https://vainsocial.com", 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 ign = args.name, - iterator = await api.searchPlayer(ign), - response, player; - while (true) { - try { - player = await iterator.next(); - } catch (err) { - if (err == "not found") { - await msg.say("Could not find player `" + ign + "`."); - break; - } - if (err == "exhausted") - break; - if (err.statusCode == 404) - continue; + const ign = args.name, + waiter = api.subscribeUpdates(ign); + let responded = false, + response, + reactionsAdded = false; + + while (await waiter.next() != undefined) { + const [player, matches] = await Promise.all([ + api.getPlayer(ign), + api.getMatches(ign) + ]); + if (player == undefined) { + response = await respond(msg, + "Loading your data…", response); + continue; } - let matches = (await api.searchMatches(ign)).data; - let matchstr = "not available", - skill_tier = -1, - last_match_date = new Date(); - if (matches.length > 0) { - matchstr = formatMatch(matches[0]); - skill_tier = matches[0].skill_tier; - last_match_date = new Date(matches[0].created_at); + if (matches == undefined || matches.data.length == 0) { + response = await respond(msg, + "No match history for you yet", response); + continue; } - let embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) + const moreHelp = oneLine` +*${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, +${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` + + const embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) .setThumbnail("https://vainsocial.com/images/game/skill_tiers/" + - skill_tier + ".png") + matches.data[0].skill_tier + ".png") .setDescription("") .addField("Profile", formatPlayer(player), true) - .addField("Last match", matchstr + ` -*${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more* - `, true) - .setTimestamp(last_match_date) - ; - - if (response == undefined) { - response = await msg.embed(embed); - msg.client.on("messageReactionAdd", async (react) => { - if (react.message != response) return; - if (react.users.array().length <= 1) return; // was me - if (react.emoji.name == emoji.symbols.information_source) - await showMatch(msg, { - name: ign, - id: matches[0].match_api_id - }); - if (react.emoji.name == emoji.symbols["1234"]) - await showMatches(msg, { name: ign }); - }); - await response.react(emoji.symbols.information_source); - await response.react(emoji.symbols["1234"]); - } else { - response = await msg.editResponse(response, { - type: "plain", - content: "", - options: { embed: embed } - }); + .addField("Last match", await formatMatch(matches.data[0]) + moreHelp, true) + .setTimestamp(new Date(matches.data[0].created_at)); + response = await respond(msg, embed, response); + + if (!reactionsAdded) { + reactionsAdded = true; + let reactionWaiter = 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) + await respondMatch(msg, ign, matches.data[0].match_api_id); + if (rmoji == emoji.symbols["1234"]) + await respondMatches(msg, ign); + } + })(); // async in background } } + if (!reactionsAdded) + await respond(msg, `Could not find \`${ign}\`.`, response); } // show match in detail -let showMatch = module.exports.showMatch = async (msg, args) => { - let response, - ign = args.name, - id = args.id, - index = args.number; - - let match; - try { - if (id == undefined) - id = (await api.searchMatches(ign)).data[index - 1].match_api_id; - match = await api.searchMatch(id); - } catch (err) { - await msg.say(oneLine` - Ooops! I don't have any data for you yet. - Please take a look at your profile first! - ${usg(msg, "v " + ign)} - `); // TODO! - return; - } +async function respondMatch(msg, ign, id, response=undefined) { + const match = await api.getMatch(id); + let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, "match/" + id) - .setTimestamp(new Date(match.created_at)) - formatMatchDetail(match).forEach(([title, text]) => { + .setTimestamp(new Date(match.created_at)); + (await formatMatchDetail(match)).forEach(([title, text]) => { embed.addField(title, text, true); }); - ; - response = await msg.editResponse(response, { - type: "plain", - content: "", - options: { embed: embed } - }); + return await respond(msg, embed, response); } -// show match history -let showMatches = module.exports.showMatches = async (msg, args) => { - let ign = args.name, +module.exports.showMatch = async (msg, args) => { + const ign = args.name, + index = args.number, + waiter = api.subscribeUpdates(ign); + let responded = false, response; - let 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, + + while (await waiter.next() != undefined) { + let matches = await api.getMatches(ign); + if (matches == undefined || matches.data.length == 0) { + response = await respond(msg, "No matches for you yet", + response); + continue; + } + if (index - 1 > matches.data.length) { + response = await 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; + } + if (!responded) + await 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 ]; - let data, matches, matches_num; - try { - data = await api.searchMatches(ign); - } catch (err) { - await msg.say(oneLine` - Ooops! I don't have any data for you yet. - Please take a look at your profile first! - ${usg(msg, "v " + ign)} - `); // TODO! - return; - } - matches = data.data.slice(0, 3); - matches_num = matches.length; + const data = await api.getMatches(ign), + matches = data.data.slice(0, MATCH_HISTORY_LEN), + matches_num = matches.length; let embed = vainsocialEmbed(ign, "player/" + ign) .setDescription(` Last ${matches_num} casual and ranked matches. *${emoji.symbols["1234"]} or ${usg(msg, "vm " + ign + " number")} for details* - `); - matches.forEach((match, idx) => - embed.addField(`Match ${idx + 1}`, formatMatch(match))); - response = await msg.editResponse(response, { - type: "plain", - content: "", - options: { embed: embed } - }); + `) + .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); - msg.client.on("messageReactionAdd", async (react) => { - if (react.message != response) return; - if (react.users.array().length <= 1) return; // was me - let idx = count.indexOf(react.emoji.name); - if (idx == -1) return; - await showMatch(msg, { - name: ign, - id: matches[idx].match_api_id - }); - }); + const reactionWaiter = 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); + } + })(); // async in background + return response; +} + +module.exports.showMatches = async (msg, args) => { + const ign = args.name, + waiter = api.subscribeUpdates(ign); + let responded = false, + response; - for (let num of count.slice(0, matches_num)) - await response.react(num) + while (await waiter.next() != undefined) { + let matches = await api.getMatches(ign); + if (matches == undefined || matches.data.length == 0) { + response = await respond(msg, "No matches for you yet", + response); + continue; + } + response = await respondMatches(msg, ign, response); + responded = true; + } + if (!responded) + await respond(msg, `Could not find \`${ign}\`.`, + response); } -- cgit v1.3.1 From 6e2f2c80463e8d19c24edebbe85331f6b083cf06 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 17 Apr 2017 19:05:12 +0200 Subject: have timeouts as env vars --- api.js | 4 +++- responses.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api.js b/api.js index a13eb8c..fe0ba82 100644 --- a/api.js +++ b/api.js @@ -14,6 +14,8 @@ let cache = cacheManager.caching({ ttl: 10 // s }); +const UPDATE_TIMEOUT = parseInt(process.env.UPDATE_TIMEOUT) || 60; // s + const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.dev/bots/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", @@ -79,7 +81,7 @@ module.exports.mapGameMode = async function(id) { // be an async iterator // next() returns promises that are awaited until there is an update -module.exports.subscribeUpdates = function(name, timeout=60) { +module.exports.subscribeUpdates = function(name, timeout=UPDATE_TIMEOUT) { const channel = new Channel(), subscription = subscribe("player." + name, channel); diff --git a/responses.js b/responses.js index 15e7ec7..9137af0 100644 --- a/responses.js +++ b/responses.js @@ -11,7 +11,8 @@ const Commando = require("discord.js-commando"), api = require("./api"); const PREVIEW = process.env.PREVIEW || true, - MATCH_HISTORY_LEN = parseInt(process.env.MATCH_HISTORY_LEN) || 3; + MATCH_HISTORY_LEN = parseInt(process.env.MATCH_HISTORY_LEN) || 3, + REACTION_TIMEOUT = parseInt(process.env.REACTION_TIMEOUT) || 60; // s const reactionsPipe = new Channel(); @@ -116,7 +117,7 @@ module.exports.onNewReaction = (reaction) => { // create an iterator that returns promises to await new reactions // the Promise result is the reaction name -function awaitReactions(message, emoji, timeout=60) { +function awaitReactions(message, emoji, timeout=REACTION_TIMEOUT) { const pipeOut = new Channel(); // stop listening after timeout setTimeout(() => pipeOut.close(), timeout*1000); -- cgit v1.3.1 From bfd1b7f962378f0cfa3e765aacff30acc8d475de Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 17 Apr 2017 19:09:47 +0200 Subject: some last text fixes --- commands/about.js | 21 --------------------- commands/vainsocial/match.js | 2 +- commands/vainsocial/matches.js | 5 +---- commands/vainsocial/user.js | 4 ++-- 4 files changed, 4 insertions(+), 28 deletions(-) delete mode 100644 commands/about.js diff --git a/commands/about.js b/commands/about.js deleted file mode 100644 index 3e8be67..0000000 --- a/commands/about.js +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/node -/* jshint esnext:true */ -"use strict"; - -const Commando = require("discord.js-commando"), - oneLine = require("common-tags").oneLine, - responses = require("../responses"); - -module.exports = class ShowMatchCommand extends Commando.Command { - constructor(client) { - super(client, { - name: "about", - group: "util", - memberName: "about", - description: "Shows invite links and developer contact details." - }); - } - async run(msg) { - await responses.showAbout(msg); - } -}; diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js index 8f4a5a4..08f52f5 100644 --- a/commands/vainsocial/match.js +++ b/commands/vainsocial/match.js @@ -13,7 +13,7 @@ module.exports = class ShowMatchCommand extends Commando.Command { aliases: ["vm"], group: "vainsocial", memberName: "vainsocial-match", - description: "Show a user's match in detail.", + description: "Shows a user's match in detail.", examples: ["vm shutterfly 1"], argsType: "multiple", argsCount: 2, diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js index 3f2de2f..adce644 100644 --- a/commands/vainsocial/matches.js +++ b/commands/vainsocial/matches.js @@ -13,10 +13,7 @@ module.exports = class ShowMatchesCommand extends Commando.Command { aliases: ["vh"], group: "vainsocial", memberName: "vainsocial-matches", - description: "Show a user's match history.", - details: oneLine` - todo - `, + description: "Shows a user's match history.", examples: ["vh shutterfly"], argsType: "single", diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js index 1f41c12..95bb86f 100644 --- a/commands/vainsocial/user.js +++ b/commands/vainsocial/user.js @@ -13,9 +13,9 @@ module.exports = class ShowUserCommand extends Commando.Command { aliases: ["v", "vu"], group: "vainsocial", memberName: "vainsocial-user", - description: "Show a player\'s profile.", + description: "Shows a player\'s profile.", details: oneLine` - Display VainSocial lifetime statistics from Vainglory +Display VainSocial lifetime statistics from Vainglory `, examples: ["vu shutterfly"], -- cgit v1.3.1 From 511005814b9afbe0618ed8d5901a352ad556324c Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 17 Apr 2017 19:11:15 +0200 Subject: too tired to notice missing files --- commands/vainsocial/about.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 commands/vainsocial/about.js diff --git a/commands/vainsocial/about.js b/commands/vainsocial/about.js new file mode 100644 index 0000000..22d7fb4 --- /dev/null +++ b/commands/vainsocial/about.js @@ -0,0 +1,21 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + responses = require("../../responses"); + +module.exports = class ShowAboutCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "about", + group: "vainsocial", + memberName: "about", + description: "Shows invite links and developer contact details." + }); + } + async run(msg) { + await responses.showAbout(msg); + } +}; -- cgit v1.3.1 From ad88b9a19cd5438d18be061db08511183b6a20bf Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 18 Apr 2017 18:14:56 +0200 Subject: update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 00cbbdf..6c64673 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.sqlite3 + # Logs logs *.log -- cgit v1.3.1 From 1805e6d82a54a527323b14f47f31f35967541ce3 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 18 Apr 2017 19:32:36 +0200 Subject: fix undefineds; add simple account-ign storage --- api.js | 2 +- commands/vainsocial/match.js | 1 + commands/vainsocial/matches.js | 1 + commands/vainsocial/me.js | 35 +++++++++++++++++++++++ commands/vainsocial/user.js | 1 + responses.js | 63 ++++++++++++++++++++++++++++++++++++------ 6 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 commands/vainsocial/me.js diff --git a/api.js b/api.js index fe0ba82..7b38de4 100644 --- a/api.js +++ b/api.js @@ -68,7 +68,7 @@ async function getMappings() { ["gamemode"], async (table) => { mapping[table] = new Map(); (await getMap(table)).map( - (map) => mapping[table]["id"] = map["name"]) + (map) => mapping[table][map["id"]] = map["name"]) } ); return mapping; diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js index 08f52f5..ba24baf 100644 --- a/commands/vainsocial/match.js +++ b/commands/vainsocial/match.js @@ -23,6 +23,7 @@ module.exports = class ShowMatchCommand extends Commando.Command { label: "name", prompt: "Please specify your in game name (Case Sensitive).", type: "string", + default: "?", min: 3, max: 16 }, { diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js index adce644..7eaa513 100644 --- a/commands/vainsocial/matches.js +++ b/commands/vainsocial/matches.js @@ -22,6 +22,7 @@ module.exports = class ShowMatchesCommand extends Commando.Command { label: "name", prompt: "Please specify your in game name (Case Sensitive).", type: "string", + default: "?", min: 3, max: 16 } ] diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js new file mode 100644 index 0000000..4de7c8f --- /dev/null +++ b/commands/vainsocial/me.js @@ -0,0 +1,35 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + responses = require("../../responses"); + +module.exports = class RememberUserCommand 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.", + details: oneLine` +Store your in game name for quicker access to other commands. + `, + examples: ["vme shutterfly"], + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your in game name (Case Sensitive).", + type: "string", + min: 3, + max: 16 + } ] + }); + } + async run(msg, args) { + await responses.rememberUser(msg, args); + } +}; diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js index 95bb86f..43aecac 100644 --- a/commands/vainsocial/user.js +++ b/commands/vainsocial/user.js @@ -24,6 +24,7 @@ Display VainSocial lifetime statistics from Vainglory label: "name", prompt: "Please specify your in game name (Case Sensitive).", type: "string", + default: "?", min: 3, max: 16 } ] diff --git a/responses.js b/responses.js index 9137af0..d1284e5 100644 --- a/responses.js +++ b/responses.js @@ -98,7 +98,7 @@ function formatPlayer(player) { `; if (player.picks.length > 0) picks = oneLine` - Favorite | \`${player.picks[0].name}\`, \`${player.picks[0].hero_pick} picks\` + Favorite | \`${player.picks[0].actor.replace(/\*/g, "")}\`, \`${player.picks[0].hero_pick} picks\` `; return ` ${stats} @@ -183,14 +183,41 @@ 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; + 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.`); +} + // show player profile and last match module.exports.showUser = async (msg, args) => { - const ign = args.name, - waiter = api.subscribeUpdates(ign); let responded = false, response, + ign = args.name, reactionsAdded = false; + // 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)); + return; + } + } + + const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { const [player, matches] = await Promise.all([ api.getPlayer(ign), @@ -253,12 +280,21 @@ async function respondMatch(msg, ign, id, response=undefined) { } module.exports.showMatch = async (msg, args) => { - const ign = args.name, - index = args.number, - waiter = api.subscribeUpdates(ign); + const index = args.number; let responded = false, + ign = args.name, response; + // shorthand + if (ign == "?") { + ign = nameByUser(msg); + if (ign == undefined) { + await msg.reply(formatSorryUnknown(msg)); + return; + } + } + + const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { let matches = await api.getMatches(ign); if (matches == undefined || matches.data.length == 0) { @@ -315,11 +351,20 @@ Last ${matches_num} casual and ranked matches. } module.exports.showMatches = async (msg, args) => { - const ign = args.name, - waiter = api.subscribeUpdates(ign); - let responded = false, + let ign = args.name, + responded = false, response; + // shorthand + if (ign == "?") { + ign = nameByUser(msg); + if (ign == undefined) { + await msg.reply(formatSorryUnknown(msg)); + return; + } + } + + const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { let matches = await api.getMatches(ign); if (matches == undefined || matches.data.length == 0) { -- cgit v1.3.1 From 273039d6f4a882b4b4665f13a46ee2a3d3b02bcc Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 18 Apr 2017 19:35:36 +0200 Subject: keepalive --- api.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api.js b/api.js index 7b38de4..915530b 100644 --- a/api.js +++ b/api.js @@ -35,21 +35,24 @@ const notif = webstomp.over(new WebSocket(API_WS_URL, function getMap(url) { return request({ uri: API_MAP_URL + url, - json: true + json: true, + forever: true }); } function getFE(url) { return request({ uri: API_FE_URL + url, - json: true + json: true, + forever: true }); } function postBE(url) { return request.post({ uri: API_BE_URL + url, - json: true + json: true, + forever: true }); } -- cgit v1.3.1 From a0cb47ddcfcafc8a254de6b7574838d61a593bdd Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 18 Apr 2017 19:35:44 +0200 Subject: keepalive --- api.js | 1 - 1 file changed, 1 deletion(-) diff --git a/api.js b/api.js index 915530b..1672728 100644 --- a/api.js +++ b/api.js @@ -31,7 +31,6 @@ const notif = webstomp.over(new WebSocket(API_WS_URL, ); })(); -// TODO use keepalive / connection pool function getMap(url) { return request({ uri: API_MAP_URL + url, -- cgit v1.3.1 From 65da4cba0053bc6ee6f5442a45022bf5b4198e70 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 19 Apr 2017 12:02:21 +0200 Subject: rotate IGNs in playing status --- api.js | 9 ++++++++- bot.js | 2 +- responses.js | 13 +++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/api.js b/api.js index 1672728..24c9a9a 100644 --- a/api.js +++ b/api.js @@ -74,13 +74,20 @@ async function getMappings() { } ); return mapping; - }); + }, { ttl: 60 * 30 }); } module.exports.mapGameMode = async function(id) { return (await getMappings())["gamemode"][id]; } +// return a set of IGN of supporters +module.exports.getGamers = async function() { + return await cache.wrap("gamers", async () => { + return (await getFE("/gamer")).map((gamer) => gamer.name); + }, { ttl: 60 * 30 }); +} + // be an async iterator // next() returns promises that are awaited until there is an update module.exports.subscribeUpdates = function(name, timeout=UPDATE_TIMEOUT) { diff --git a/bot.js b/bot.js index a7bd8e5..d4687b9 100644 --- a/bot.js +++ b/bot.js @@ -25,7 +25,7 @@ client .on("debug", console.log) .on("ready", () => { console.log(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); - client.user.setGame("?v shutterfly | vainsocial.com"); + responses.rotateGameStatus(client); }) .on('disconnect', () => { console.warn('Disconnected!'); }) .on('reconnecting', () => { console.warn('Reconnecting...'); }) diff --git a/responses.js b/responses.js index d1284e5..39369ed 100644 --- a/responses.js +++ b/responses.js @@ -12,6 +12,7 @@ const Commando = require("discord.js-commando"), const PREVIEW = process.env.PREVIEW || true, 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 const reactionsPipe = new Channel(); @@ -108,6 +109,18 @@ ${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); + })(); +} + // reaction -> pipe ->>> consumers // handle new reaction event module.exports.onNewReaction = (reaction) => { -- cgit v1.3.1 From 6f9173764a3c40bd252097d2029c82a0401a42ec Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 19 Apr 2017 12:11:12 +0200 Subject: use actor - name mapping --- api.js | 28 +++++++++++++++++++++------- responses.js | 22 +++++++++++++--------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/api.js b/api.js index 24c9a9a..a5d42af 100644 --- a/api.js +++ b/api.js @@ -66,13 +66,23 @@ function subscribe(topic, channel) { async function getMappings() { return await cache.wrap("mappings", async () => { let mapping = new Map(); - await Promise.map( - ["gamemode"], async (table) => { - mapping[table] = new Map(); - (await getMap(table)).map( - (map) => mapping[table][map["id"]] = map["name"]) - } - ); + await Promise.all([ + Promise.map( + ["gamemode"], async (table) => { + mapping[table] = new Map(); + (await getMap(table)).map( + (map) => mapping[table][map["id"]] = map["name"]) + } + ), + // name <-> API name + Promise.map( + ["hero"], async (table) => { + mapping[table] = new Map(); + (await getMap(table)).map( + (map) => mapping[table][map["api_name"]] = map["name"]) + } + ) + ]); return mapping; }, { ttl: 60 * 30 }); } @@ -81,6 +91,10 @@ module.exports.mapGameMode = async function(id) { return (await getMappings())["gamemode"][id]; } +module.exports.mapActor = async function(api_name) { + return (await getMappings())["hero"][api_name]; +} + // return a set of IGN of supporters module.exports.getGamers = async function() { return await cache.wrap("gamers", async () => { diff --git a/responses.js b/responses.js index 39369ed..e28af93 100644 --- a/responses.js +++ b/responses.js @@ -55,10 +55,11 @@ function emojifyScore(score) { // 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 \`${participant.actor.replace(/\*/g, "")}\` +${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)}%\` @@ -72,8 +73,9 @@ async function formatMatchDetail(match) { let rosterstr = `${roster.side} - \`${roster.hero_kills}\` Kills`; let teamstr = ""; for(let participant of roster.participants) { + const hero = await api.mapActor(participant.actor); teamstr += ` -\`${participant.actor.replace(/\*/g, "")}\`, [${participant.player.name}](https://vainsocial.com/player/${participant.player.name}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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}](https://vainsocial.com/player/${participant.player.name}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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]); } @@ -81,8 +83,8 @@ async function formatMatchDetail(match) { } // return a profile string -function formatPlayer(player) { - let stats = oneLine` +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) @@ -90,17 +92,19 @@ function formatPlayer(player) { `, total_kda = oneLine` Total KDA | \`${player.stats.kills}\` / \`${player.stats.deaths}\` / \`${player.stats.assists}\` - `, - best_hero = "", + `; + let best_hero = "", picks = ""; if (player.best_hero.length > 0) best_hero = oneLine` Best | \`${player.best_hero[0].name}\` `; - if (player.picks.length > 0) + if (player.picks.length > 0) { + const hero = await api.mapActor(player.picks[0].actor); picks = oneLine` - Favorite | \`${player.picks[0].actor.replace(/\*/g, "")}\`, \`${player.picks[0].hero_pick} picks\` + Favorite | \`${hero}\`, \`${player.picks[0].hero_pick} picks\` `; + } return ` ${stats} ${total_kda} @@ -255,7 +259,7 @@ ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` .setThumbnail("https://vainsocial.com/images/game/skill_tiers/" + matches.data[0].skill_tier + ".png") .setDescription("") - .addField("Profile", formatPlayer(player), true) + .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); -- cgit v1.3.1 From af0a93a42799b8685f606d1b65fa1e587118abe3 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 19 Apr 2017 12:25:13 +0200 Subject: add favicons --- responses.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/responses.js b/responses.js index e28af93..e798f97 100644 --- a/responses.js +++ b/responses.js @@ -25,14 +25,14 @@ function vainsocialEmbed(title, link) { if (PREVIEW) { embed .setURL("https://preview.vainsocial.com/" + link) - .setAuthor("VainSocial preview", null, + .setAuthor("VainSocial preview", "http://preview.vainsocial.com/images/brands/logo-blue.png", "https://preview.vainsocial.com") - .setFooter("VainSocial preview"); + .setFooter("VainSocial preview", "http://preview.vainsocial.com/images/brands/logo-blue.png"); } else { embed .setURL("https://vainsocial.com/" + link) - .setAuthor("VainSocial", null, "https://vainsocial.com") - .setFooter("VainSocial"); + .setAuthor("VainSocial", "http://vainsocial.com/images/brands/logo-blue.png", "https://vainsocial.com") + .setFooter("VainSocial", "http://vainsocial.com/images/brands/logo-blue.png"); } return embed; } -- cgit v1.3.1 From f97e37a6e5f00c1d88879084147a3f6998c1dc64 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 19 Apr 2017 13:09:16 +0200 Subject: update urls --- responses.js | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/responses.js b/responses.js index e798f97..c61ebc1 100644 --- a/responses.js +++ b/responses.js @@ -13,28 +13,19 @@ const Commando = require("discord.js-commando"), const PREVIEW = process.env.PREVIEW || true, 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 + REACTION_TIMEOUT = parseInt(process.env.REACTION_TIMEOUT) || 60, // s + ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/"); const reactionsPipe = new Channel(); // embed template function vainsocialEmbed(title, link) { - let embed = new Discord.RichEmbed() + return new Discord.RichEmbed() .setTitle(title) - .setColor("#55ADD3"); - if (PREVIEW) { - embed - .setURL("https://preview.vainsocial.com/" + link) - .setAuthor("VainSocial preview", "http://preview.vainsocial.com/images/brands/logo-blue.png", - "https://preview.vainsocial.com") - .setFooter("VainSocial preview", "http://preview.vainsocial.com/images/brands/logo-blue.png"); - } else { - embed - .setURL("https://vainsocial.com/" + link) - .setAuthor("VainSocial", "http://vainsocial.com/images/brands/logo-blue.png", "https://vainsocial.com") - .setFooter("VainSocial", "http://vainsocial.com/images/brands/logo-blue.png"); - } - return embed; + .setColor("#55ADD3") + .setURL(ROOTURL + link) + .setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png", ROOTURL) + .setFooter("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png") } // return the shortest version of the usage help @@ -75,7 +66,7 @@ async function formatMatchDetail(match) { for(let participant of roster.participants) { const hero = await api.mapActor(participant.actor); teamstr += ` -\`${hero}\`, [${participant.player.name}](https://vainsocial.com/player/${participant.player.name}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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]); } @@ -190,7 +181,7 @@ module.exports.showAbout = async (msg) => { `Built by the VainSocial development team using the MadGlory API. Currently running on ${msg.client.guilds.size} servers.`) .addField("Website", - "https://vainsocial.com", true) + ROOTURL, true) .addField("Bot invite link", "https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot&permissions=52288", true) .addField("Developer Discord invite", @@ -256,7 +247,7 @@ module.exports.showUser = async (msg, args) => { ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` const embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) - .setThumbnail("https://vainsocial.com/images/game/skill_tiers/" + + .setThumbnail(ROOTURL + "images/game/skill_tiers/" + matches.data[0].skill_tier + ".png") .setDescription("") .addField("Profile", await formatPlayer(player), true) @@ -288,7 +279,7 @@ ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` async function respondMatch(msg, ign, id, response=undefined) { const match = await api.getMatch(id); - let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, "match/" + id) + let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, ign + "/" + id) .setTimestamp(new Date(match.created_at)); (await formatMatchDetail(match)).forEach(([title, text]) => { embed.addField(title, text, true); -- cgit v1.3.1 From 5882bf54dd458b91896571183ff64bcd4df3b1be Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 19 Apr 2017 13:27:05 +0200 Subject: add campaign analytics --- responses.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/responses.js b/responses.js index c61ebc1..feb40be 100644 --- a/responses.js +++ b/responses.js @@ -19,15 +19,21 @@ const PREVIEW = process.env.PREVIEW || true, const reactionsPipe = new Channel(); // embed template -function vainsocialEmbed(title, link) { +function vainsocialEmbed(title, link, command) { return new Discord.RichEmbed() .setTitle(title) .setColor("#55ADD3") - .setURL(ROOTURL + link) - .setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png", ROOTURL) + .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; +} + // return the shortest version of the usage help // just '?vm' function usg(msg, cmd) { @@ -66,7 +72,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}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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}${track("match-detail")}) \`T${Math.floor(participant.skill_tier/3)}\` | \`${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]); } @@ -176,12 +182,12 @@ async function respond(msg, data, response) { // about module.exports.showAbout = async (msg) => { - await msg.embed(vainsocialEmbed("About VainSocial", "") + await msg.embed(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, true) + ROOTURL + track("about"), true) .addField("Bot invite link", "https://discordapp.com/oauth2/authorize?&client_id=287297889024213003&scope=bot&permissions=52288", true) .addField("Developer Discord invite", @@ -246,7 +252,7 @@ module.exports.showUser = async (msg, args) => { *${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` - const embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign) + const embed = vainsocialEmbed(`${ign} - ${player.shard_id}`, "player/" + ign, "vainsocial-user") .setThumbnail(ROOTURL + "images/game/skill_tiers/" + matches.data[0].skill_tier + ".png") .setDescription("") @@ -279,7 +285,8 @@ ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` async function respondMatch(msg, ign, id, response=undefined) { const match = await api.getMatch(id); - let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, ign + "/" + id) + let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, + ign + "/" + id, "vainsocial-match") .setTimestamp(new Date(match.created_at)); (await formatMatchDetail(match)).forEach(([title, text]) => { embed.addField(title, text, true); @@ -334,7 +341,7 @@ async function respondMatches(msg, ign, response=undefined) { matches = data.data.slice(0, MATCH_HISTORY_LEN), matches_num = matches.length; - let embed = vainsocialEmbed(ign, "player/" + ign) + let embed = 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* -- cgit v1.3.1 From e32769eb47362ec7353014417a9edd4f1be5cc83 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 19 Apr 2017 16:34:05 +0200 Subject: implement GA tracking --- package.json | 1 + responses.js | 31 +++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13ee525..1ddb489 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "request-promise": "^4.2.0", "request-promise-native": "^1.0.3", "sqlite": "^2.5.0", + "universal-analytics": "^0.4.13", "uws": "^0.14.1", "webstomp": "0.0.10", "webstomp-client": "^1.0.6", diff --git a/responses.js b/responses.js index feb40be..b26b344 100644 --- a/responses.js +++ b/responses.js @@ -5,6 +5,7 @@ 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, @@ -14,6 +15,7 @@ const PREVIEW = process.env.PREVIEW || true, 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(); @@ -34,6 +36,21 @@ function track(command) { return "?utm_source=discordbot&utm_medium=discord&utm_campaign=" + command; } +// direct analytics +function trackAction(msg, action, ign="") { + console.log("--------------------------------------"); + console.log(msg.guild.id); + 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) { @@ -182,6 +199,7 @@ async function respond(msg, data, response) { // about module.exports.showAbout = async (msg) => { + trackAction(msg, "about"); await msg.embed(vainsocialEmbed("About VainSocial", "", "about") .setDescription( `Built by the VainSocial development team using the MadGlory API. @@ -209,6 +227,7 @@ function formatSorryUnknown(msg) { 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.`); @@ -220,6 +239,7 @@ module.exports.showUser = async (msg, args) => { response, ign = args.name, reactionsAdded = false; + trackAction(msg, "vainsocial-user", ign); // shorthand // "?" is not accepted as user input, but the default for empty @@ -269,10 +289,14 @@ ${emoji.symbols["1234"]} or ${usg(msg, "vh " + ign)} for more*` while (true) { let rmoji = await reactionWaiter.next(); if (rmoji == undefined) break; // timeout - if (rmoji == emoji.symbols.information_source) + if (rmoji == emoji.symbols.information_source) { + trackAction(msg, "reaction-match", ign); await respondMatch(msg, ign, matches.data[0].match_api_id); - if (rmoji == emoji.symbols["1234"]) + } + if (rmoji == emoji.symbols["1234"]) { + trackAction(msg, "reaction-matches", ign); await respondMatches(msg, ign); + } } })(); // async in background } @@ -299,6 +323,7 @@ module.exports.showMatch = async (msg, args) => { let responded = false, ign = args.name, response; + trackAction(msg, "vainsocial-match", ign); // shorthand if (ign == "?") { @@ -360,6 +385,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); } })(); // async in background return response; @@ -369,6 +395,7 @@ module.exports.showMatches = async (msg, args) => { let ign = args.name, responded = false, response; + trackAction(msg, "vainsocial-matches", ign); // shorthand if (ign == "?") { -- cgit v1.3.1 From dbee8b073b0103162408299df6ae4e50edde73b9 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 20 Apr 2017 12:19:11 +0200 Subject: integrate loggly --- bot.js | 48 ++++++++++++++++++++++++++++++++++-------------- package.json | 2 ++ responses.js | 2 -- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/bot.js b/bot.js index d4687b9..ae1d4c6 100644 --- a/bot.js +++ b/bot.js @@ -6,6 +6,8 @@ const PREVIEW = process.env.PREVIEW || true; const sqlite = require("sqlite"), path = require("path"), + winston = require("winston"), + loggly = require("winston-loggly-bulk"), Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, client = new Commando.Client({ @@ -17,43 +19,61 @@ const sqlite = require("sqlite"), }), responses = require("./responses"); -const DISCORD_TOKEN = process.env.DISCORD_TOKEN; +const DISCORD_TOKEN = process.env.DISCORD_TOKEN, + LOGGLY_TOKEN = process.env.LOGGLY_TOKEN; + +const logger = new (winston.Logger)({ + transports: [ + new (winston.transports.Console)({ + timestamp: true, + colorize: true + }) + ] +}); + +if (LOGGLY_TOKEN) + logger.add(winston.transports.Loggly, { + inputToken: LOGGLY_TOKEN, + subdomain: "kvahuja", + tags: ["frontend", "discordbot"], + json: true + }); client - .on("error", console.error) - .on("warn", console.warn) - .on("debug", console.log) + .on("error", logger.error) + .on("warn", logger.warn) + .on("debug", logger.info) .on("ready", () => { - console.log(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); + logger.info(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); responses.rotateGameStatus(client); }) - .on('disconnect', () => { console.warn('Disconnected!'); }) - .on('reconnecting', () => { console.warn('Reconnecting...'); }) + .on('disconnect', () => { logger.warn('Disconnected!'); }) + .on('reconnecting', () => { logger.warn('Reconnecting...'); }) .on('commandError', (cmd, err) => { if(err instanceof Commando.FriendlyError) return; - console.error(`Error in command ${cmd.groupID}:${cmd.memberName}`, err); + logger.error(`Error in command ${cmd.groupID}:${cmd.memberName}`, err); }) .on('commandBlocked', (msg, reason) => { - console.log(oneLine` + logger.info(oneLine` Command ${msg.command ? `${msg.command.groupID}:${msg.command.memberName}` : ''} blocked; ${reason} `); }) .on('commandPrefixChange', (guild, prefix) => { - console.log(oneLine` + logger.info(oneLine` Prefix ${prefix === '' ? 'removed' : `changed to ${prefix || 'the default'}`} ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. `); }) .on('commandStatusChange', (guild, command, enabled) => { - console.log(oneLine` + logger.info(oneLine` Command ${command.groupID}:${command.memberName} ${enabled ? 'enabled' : 'disabled'} ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. - `); + `); }) .on('groupStatusChange', (guild, group, enabled) => { - console.log(oneLine` + logger.info(oneLine` Group ${group.id} ${enabled ? 'enabled' : 'disabled'} ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. @@ -78,5 +98,5 @@ client.registry client.login(DISCORD_TOKEN); process.on("unhandledRejection", err => { - console.error("Uncaught Promise Error: \n" + err.stack); + logger.error("Uncaught Promise Error", err); }); diff --git a/package.json b/package.json index 1ddb489..4536036 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "uws": "^0.14.1", "webstomp": "0.0.10", "webstomp-client": "^1.0.6", + "winston": "^2.3.1", + "winston-loggly-bulk": "^1.4.2", "ws": "^2.2.3" } } diff --git a/responses.js b/responses.js index b26b344..62d11e2 100644 --- a/responses.js +++ b/responses.js @@ -38,8 +38,6 @@ function track(command) { // direct analytics function trackAction(msg, action, ign="") { - console.log("--------------------------------------"); - console.log(msg.guild.id); if (GOOGLEANALYTICS_ID == undefined) return; const user = ua(GOOGLEANALYTICS_ID, msg.author.id, { strictCidFormat: false }); -- cgit v1.3.1 From 0149bb3dcbc19a58de69d6df9ab05f70efc03be4 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 20 Apr 2017 12:23:09 +0200 Subject: link to correct match page --- responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.js b/responses.js index 62d11e2..a0c93e6 100644 --- a/responses.js +++ b/responses.js @@ -308,7 +308,7 @@ async function respondMatch(msg, ign, id, response=undefined) { const match = await api.getMatch(id); let embed = vainsocialEmbed(`${match.game_mode}, ${match.duration} minutes`, - ign + "/" + id, "vainsocial-match") + "player/" + ign + "/match/" + id, "vainsocial-match") .setTimestamp(new Date(match.created_at)); (await formatMatchDetail(match)).forEach(([title, text]) => { embed.addField(title, text, true); -- cgit v1.3.1 From 132f520dceee48f0d1b38701c454afeda8db859b Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 20 Apr 2017 15:10:43 +0200 Subject: buff vst by 1 --- responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.js b/responses.js index a0c93e6..52c7309 100644 --- a/responses.js +++ b/responses.js @@ -87,7 +87,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)}\` | \`${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}${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]); } -- cgit v1.3.1 From d0a32b9b0e8edc58dd46f988a883aec5687ad1e5 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 20 Apr 2017 16:22:15 +0200 Subject: fix preview env var --- bot.js | 2 +- responses.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.js b/bot.js index ae1d4c6..13ca105 100644 --- a/bot.js +++ b/bot.js @@ -2,7 +2,7 @@ /* jshint esnext:true */ "use strict"; -const PREVIEW = process.env.PREVIEW || true; +const PREVIEW = process.env.PREVIEW != "false"; const sqlite = require("sqlite"), path = require("path"), diff --git a/responses.js b/responses.js index 52c7309..7508cb4 100644 --- a/responses.js +++ b/responses.js @@ -11,7 +11,7 @@ const Commando = require("discord.js-commando"), Channel = require("async-csp").Channel, api = require("./api"); -const PREVIEW = process.env.PREVIEW || true, +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 -- cgit v1.3.1 From baed3a120f91ea51fd4f039583fabcdbe7f7e4d8 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 20 Apr 2017 16:43:16 +0200 Subject: show win/loss on roster --- responses.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/responses.js b/responses.js index 7508cb4..63e7359 100644 --- a/responses.js +++ b/responses.js @@ -82,7 +82,9 @@ Score | ${emojifyScore(participant.stats.impact_score)} \`${Math.floor(100 * par async function formatMatchDetail(match) { let strings = []; for(let roster of match.rosters) { - let rosterstr = `${roster.side} - \`${roster.hero_kills}\` Kills`; + let winstr = "Won"; + if (!participant.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); -- cgit v1.3.1 From a9683a5123206ab483fd638555dd6a59bc866d53 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 20 Apr 2017 16:45:00 +0200 Subject: shit typo; next time test before push --- responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.js b/responses.js index 63e7359..ea7c01b 100644 --- a/responses.js +++ b/responses.js @@ -83,7 +83,7 @@ async function formatMatchDetail(match) { let strings = []; for(let roster of match.rosters) { let winstr = "Won"; - if (!participant.winner) winstr = "Lost"; + if (!roster.winner) winstr = "Lost"; let rosterstr = `${roster.side} - \`${roster.hero_kills}\` Kills - ${winstr}`; let teamstr = ""; for(let participant of roster.participants) { -- cgit v1.3.1 From 3104750b606e1c75dd39a40995c0fa3d64308ba0 Mon Sep 17 00:00:00 2001 From: vainsocial Date: Fri, 21 Apr 2017 07:02:38 +0000 Subject: switch websocket lib --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 4536036..6da943d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "async-csp": "^0.5.0", "bluebird": "^3.5.0", - "bufferutil": "^3.0.0", + "bufferutil": "^2.0.0", "cache-manager": "^2.4.0", "discord-emoji": "^1.1.1", "discord.js": "git+https://github.com/hydrabolt/discord.js.git", @@ -22,7 +22,6 @@ "request-promise-native": "^1.0.3", "sqlite": "^2.5.0", "universal-analytics": "^0.4.13", - "uws": "^0.14.1", "webstomp": "0.0.10", "webstomp-client": "^1.0.6", "winston": "^2.3.1", -- cgit v1.3.1 From c09e757bd7cb03c0f00e0ee5e294e3b642999e1c Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 21 Apr 2017 11:28:15 +0200 Subject: remove reaction buttons after timeout --- responses.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/responses.js b/responses.js index ea7c01b..18b42a4 100644 --- a/responses.js +++ b/responses.js @@ -150,11 +150,16 @@ module.exports.onNewReaction = (reaction) => { // 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(), timeout*1000); + setTimeout(() => { + pipeOut.close(); + Promise.map(reactions, async (r) => r.remove()); + }, timeout*1000); + // async in background Promise.each(emoji, async (em) => - message.react(em)); // async in background + reactions.push(await message.react(em))); let reaction; reactionsPipe.pipe(pipeOut); -- cgit v1.3.1 From a9f1d0adf579306a277064983d00cca7fa1a962d Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 28 Apr 2017 14:41:58 +0200 Subject: trying to fix bot token resets --- bot.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot.js b/bot.js index 13ca105..f38bbec 100644 --- a/bot.js +++ b/bot.js @@ -47,7 +47,10 @@ client logger.info(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); responses.rotateGameStatus(client); }) - .on('disconnect', () => { logger.warn('Disconnected!'); }) + .on('disconnect', () => { + logger.warn('Disconnected!'); + process.exit(1); // make pm2 restart bot + }) .on('reconnecting', () => { logger.warn('Reconnecting...'); }) .on('commandError', (cmd, err) => { if(err instanceof Commando.FriendlyError) return; -- cgit v1.3.1 From 5b3b8b4f4d0898dd9f0519c4a374e63049621aac Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 28 Apr 2017 15:25:20 +0200 Subject: nvm This reverts commit a9f1d0adf579306a277064983d00cca7fa1a962d. --- bot.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bot.js b/bot.js index f38bbec..13ca105 100644 --- a/bot.js +++ b/bot.js @@ -47,10 +47,7 @@ client logger.info(`Client ready; logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`); responses.rotateGameStatus(client); }) - .on('disconnect', () => { - logger.warn('Disconnected!'); - process.exit(1); // make pm2 restart bot - }) + .on('disconnect', () => { logger.warn('Disconnected!'); }) .on('reconnecting', () => { logger.warn('Reconnecting...'); }) .on('commandError', (cmd, err) => { if(err instanceof Commando.FriendlyError) return; -- cgit v1.3.1 From 03b175e8972d7de1a3a05232d6eee6bc3f53c23c Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 30 Apr 2017 14:30:04 +0200 Subject: hotfix gamers --- responses.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/responses.js b/responses.js index 18b42a4..55baabb 100644 --- a/responses.js +++ b/responses.js @@ -129,12 +129,14 @@ ${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); })(); } -- cgit v1.3.1 From 6b991150f4d8439f58da6eae32458c529a682029 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 30 Apr 2017 14:36:27 +0200 Subject: fuck bot is down fuck --- responses.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/responses.js b/responses.js index 55baabb..52cf633 100644 --- a/responses.js +++ b/responses.js @@ -260,7 +260,7 @@ module.exports.showUser = async (msg, args) => { const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { - const [player, matches] = await Promise.all([ + const [player, stormcallerpleasefix] = await Promise.all([ api.getPlayer(ign), api.getMatches(ign) ]); @@ -269,11 +269,12 @@ module.exports.showUser = async (msg, args) => { "Loading your data…", response); continue; } - if (matches == undefined || matches.data.length == 0) { + if (stormcallerpleasefix == undefined || stormcallerpleasefix[0].data.length == 0) { response = await respond(msg, "No match history for you yet", response); continue; } + const matches = stormcallerpleasefix[0]; const moreHelp = oneLine` *${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, -- cgit v1.3.1 From b8ef09d6be33622bc6131802a80520c57ed1ea20 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 30 Apr 2017 14:37:54 +0200 Subject: fuck bot is down fuck --- responses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.js b/responses.js index 52cf633..582e85d 100644 --- a/responses.js +++ b/responses.js @@ -371,7 +371,7 @@ async function respondMatches(msg, ign, response=undefined) { emoji.symbols.ten ]; const data = await api.getMatches(ign), - matches = data.data.slice(0, MATCH_HISTORY_LEN), + matches = data[0].data.slice(0, MATCH_HISTORY_LEN), // TODO matches_num = matches.length; let embed = vainsocialEmbed(ign, "player/" + ign, "vainsocial-matches") -- cgit v1.3.1 From 1bca32e75e6888cfdd6cd0e9f581120da560cbb0 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 1 May 2017 15:47:20 +0200 Subject: clean integration of bots api change --- responses.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/responses.js b/responses.js index 582e85d..d8f49ab 100644 --- a/responses.js +++ b/responses.js @@ -260,7 +260,7 @@ module.exports.showUser = async (msg, args) => { const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { - const [player, stormcallerpleasefix] = await Promise.all([ + const [player, data] = await Promise.all([ api.getPlayer(ign), api.getMatches(ign) ]); @@ -269,12 +269,12 @@ module.exports.showUser = async (msg, args) => { "Loading your data…", response); continue; } - if (stormcallerpleasefix == undefined || stormcallerpleasefix[0].data.length == 0) { + if (data == undefined || data[0].data.length == 0) { response = await respond(msg, "No match history for you yet", response); continue; } - const matches = stormcallerpleasefix[0]; + const matches = data[0]; const moreHelp = oneLine` *${emoji.symbols.information_source} or ${usg(msg, "vm " + ign)} for detail, @@ -344,12 +344,13 @@ module.exports.showMatch = async (msg, args) => { const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { - let matches = await api.getMatches(ign); - if (matches == undefined || matches.data.length == 0) { + const data = await api.getMatches(ign); + if (data == undefined || data[0].data.length == 0) { response = await 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); @@ -416,12 +417,13 @@ module.exports.showMatches = async (msg, args) => { const waiter = api.subscribeUpdates(ign); while (await waiter.next() != undefined) { - let matches = await api.getMatches(ign); - if (matches == undefined || matches.data.length == 0) { + const data = await api.getMatches(ign); + if (data == undefined || data[0].data.length == 0) { response = await respond(msg, "No matches for you yet", response); continue; } + const matches = data[0]; response = await respondMatches(msg, ign, response); responded = true; } -- cgit v1.3.1 From 723990c914352cd5b16211469f3c6bc71c91a676 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 1 May 2017 15:50:46 +0200 Subject: use command @git --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6da943d..40fbca2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "cache-manager": "^2.4.0", "discord-emoji": "^1.1.1", "discord.js": "git+https://github.com/hydrabolt/discord.js.git", - "discord.js-commando": "^0.9.0", + "discord.js-commando": "git+https://github.com/Gawdl3y/discord.js-commando.git", "erlpack": "github:hammerandchisel/erlpack", "request": "^2.81.0", "request-promise": "^4.2.0", -- cgit v1.3.1 From 188e3997092a57be11574e5e121af5882dfb40ec Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 1 May 2017 15:52:41 +0200 Subject: reenable gamer rotation --- responses.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/responses.js b/responses.js index d8f49ab..692d1f4 100644 --- a/responses.js +++ b/responses.js @@ -129,14 +129,12 @@ ${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); })(); } -- cgit v1.3.1 From 5045641b065bfea9a0db39dd4814b2cf2269e723 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 4 May 2017 22:33:17 +0200 Subject: fame support (wip) --- api.js | 25 +++-- bot.js | 6 +- commands/vainsocial/guild_add.js | 80 ++++++++++++++ commands/vainsocial/guild_create.js | 66 +++++++++++ commands/vainsocial/guild_view.js | 58 ++++++++++ commands/vainsocial/me.js | 26 ++++- package.json | 1 - responses.js | 214 ++++++++++-------------------------- util.js | 118 ++++++++++++++++++++ 9 files changed, 419 insertions(+), 175 deletions(-) create mode 100644 commands/vainsocial/guild_add.js create mode 100644 commands/vainsocial/guild_create.js create mode 100644 commands/vainsocial/guild_view.js create mode 100644 util.js diff --git a/api.js b/api.js index a5d42af..5e0a456 100644 --- a/api.js +++ b/api.js @@ -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 diff --git a/bot.js b/bot.js index 13ca105..5968392 100644 --- a/bot.js +++ b/bot.js @@ -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); } diff --git a/util.js b/util.js new file mode 100644 index 0000000..f2e894b --- /dev/null +++ b/util.js @@ -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")}.`; +}; -- cgit v1.3.1 From 0e5a0b876aa36786430d313f8ded843aa849eb00 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 5 May 2017 17:06:58 +0200 Subject: refactor --- api.js | 97 +++++++++++++++++++------------------ commands/vainsocial/guild_add.js | 16 +++--- commands/vainsocial/guild_create.js | 18 +++---- commands/vainsocial/guild_view.js | 12 +---- commands/vainsocial/me.js | 13 ++--- responses.js | 85 +++++++++++--------------------- 6 files changed, 98 insertions(+), 143 deletions(-) diff --git a/api.js b/api.js index 5e0a456..598b1eb 100644 --- a/api.js +++ b/api.js @@ -39,14 +39,33 @@ function getMap(url) { }); } -function getFE(url) { - return request({ - uri: API_FE_URL + url, +async function getFE(url, params={}, ttl=60, cachekey=undefined) { + if (cachekey == undefined) cachekey = url + JSON.stringify(params); + return await cache.wrap(cachekey, async () => { + try { + return await request({ + uri: API_FE_URL + url, + qs: params, + json: true, + forever: true + }); + } catch (err) { + return undefined; + } + }, { ttl: ttl }); +} + +async function postFE(url, params={}) { + return await request.post(API_FE_URL + url, { + form: params, json: true, forever: true }); } +module.exports.get = getFE; +module.exports.post = postFE; + function postBE(url) { return request.post({ uri: API_BE_URL + url, @@ -87,35 +106,36 @@ async function getMappings() { }, { ttl: 60 * 30 }); } -module.exports.mapGameMode = async function(id) { - return (await getMappings())["gamemode"][id]; -} +module.exports.mapGameMode = async (id) => + (await getMappings())["gamemode"][id]; -module.exports.mapActor = async function(api_name) { - return (await getMappings())["hero"][api_name]; -} +module.exports.mapActor = async (api_name) => + (await getMappings())["hero"][api_name]; // return a set of IGN of supporters -module.exports.getGamers = async function() { - return await cache.wrap("gamers", async () => { - return (await getFE("/gamer")).map((gamer) => gamer.name); - }, { ttl: 60 * 30 }); -} +module.exports.getGamers = async () => + (await getFE("/gamer", {}, 60 * 30)).map((gamer) => gamer.name); // be an async iterator // next() returns promises that are awaited until there is an update -module.exports.subscribeUpdates = function(name, timeout=UPDATE_TIMEOUT) { +module.exports.subscribeUpdates = async (name, refresh=true, timeout=UPDATE_TIMEOUT) => { const channel = new Channel(), subscription = subscribe("player." + name, channel); + if (refresh) { + // trigger backend + if (await module.exports.getPlayer(name) == undefined) + await module.exports.searchPlayer(name); + else await module.exports.updatePlayer(name); + } + // stop updates after timeout setTimeout(() => channel.close(), timeout*1000); let msg; return { next: async function () { - do { - msg = await channel.take(); - } while([Channel.DONE, "search_fail", "search_success", + do msg = await channel.take(); + while([Channel.DONE, "search_fail", "search_success", "stats_update", "matches_update"].indexOf(msg) == -1); // bust caches if (["stats_update"].indexOf(msg) != -1) @@ -133,40 +153,25 @@ module.exports.subscribeUpdates = function(name, timeout=UPDATE_TIMEOUT) { } // search an unknown player -module.exports.searchPlayer = async function(name) { - return await postBE("/player/" + name + "/search"); -} +module.exports.searchPlayer = (name) => + postBE("/player/" + name + "/search"); // update a known player -module.exports.updatePlayer = async function(name) { - return await postBE("/player/" + name + "/update"); -} +module.exports.updatePlayer = (name) => + postBE("/player/" + name + "/update"); // return player -module.exports.getPlayer = async function(name) { - return await cache.wrap("player+" + name, async () => { - try { - return await getFE("/player/" + name); - } catch (err) { - return undefined; - } - }, { ttl: 60 }); -} +module.exports.getPlayer = (name) => + getFE("/player/" + name, {}, 60, "player+" + name); // return matches -module.exports.getMatches = async function(name) { - return await cache.wrap("matches+" + name, async () => { - try { - return await getFE("/player/" + name + "/matches/1.1.1.1"); - } catch (err) { - return undefined; - } - }, { ttl: 60 }); +module.exports.getMatches = async (name) => { + const data = await getFE("/player/" + name + "/matches/2.3.1.1", {}, + 60 * 60, "matches+" + name); + if (data == undefined) return []; + return data[0].data; } // return single match -module.exports.getMatch = async function(id) { - return await cache.wrap("match+" + id, async () => - getFE("/match/" + id), - { ttl: 60 * 60 }); -} +module.exports.getMatch = (id) => + getFE("/match/" + id, {}, 60 * 60); diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index 7f6aac7..8dc2ab8 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -5,12 +5,9 @@ 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, { @@ -44,9 +41,11 @@ Register IGNs to your Guild. let player = await api.getPlayer(user); if (player == undefined) { await progress(`Loading ${user}'s data into VainSocial…`); - // if not, wait for backend to fetch + // 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) { @@ -63,12 +62,9 @@ Register IGNs to your Guild. } // 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 api.post("/guild/members", { + user_token: msg.author.id, + member_name: user }); await progress(`Added ${user}.`, true); }); diff --git a/commands/vainsocial/guild_create.js b/commands/vainsocial/guild_create.js index 9114703..1e5a368 100644 --- a/commands/vainsocial/guild_create.js +++ b/commands/vainsocial/guild_create.js @@ -4,11 +4,9 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, - 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 RegisterGuildCommand extends Commando.Command { constructor(client) { super(client, { @@ -49,15 +47,11 @@ 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"); - 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 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. diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js index c459590..e692402 100644 --- a/commands/vainsocial/guild_view.js +++ b/commands/vainsocial/guild_view.js @@ -4,11 +4,9 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, - 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 ViewGuildCommand extends Commando.Command { constructor(client) { super(client, { @@ -36,13 +34,7 @@ Show a summary of your Guild. 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 - } - }); + const guild = await api.get("/guild", { user_token: msg.author.id }); if (guild == undefined) { await msg.reply("You are not registered in any guilds."); return; diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index 02818be..400bc70 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -4,11 +4,9 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, - 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 RegisterUserCommand extends Commando.Command { constructor(client) { super(client, { @@ -36,12 +34,9 @@ Store your in game name for quicker access to other commands and for Guild manag async run(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 api.post("/user", { + 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. diff --git a/responses.js b/responses.js index 49302a4..ef749eb 100644 --- a/responses.js +++ b/responses.js @@ -117,9 +117,10 @@ Currently running on ${msg.client.guilds.size} servers.`) } // return the sqlite stored ign for this user -function nameByUser(msg) { - throw "nope that's not possible yet"; - //return msg.guild.settings.get("remember+" + msg.author.id); +async function nameByUser(msg) { + const user = await api.get("/user", { user_token: msg.author.id }); + if (user) return user.name; + return undefined; } // show player profile and last match @@ -133,36 +134,24 @@ module.exports.showUser = async (msg, args) => { // shorthand // "?" is not accepted as user input, but the default for empty if (ign == "?") { - ign = nameByUser(msg); + ign = await nameByUser(msg); if (ign == undefined) { 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); + const waiter = await api.subscribeUpdates(ign, true); do { - const [player, data] = await Promise.all([ + const [player, matches] = await Promise.all([ api.getPlayer(ign), api.getMatches(ign) ]); - if (player == undefined) { + if (player == undefined || matches.length == 0) { response = await util.respond(msg, "Loading your data…", response); continue; } - if (data == undefined || data[0].data.length == 0) { - 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 ${util.usg(msg, "vm " + ign)} for detail, @@ -170,14 +159,15 @@ ${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.data[0].skill_tier + ".png") + matches[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)); + .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"]]); @@ -187,7 +177,7 @@ ${emoji.symbols["1234"]} or ${util.usg(msg, "vh " + ign)} for more*` if (rmoji == undefined) break; // timeout if (rmoji == emoji.symbols.information_source) { util.trackAction(msg, "reaction-match", ign); - await respondMatch(msg, ign, matches.data[0].match_api_id); + await respondMatch(msg, ign, matches[0].match_api_id); } if (rmoji == emoji.symbols["1234"]) { util.trackAction(msg, "reaction-matches", ign); @@ -223,33 +213,22 @@ module.exports.showMatch = async (msg, args) => { // shorthand if (ign == "?") { - ign = nameByUser(msg); + ign = await nameByUser(msg); if (ign == undefined) { 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); + const waiter = await api.subscribeUpdates(ign, true); do { - const data = await api.getMatches(ign); - if (data == undefined || data[0].data.length == 0) { - response = await util.respond(msg, "No matches for you yet", + const matches = await api.getMatches(ign); + if (index > matches.length) { + response = await util.respond(msg, "Not enough matches yet.", response); continue; } - const matches = data[0]; - if (index - 1 > matches.data.length) { - response = await util.respond(msg, "Not enough matches yet", - response); - continue; - } - let id = matches.data[index -1].match_api_id; + const id = matches[index - 1].match_api_id; response = await respondMatch(msg, ign, id, response); responded = true; } while (await waiter.next() != undefined); @@ -264,10 +243,15 @@ async function respondMatches(msg, ign, response=undefined) { emoji.symbols.seven, emoji.symbols.eight, emoji.symbols.nine, emoji.symbols.ten ]; - const data = await api.getMatches(ign), - matches = data[0].data.slice(0, MATCH_HISTORY_LEN), // TODO + 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. @@ -279,6 +263,7 @@ Last ${matches_num} casual and ranked matches. ); response = await util.respond(msg, embed, response); + // reaction button bar const reactionWaiter = util.awaitReactions(response, count.slice(0, matches_num)); (async () => { @@ -301,27 +286,15 @@ module.exports.showMatches = async (msg, args) => { // shorthand if (ign == "?") { - ign = nameByUser(msg); + ign = await nameByUser(msg); if (ign == undefined) { 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); + const waiter = await api.subscribeUpdates(ign, true); do { - const data = await api.getMatches(ign); - if (data == undefined || data[0].data.length == 0) { - 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); -- cgit v1.3.1 From a5bd191b194ca97afd777a4bb523981e4855df92 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 5 May 2017 18:03:04 +0200 Subject: refactor, fix caching --- api.js | 60 +++++++++++++++++++++++++++++++-------- commands/vainsocial/guild_add.js | 5 +--- commands/vainsocial/guild_view.js | 2 +- responses.js | 9 ++++-- util.js | 5 ++-- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/api.js b/api.js index 598b1eb..9416486 100644 --- a/api.js +++ b/api.js @@ -50,12 +50,15 @@ async function getFE(url, params={}, ttl=60, cachekey=undefined) { forever: true }); } catch (err) { + // TODO sort errors, loggly return undefined; } }, { ttl: ttl }); } -async function postFE(url, params={}) { +// send a POST and optionally bust cache +async function postFE(url, params={}, cachekey=undefined) { + if (cachekey) cache.del(cachekey); return await request.post(API_FE_URL + url, { form: params, json: true, @@ -63,9 +66,6 @@ async function postFE(url, params={}) { }); } -module.exports.get = getFE; -module.exports.post = postFE; - function postBE(url) { return request.post({ uri: API_BE_URL + url, @@ -74,6 +74,10 @@ function postBE(url) { }); } +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); @@ -118,17 +122,10 @@ module.exports.getGamers = async () => // be an async iterator // next() returns promises that are awaited until there is an update -module.exports.subscribeUpdates = async (name, refresh=true, timeout=UPDATE_TIMEOUT) => { +module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => { const channel = new Channel(), subscription = subscribe("player." + name, channel); - if (refresh) { - // trigger backend - if (await module.exports.getPlayer(name) == undefined) - await module.exports.searchPlayer(name); - else await module.exports.updatePlayer(name); - } - // stop updates after timeout setTimeout(() => channel.close(), timeout*1000); @@ -164,6 +161,13 @@ module.exports.updatePlayer = (name) => module.exports.getPlayer = (name) => 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); +} + // return matches module.exports.getMatches = async (name) => { const data = await getFE("/player/" + name + "/matches/2.3.1.1", {}, @@ -175,3 +179,35 @@ module.exports.getMatches = async (name) => { // return single match module.exports.getMatch = (id) => getFE("/match/" + id, {}, 60 * 60); + +// return a guild +module.exports.getGuild = (token) => + getFE("/guild", { user_token: token }, 60, "guild+" + token); +// TODO! cache guilds by guild id, not by user token + +// add user to guild +module.exports.addToGuild = async (token, member) => { + const membership = await postFE("/guild/members", { + user_token: token, + member_name: member + }, "guild+" + token); + cache.del("guild+" + token); + return membership; +} + +// recalc fame, block until timeout or points update +module.exports.calculateGuild = async (id, token) => { + const channel = new Channel(), + subscription = subscribe("global", channel); + await module.exports.backend("/team/" + id + "/crunch"); + + // stop updates after timeout + setTimeout(() => channel.close(), UPDATE_TIMEOUT*1000); + + let msg; + do msg = await channel.take(); + while([Channel.DONE, "points_update"].indexOf(msg) == -1); + + if (msg == "points_update") cache.del("guild+" + token); + subscription.unsubscribe(); +} diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index 8dc2ab8..b4aa864 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -62,10 +62,7 @@ Register IGNs to your Guild. } // all good, register to self guild - await api.post("/guild/members", { - user_token: msg.author.id, - member_name: user - }); + await api.addToGuild(msg.author.id, user); await progress(`Added ${user}.`, true); }); await progress(oneLine` diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js index e692402..835b717 100644 --- a/commands/vainsocial/guild_view.js +++ b/commands/vainsocial/guild_view.js @@ -34,7 +34,7 @@ Show a summary of your Guild. async run(msg, args) { util.trackAction(msg, "vainsocial-guild-view"); // get this user's guild - const guild = await api.get("/guild", { user_token: msg.author.id }); + const guild = await api.getGuild(msg.author.id); if (guild == undefined) { await msg.reply("You are not registered in any guilds."); return; diff --git a/responses.js b/responses.js index ef749eb..a2eb83d 100644 --- a/responses.js +++ b/responses.js @@ -141,7 +141,8 @@ module.exports.showUser = async (msg, args) => { } } - const waiter = await api.subscribeUpdates(ign, true); + const waiter = api.subscribeUpdates(ign); + await api.upsearchPlayer(ign); do { const [player, matches] = await Promise.all([ api.getPlayer(ign), @@ -220,7 +221,8 @@ module.exports.showMatch = async (msg, args) => { } } - const waiter = await api.subscribeUpdates(ign, true); + const waiter = await api.subscribeUpdates(ign); + await api.upsearchPlayer(ign); do { const matches = await api.getMatches(ign); if (index > matches.length) { @@ -293,7 +295,8 @@ module.exports.showMatches = async (msg, args) => { } } - const waiter = await api.subscribeUpdates(ign, true); + const waiter = await api.subscribeUpdates(ign); + await api.upsearchPlayer(ign); do { response = await respondMatches(msg, ign, response); responded = true; diff --git a/util.js b/util.js index f2e894b..a1deaf7 100644 --- a/util.js +++ b/util.js @@ -99,8 +99,9 @@ module.exports.respond = async (msg, data, response) => { if (data != response.content) await response.edit(data); } else { - if (new Date(response.embeds[0].createdTimestamp).getTime() - != data.timestamp.getTime()) + if (response.embeds.length ==0 || + new Date(response.embeds[0].createdTimestamp).getTime() + != data.timestamp.getTime()) // TODO how2 edit embed properly?! await msg.editResponse(response, { type: "plain", -- cgit v1.3.1 From 2ad459a0910498f39c8c5abd697189b788df827a Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 5 May 2017 18:03:46 +0200 Subject: add crunch request command for beta test --- commands/vainsocial/guild_fame_calc.js | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 commands/vainsocial/guild_fame_calc.js diff --git a/commands/vainsocial/guild_fame_calc.js b/commands/vainsocial/guild_fame_calc.js new file mode 100644 index 0000000..fbd7469 --- /dev/null +++ b/commands/vainsocial/guild_fame_calc.js @@ -0,0 +1,36 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + api = require("../../api"), + util = require("../../util"); + +module.exports = class CalculateGuildFameCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildcalc", + aliases: ["vguild-calc", "vgcalc", "vgc"], + group: "vainsocial-guild", + memberName: "vainsocial-guildcalc", + description: "Update your Guild's fame.", + details: oneLine` +Recalculate your Guild members' fame. +`, + examples: ["vgcalc"] + }); + } + // internal / premium: immediately call backend fame refresh + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-calculate"); + const guild = await api.getGuild(msg.author.id); + if (guild == undefined) { + await msg.reply("You are not registered in any guilds."); + return; + } + await msg.reply("Your Guild members' fame will be updated soon…"); + await api.calculateGuild(guild.id, msg.author.id); + await msg.reply("Your Guild members' fame has been updated."); + } +}; -- cgit v1.3.1 From f568816fcfd9df37e01b898a0f2b5cee58e0f9c3 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 5 May 2017 22:46:34 +0200 Subject: refactor, add beta guild member update command --- api.js | 15 +++++++++++++- commands/vainsocial/guild_add.js | 2 +- commands/vainsocial/guild_fame_calc.js | 2 +- commands/vainsocial/guild_update.js | 38 ++++++++++++++++++++++++++++++++++ commands/vainsocial/me.js | 24 +++++++++++++++++---- responses.js | 13 +++--------- util.js | 5 ++--- 7 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 commands/vainsocial/guild_update.js diff --git a/api.js b/api.js index 9416486..735308b 100644 --- a/api.js +++ b/api.js @@ -170,7 +170,7 @@ module.exports.upsearchPlayer = async (name) => { // return matches module.exports.getMatches = async (name) => { - const data = await getFE("/player/" + name + "/matches/2.3.1.1", {}, + const data = await getFE("/player/" + name + "/matches/1.1.1.1", {}, 60 * 60, "matches+" + name); if (data == undefined) return []; return data[0].data; @@ -211,3 +211,16 @@ module.exports.calculateGuild = async (id, token) => { if (msg == "points_update") cache.del("guild+" + token); subscription.unsubscribe(); } + +// store Discord ID <-> IGN +module.exports.setUser = async (token, name) => { + cache.del("user+" + token); + await module.exports.post("/user", { + name: name, + user_token: token + }); +} + +// retrieve Discord ID -> IGN +module.exports.getUser = async (token) => + (await getFE("/user", { user_token: token }, 60, "user+" + token)).name; diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index b4aa864..531d3dd 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -50,7 +50,7 @@ Register IGNs to your Guild. // wait until search success while (true) { notif = await waiter.next(); - if (notif == "search_success") break; + if (notif == "stats_update") break; if (notif == undefined) { // give up await progress(`Ooops! Could not find ${user}.`, true); diff --git a/commands/vainsocial/guild_fame_calc.js b/commands/vainsocial/guild_fame_calc.js index fbd7469..e1ec912 100644 --- a/commands/vainsocial/guild_fame_calc.js +++ b/commands/vainsocial/guild_fame_calc.js @@ -11,7 +11,7 @@ module.exports = class CalculateGuildFameCommand extends Commando.Command { constructor(client) { super(client, { name: "vainsocial-guildcalc", - aliases: ["vguild-calc", "vgcalc", "vgc"], + aliases: ["vguild-calc", "vgcalc"], group: "vainsocial-guild", memberName: "vainsocial-guildcalc", description: "Update your Guild's fame.", diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js new file mode 100644 index 0000000..9c9fe1d --- /dev/null +++ b/commands/vainsocial/guild_update.js @@ -0,0 +1,38 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + Promise = require("bluebird"), + api = require("../../api"), + util = require("../../util"); + +module.exports = class UpdateGuildCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildupdate", + aliases: ["vguild-update", "vgupdate"], + group: "vainsocial-guild", + memberName: "vainsocial-guildupdate", + description: "Update your Guild members' data.", + details: oneLine` +Update the match history for all your Guild members. +`, + examples: ["vgupdate"] + }); + } + // internal / premium: immediately call backend player refresh + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-update"); + const guild = await api.getGuild(msg.author.id); + if (guild == undefined) { + await msg.reply("You are not registered in any guilds."); + return; + } + // TODO progress report + await Promise.map(guild.members, + (member) => api.upsearchPlayer(member.player.name)); + await msg.reply("Your Guild members will be up to date soon."); + } +}; diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index 400bc70..717e36c 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -34,10 +34,26 @@ Store your in game name for quicker access to other commands and for Guild manag async run(msg, args) { const ign = args.name; util.trackAction(msg, "vainsocial-me", ign); - await api.post("/user", { - name: ign, - user_token: msg.author.id - }); + 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 have access to Guild management features. diff --git a/responses.js b/responses.js index a2eb83d..32ce136 100644 --- a/responses.js +++ b/responses.js @@ -116,13 +116,6 @@ Currently running on ${msg.client.guilds.size} servers.`) ); } -// return the sqlite stored ign for this user -async function nameByUser(msg) { - const user = await api.get("/user", { user_token: msg.author.id }); - if (user) return user.name; - return undefined; -} - // show player profile and last match module.exports.showUser = async (msg, args) => { let responded = false, @@ -134,7 +127,7 @@ module.exports.showUser = async (msg, args) => { // shorthand // "?" is not accepted as user input, but the default for empty if (ign == "?") { - ign = await nameByUser(msg); + ign = await api.getUser(msg.author.id); if (ign == undefined) { await msg.reply(util.formatSorryUnknown(msg)); return; @@ -214,7 +207,7 @@ module.exports.showMatch = async (msg, args) => { // shorthand if (ign == "?") { - ign = await nameByUser(msg); + ign = await api.getUser(msg.author.id); if (ign == undefined) { await msg.reply(util.formatSorryUnknown(msg)); return; @@ -288,7 +281,7 @@ module.exports.showMatches = async (msg, args) => { // shorthand if (ign == "?") { - ign = await nameByUser(msg); + ign = await api.getUser(msg.author.id); if (ign == undefined) { await msg.reply(util.formatSorryUnknown(msg)); return; diff --git a/util.js b/util.js index a1deaf7..0e09b7a 100644 --- a/util.js +++ b/util.js @@ -114,6 +114,5 @@ module.exports.respond = async (msg, data, 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")}.`; -}; +module.exports.formatSorryUnknown = (msg) => + `You're unknown to our service. Try ${module.exports.usg(msg, "help vme")}.`; -- cgit v1.3.1 From f3aa08e72c6ffdb942ea06e21ac19e28b8cd5550 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 6 May 2017 15:37:48 +0200 Subject: vgme alias & wording --- commands/vainsocial/me.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index 717e36c..76619bb 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -11,7 +11,7 @@ module.exports = class RegisterUserCommand extends Commando.Command { constructor(client) { super(client, { name: "vainsocial-me", - aliases: ["vme"], + aliases: ["vme", "vgme"], group: "vainsocial", memberName: "vainsocial-me", description: "Register a users's in game name.", @@ -56,7 +56,7 @@ Store your in game name for quicker access to other commands and for Guild manag 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 have access to Guild management features. +You now create a Guild with ${util.usg(msg, "vgcreate")}. `); } }; -- cgit v1.3.1 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 From 54b59d1168e79c8706b164194bc1a48ec49726e4 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 13:24:41 +0200 Subject: error handling --- api.js | 56 +++++++++++++++++++--------------- commands/vainsocial/guild_add.js | 3 +- commands/vainsocial/guild_fame_calc.js | 36 ---------------------- commands/vainsocial/guild_update.js | 28 +++++++++++------ commands/vainsocial/match.js | 32 +++++++++++-------- commands/vainsocial/matches.js | 22 ++++++++----- commands/vainsocial/me.js | 16 +++++++--- commands/vainsocial/user.js | 23 +++++++++----- util.js | 10 ++++-- views/guild_add.js | 34 --------------------- views/match.js | 11 ++----- views/register.js | 16 +++++++--- views/view.js | 10 +++++- 13 files changed, 144 insertions(+), 153 deletions(-) delete mode 100644 commands/vainsocial/guild_fame_calc.js delete mode 100644 views/guild_add.js diff --git a/api.js b/api.js index d303cab..74614c2 100644 --- a/api.js +++ b/api.js @@ -43,19 +43,12 @@ module.exports.getMap = async (url) => { 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 { - return await request({ - uri: API_FE_URL + url, - qs: params, - json: true, - forever: true - }); - } catch (err) { - // TODO sort errors, loggly - return undefined; - } - }, { ttl: ttl }); + return await cache.wrap(cachekey, async () => await request({ + uri: API_FE_URL + url, + qs: params, + json: true, + forever: true + }), { ttl: ttl }); } // send a POST and optionally bust cache @@ -139,7 +132,13 @@ module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => { cache.del("matches+" + name); cache.del("player+" + name); } - if ([Channel.DONE, "search_fail"].indexOf(msg) != -1) { + if (msg == "search_fail") { + subscription.unsubscribe(); + throw { error: { + err: "No player found for the provided IGN." + } }; + } + if (msg == Channel.DONE) { subscription.unsubscribe(); return undefined; } @@ -156,14 +155,18 @@ module.exports.updatePlayer = (name) => api.postBE("/player/" + name + "/update"); // return player -module.exports.getPlayer = (name) => - api.getFE("/player/" + name, {}, 60, "player+" + name); +module.exports.getPlayer = async (name) => { + return await api.getFE("/player/" + name, {}, 60, "player+" + name); +} // search or update a player module.exports.upsearchPlayer = async (name) => { - if (await api.getPlayer(name) == undefined) + try { + await api.getPlayer(name); + await api.updatePlayer(name); + } catch (err) { await api.searchPlayer(name); - else await api.updatePlayer(name); + } } // block until update is completely done @@ -181,17 +184,18 @@ module.exports.upsearchPlayerSync = async (name) => { module.exports.getMatches = async (name) => { 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 = async (id) => - await api.getFE("/match/" + id, {}, 60 * 60); +module.exports.getMatch = async (id) => { + return await api.getFE("/match/" + id, {}, 60 * 60); +} // return a guild -module.exports.getGuild = (token) => - api.getFE("/guild", { user_token: token }, 60, "guild+" + token); +module.exports.getGuild = async (token) => { + return await api.getFE("/guild", { user_token: token }, 60, "guild+" + token); +} // TODO! cache guilds by guild id, not by user token // add user to guild @@ -231,5 +235,7 @@ module.exports.setUser = async (token, name) => { } // retrieve Discord ID -> IGN -module.exports.getUser = async (token) => - (await api.getFE("/user", { user_token: token }, 60, "user+" + token)).name; +module.exports.getUser = async (token) => { + const user = await api.getFE("/user", { user_token: token }, 60, "user+" + token); + return user.name; +} diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index 0021360..8474724 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -7,7 +7,7 @@ const Commando = require("discord.js-commando"), Promise = require("bluebird"), api = require("../../api"), util = require("../../util"), - GuildAddView = require("../../views/guild_add"); + GuildAddView = require("../../views/guild_progress"); module.exports = class AddGuildMemberCommand extends Commando.Command { constructor(client) { @@ -24,7 +24,6 @@ Register IGNs to your Guild. argsType: "multiple" }); } - // register a VainSocial Guild to a Discord account async run(msg, args) { util.trackAction(msg, "vainsocial-guild-add"); let playersData = {}; diff --git a/commands/vainsocial/guild_fame_calc.js b/commands/vainsocial/guild_fame_calc.js deleted file mode 100644 index e1ec912..0000000 --- a/commands/vainsocial/guild_fame_calc.js +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/node -/* jshint esnext:true */ -"use strict"; - -const Commando = require("discord.js-commando"), - oneLine = require("common-tags").oneLine, - api = require("../../api"), - util = require("../../util"); - -module.exports = class CalculateGuildFameCommand extends Commando.Command { - constructor(client) { - super(client, { - name: "vainsocial-guildcalc", - aliases: ["vguild-calc", "vgcalc"], - group: "vainsocial-guild", - memberName: "vainsocial-guildcalc", - description: "Update your Guild's fame.", - details: oneLine` -Recalculate your Guild members' fame. -`, - examples: ["vgcalc"] - }); - } - // internal / premium: immediately call backend fame refresh - async run(msg, args) { - util.trackAction(msg, "vainsocial-guild-calculate"); - const guild = await api.getGuild(msg.author.id); - if (guild == undefined) { - await msg.reply("You are not registered in any guilds."); - return; - } - await msg.reply("Your Guild members' fame will be updated soon…"); - await api.calculateGuild(guild.id, msg.author.id); - await msg.reply("Your Guild members' fame has been updated."); - } -}; diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js index 9c9fe1d..66e8a5f 100644 --- a/commands/vainsocial/guild_update.js +++ b/commands/vainsocial/guild_update.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"), + GuildMembersProgressView = require("../../views/guild_progress"); module.exports = class UpdateGuildCommand extends Commando.Command { constructor(client) { @@ -25,14 +26,21 @@ Update the match history for all your Guild members. // internal / premium: immediately call backend player refresh async run(msg, args) { util.trackAction(msg, "vainsocial-guild-update"); - const guild = await api.getGuild(msg.author.id); - if (guild == undefined) { - await msg.reply("You are not registered in any guilds."); - return; - } - // TODO progress report - await Promise.map(guild.members, - (member) => api.upsearchPlayer(member.player.name)); - await msg.reply("Your Guild members will be up to date soon."); + let playersData = {}; + const playersWaiters = args.map((name) => api.subscribeUpdates(name)), + guildUpdateView = new GuildMembersProgressView(msg, playersData); + // create waiter dict & data dict + await Promise.map(playersWaiters, async (waiter, idx) => { + await api.updatePlayer(args[idx]); + let success = false; + while (await waiter.next() != undefined) { + playersData[args[idx]] = await api.getPlayer(args[idx]); + await guildUpdateView.respond(); + success = true; + } + }); + await guildUpdateView.respond("Your Guild's fame is being updated…"); + await api.calculateGuild(guild.id, msg.author.id); + await guildUpdateView.respond("Your Guild was updated."); } }; diff --git a/commands/vainsocial/match.js b/commands/vainsocial/match.js index c3d2906..3acde84 100644 --- a/commands/vainsocial/match.js +++ b/commands/vainsocial/match.js @@ -42,19 +42,27 @@ module.exports = class ShowMatchCommand extends Commando.Command { } async run(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)); + let ign; + try { + ign = await util.ignForUser(args.name, msg.author.id); + } catch (err) { + return await new MatchView(msg, undefined).error(strings.unknown(msg)); + } - const 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); + const matchView = new MatchView(msg); + try { + const participations = await api.getMatches(ign); + if (args.number > participations.length) + return await msg.say(strings.tooFewMatches(ign)); + // wait for BE update + const waiter = api.subscribeUpdates(ign); + await api.upsearchPlayer(ign); - do await matchView.respond(); - while (await waiter.next() != undefined); + do await matchView.respond(participations[args.number-1].match_api_id); + while (await waiter.next() != undefined); + } catch (err) { + console.error(err); + return await matchView.error(err.error.err); + } } }; diff --git a/commands/vainsocial/matches.js b/commands/vainsocial/matches.js index 0197b8b..7499725 100644 --- a/commands/vainsocial/matches.js +++ b/commands/vainsocial/matches.js @@ -33,16 +33,24 @@ module.exports = class ShowMatchesCommand extends Commando.Command { } async run(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); + let ign; + try { + ign = await util.ignForUser(args.name, msg.author.id); + } catch (err) { + 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); + try { + const waiter = api.subscribeUpdates(ign); + await api.upsearchPlayer(ign); - do await matchesView.respond(); - while (await waiter.next() != undefined); + do await matchesView.respond(); + while (await waiter.next() != undefined); + } catch (err) { + console.log(err); + return await matchesView.error(err.error.err); + } } }; diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index e17afea..d2afb24 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -5,7 +5,8 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, api = require("../../api"), - util = require("../../util"); + util = require("../../util"), + RegisterView = require("../../views/register"); module.exports = class RegisterUserCommand extends Commando.Command { constructor(client) { @@ -33,8 +34,15 @@ 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) { util.trackAction(msg, "vainsocial-me", args.name); - await api.upsearchPlayer(ign); - await api.subscribeUpdates(ign).next(); - await new RegisterView(msg).respond(); + const registerView = new RegisterView(msg, args.name); + await api.upsearchPlayer(args.name); + try { + await api.subscribeUpdates(args.name).next(); + await api.setUser(msg.author.id, args.name); + } catch (err) { + console.log(err); + return await registerView.error(err.error.err); + } + await registerView.respond(); } }; diff --git a/commands/vainsocial/user.js b/commands/vainsocial/user.js index bfcc1a0..e251ad7 100644 --- a/commands/vainsocial/user.js +++ b/commands/vainsocial/user.js @@ -36,15 +36,24 @@ Display VainSocial lifetime statistics from Vainglory } async run(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)); + let ign; + try { + ign = await util.ignForUser(args.name, msg.author.id); + } catch (err) { + return await new PlayerView(msg, args.name).error(strings.unknown(msg)); + } const playerView = new PlayerView(msg, ign); - // wait for BE update - const waiter = api.subscribeUpdates(ign); - await api.upsearchPlayer(ign); + try { + // wait for BE update + const waiter = api.subscribeUpdates(ign); + await api.upsearchPlayer(ign); - do await playerView.respond(); - while (await waiter.next() != undefined); + do await playerView.respond(); + while (await waiter.next() != undefined); + } catch (err) { + console.log(err); + return await playerView.error(err.error.err); + } } }; diff --git a/util.js b/util.js index a20cbaa..0f37918 100644 --- a/util.js +++ b/util.js @@ -142,6 +142,12 @@ module.exports.paginate = function* chunks(arr, pagesize) { } // return ign, or associated ign, or undefined -module.exports.ignForUser = async (name, user_token) => +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); + if (name != "?") return name; + try { + return await api.getUser(user_token); + } catch (err) { + return undefined; + } +} diff --git a/views/guild_add.js b/views/guild_add.js deleted file mode 100644 index cb32dc3..0000000 --- a/views/guild_add.js +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/node -/* jshint esnext:true */ -"use strict"; - -const View = require("./view"), - util = require("../util"), - api = require("../api"), - strings = require("../strings"), - oneLine = require("common-tags").oneLine; - -const GuildAddView = module.exports; - -// match detail view -module.exports = class extends View { - constructor(msg, players) { - super(msg); - this.players = players; - } - - // players: obj, key=ign, value=player - async text(players) { - return Object.entries(players).map((tuple) => - (tuple[1] == undefined)? - `Loading ${tuple[0]}…` - : `Loaded ${tuple[0]}.` - ).join("\n"); - } - - async respond() { - this.response = await util.respond(this.msg, - await this.text(this.players), this.response); - return this.response; - }; -} diff --git a/views/match.js b/views/match.js index 8a04045..2dc55ec 100644 --- a/views/match.js +++ b/views/match.js @@ -15,13 +15,8 @@ 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) { + async text(match) { let resps = []; for(let roster of match.rosters) { let winstr = "Won"; @@ -49,8 +44,8 @@ module.exports = class extends View { return embed; }; - async respond() { - const match = await api.getMatch(this.matchid); + async respond(matchid) { + const match = await api.getMatch(matchid); this.response = await util.respond(this.msg, await this.embed(match), this.response); return this.response; diff --git a/views/register.js b/views/register.js index 0e5ff71..9943299 100644 --- a/views/register.js +++ b/views/register.js @@ -2,7 +2,8 @@ /* jshint esnext:true */ "use strict"; -const View = require("./view"), +const emoji = require("discord-emoji"), + View = require("./view"), util = require("../util"), api = require("../api"), strings = require("../strings"), @@ -12,6 +13,11 @@ const RegisterView = module.exports; // user register view module.exports = class extends View { + constructor(msg, ign) { + super(msg); + this.ign = ign; + } + async text() { return `You are now registered at VainSocial, ${this.msg.author.mention}.`; } @@ -22,18 +28,18 @@ module.exports = class extends View { 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(); + await new PlayerView(this.msg, this.ign).respond(); }; return reactions; } async respond() { - await api.setUser(msg.author.id, ign); + console.log("************************"); + console.log(await this.text()); 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)); + await this.buttons()); this.hasButtons = true; } return this.response; diff --git a/views/view.js b/views/view.js index 3d3ae38..771e3db 100644 --- a/views/view.js +++ b/views/view.js @@ -33,7 +33,15 @@ module.exports = class { // 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); + if (!this.hasButtons) { + await util.reactionButtons(this.response, await this.buttons(), this.msg); + this.hasButtons = true; + } + return this.response; + } + async error(text) { + // reply with error + this.response = await util.respond(this.msg, text, this.response); return this.response; } }; -- cgit v1.3.1 From 94adb2f9f182e50f6d60aadc70bb8bf8f74f2371 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 13:42:45 +0200 Subject: more error handling --- commands/vainsocial/guild_create.js | 21 +++++++++++++-------- commands/vainsocial/guild_view.js | 8 +++++++- views/guild.js | 8 +------- views/guild_create.js | 5 +++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/commands/vainsocial/guild_create.js b/commands/vainsocial/guild_create.js index ad8d0e7..c28fe2a 100644 --- a/commands/vainsocial/guild_create.js +++ b/commands/vainsocial/guild_create.js @@ -48,13 +48,18 @@ 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 new GuildCreateView(msg, msg.author.id).respond(); + const guildCreateView = new GuildCreateView(msg, msg.author.id); + try { + await api.post("/guild", { + shard_id: args.region, + name: args.name, + identifier: args.tag, + user_token: msg.author.id + }); + } catch (err) { + console.error(err); + return await guildCreateView.error(err.error.err); + } + await guildCreateView.respond(); } }; diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js index 7deb66f..472b284 100644 --- a/commands/vainsocial/guild_view.js +++ b/commands/vainsocial/guild_view.js @@ -33,6 +33,12 @@ Show a summary of your Guild. // show Guild details async run(msg, args) { util.trackAction(msg, "vainsocial-guild-view"); - await new GuildOverviewView(msg, msg.author.id).respond(); + const guildOverviewView = new GuildOverviewView(msg); + try { + const guild = await api.getGuild(msg.author.id); + await guildOverviewView.respond(guild); + } catch (err) { + return await guildOverviewView.error(err.error.err); + } } }; diff --git a/views/guild.js b/views/guild.js index 1a4cb78..a5eedef 100644 --- a/views/guild.js +++ b/views/guild.js @@ -12,11 +12,6 @@ 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); @@ -31,8 +26,7 @@ module.exports = class extends View { return embed; }; - async respond() { - const guild = await api.getGuild(this.user_token); + async respond(guild) { if (guild == undefined) { this.response = await util.respond(this.msg, strings.notRegistered, this.response); diff --git a/views/guild_create.js b/views/guild_create.js index 9720191..8e7be81 100644 --- a/views/guild_create.js +++ b/views/guild_create.js @@ -2,7 +2,8 @@ /* jshint esnext:true */ "use strict"; -const View = require("./view"), +const emoji = require("discord-emoji"), + View = require("./view"), util = require("../util"), api = require("../api"), strings = require("../strings"), @@ -29,7 +30,7 @@ module.exports = class extends View { let reactions = {}; reactions[emoji.symbols.information_source] = async () => { util.trackAction(this.msg, "reaction-guildview"); - await new GuildOverviewView(this.user_token).respond(); + await new GuildOverviewView(this.msg).respond(this.user_token); }; return reactions; } -- cgit v1.3.1 From 0ee8e0e5239e3f76911834da54d2c72a4a149804 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 18:02:15 +0200 Subject: error handling, migration to template --- api.js | 2 +- commands/vainsocial/guild_add.js | 2 +- commands/vainsocial/guild_update.js | 33 +++++++++++++++++++++++++++------ commands/vainsocial/guild_view.js | 2 ++ commands/vainsocial/me.js | 3 ++- views/guild.js | 11 +++-------- views/guild_create.js | 2 +- views/guild_progress.js | 33 +++++++++++++++++++++++++++++++++ views/guild_update.js | 34 ++++++++++++++++++++++++++++++++++ views/matches.js | 7 +++---- views/player.js | 19 ++++++++----------- views/register.js | 6 ++---- 12 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 views/guild_progress.js create mode 100644 views/guild_update.js diff --git a/api.js b/api.js index 74614c2..c809309 100644 --- a/api.js +++ b/api.js @@ -200,7 +200,7 @@ module.exports.getGuild = async (token) => { // add user to guild module.exports.addToGuild = async (token, member) => { - const membership = await postFE("/guild/members", { + const membership = await api.postFE("/guild/members", { user_token: token, member_name: member }, "guild+" + token); diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index 8474724..8fe952c 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -33,7 +33,7 @@ Register IGNs to your Guild. await Promise.map(playersWaiters, async (waiter, idx) => { await api.upsearchPlayer(args[idx]); let success = false; - while (await waiter.next() != undefined) { + while (["stats_update", undefined].indexOf(await waiter.next())) { playersData[args[idx]] = await api.getPlayer(args[idx]); await guildAddView.respond(); success = true; diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js index 66e8a5f..7d36b0e 100644 --- a/commands/vainsocial/guild_update.js +++ b/commands/vainsocial/guild_update.js @@ -26,21 +26,42 @@ Update the match history for all your Guild members. // internal / premium: immediately call backend player refresh async run(msg, args) { util.trackAction(msg, "vainsocial-guild-update"); + // obj of ign: player let playersData = {}; - const playersWaiters = args.map((name) => api.subscribeUpdates(name)), - guildUpdateView = new GuildMembersProgressView(msg, playersData); + // collect an array of IGNs + let names, guild; + const guildUpdateView = new GuildMembersProgressView(msg, + playersData); + try { + guild = await api.getGuild(msg.author.id); + names = guild.members.map((m) => m.player.name); + } catch (err) { + console.log(err); + return await guildUpdateView.error(err.error.err); + } + // update all the IGNs + const playersWaiters = names.map((name) => api.subscribeUpdates(name)); // create waiter dict & data dict await Promise.map(playersWaiters, async (waiter, idx) => { - await api.updatePlayer(args[idx]); + await api.updatePlayer(names[idx]); let success = false; - while (await waiter.next() != undefined) { - playersData[args[idx]] = await api.getPlayer(args[idx]); + while (["stats_update", undefined].indexOf(await waiter.next())) { + try { + playersData[names[idx]] = await api.getPlayer(names[idx]); + } catch (err) { + playersData[names[idx]] = undefined; + } await guildUpdateView.respond(); success = true; } }); await guildUpdateView.respond("Your Guild's fame is being updated…"); - await api.calculateGuild(guild.id, msg.author.id); + try { + await api.calculateGuild(guild.id, msg.author.id); + } catch (err) { + console.log(err); + await guildUpdateView.error(err.error.err); + } await guildUpdateView.respond("Your Guild was updated."); } }; diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js index 472b284..c8375d4 100644 --- a/commands/vainsocial/guild_view.js +++ b/commands/vainsocial/guild_view.js @@ -5,6 +5,7 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, util = require("../../util"), + api = require("../../api"), GuildOverviewView = require("../../views/guild"); module.exports = class ViewGuildCommand extends Commando.Command { @@ -38,6 +39,7 @@ Show a summary of your Guild. const guild = await api.getGuild(msg.author.id); await guildOverviewView.respond(guild); } catch (err) { + console.log(err); return await guildOverviewView.error(err.error.err); } } diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index d2afb24..de87b4c 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -37,7 +37,8 @@ Store your in game name for quicker access to other commands and for Guild manag const registerView = new RegisterView(msg, args.name); await api.upsearchPlayer(args.name); try { - await api.subscribeUpdates(args.name).next(); + const waiter = api.subscribeUpdates(args.name); + while (await waiter.next() != "stats_update"); await api.setUser(msg.author.id, args.name); } catch (err) { console.log(err); diff --git a/views/guild.js b/views/guild.js index a5eedef..23c2afe 100644 --- a/views/guild.js +++ b/views/guild.js @@ -24,16 +24,11 @@ module.exports = class extends View { "", "vainsocial-guild-view") .setDescription(await this.text(guild.members)); return embed; - }; + } - async respond(guild) { - if (guild == undefined) { - this.response = await util.respond(this.msg, - strings.notRegistered, this.response); - return this.response; - } + async respond(guild, extra="") { this.response = await util.respond(this.msg, await this.embed(guild), this.response); return this.response; - }; + } } diff --git a/views/guild_create.js b/views/guild_create.js index 8e7be81..38720e1 100644 --- a/views/guild_create.js +++ b/views/guild_create.js @@ -38,7 +38,7 @@ module.exports = class extends View { // TODO move to super class async respond() { this.response = await util.respond(this.msg, - await this.text(), this.response); + await this.text() + "\n" + await this.help(), this.response); if (!this.hasButtons) { await util.reactionButtons(this.response, await this.buttons()); diff --git a/views/guild_progress.js b/views/guild_progress.js new file mode 100644 index 0000000..163c627 --- /dev/null +++ b/views/guild_progress.js @@ -0,0 +1,33 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const View = require("./view"), + util = require("../util"), + api = require("../api"), + strings = require("../strings"), + oneLine = require("common-tags").oneLine; + +const GuildMembersProgressView = module.exports; + +module.exports = class extends View { + constructor(msg, players) { + super(msg); + this.players = players; + } + + // players: obj, key=ign, value=player + async text(players) { + return Object.entries(players).map((tuple) => + (tuple[1] == undefined)? + `Loading ${tuple[0]}…` + : `Loaded ${tuple[0]}.` + ).join("\n"); + } + + async respond(extra="") { + this.response = await util.respond(this.msg, + await this.text(this.players) + "\n" + extra, this.response); + return this.response; + }; +} diff --git a/views/guild_update.js b/views/guild_update.js new file mode 100644 index 0000000..cb32dc3 --- /dev/null +++ b/views/guild_update.js @@ -0,0 +1,34 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const View = require("./view"), + util = require("../util"), + api = require("../api"), + strings = require("../strings"), + oneLine = require("common-tags").oneLine; + +const GuildAddView = module.exports; + +// match detail view +module.exports = class extends View { + constructor(msg, players) { + super(msg); + this.players = players; + } + + // players: obj, key=ign, value=player + async text(players) { + return Object.entries(players).map((tuple) => + (tuple[1] == undefined)? + `Loading ${tuple[0]}…` + : `Loaded ${tuple[0]}.` + ).join("\n"); + } + + async respond() { + this.response = await util.respond(this.msg, + await this.text(this.players), this.response); + return this.response; + }; +} diff --git a/views/matches.js b/views/matches.js index 279bf9c..a79654b 100644 --- a/views/matches.js +++ b/views/matches.js @@ -39,8 +39,8 @@ Score | ${emojiScore} \`${Math.floor(100 * participant.stats.impact_score)}%\` const matchesPart = matches.slice(0, MATCH_HISTORY_LEN); // build embed - let embed = util.vainsocialEmbed( - this.ign, "player/" + this.ign, "vainsocial-matches") + 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)); @@ -60,9 +60,8 @@ Score | ${emojiScore} \`${Math.floor(100 * participant.stats.impact_score)}%\` 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(); + await new MatchView(this.msg).respond(m.match_api_id); }); return reactions; } diff --git a/views/player.js b/views/player.js index ec5d38b..a481c69 100644 --- a/views/player.js +++ b/views/player.js @@ -72,7 +72,7 @@ ${emoji.symbols["1234"]} or ${util.usg(this.msg, "vh " + this.ign)} for more*`; 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(); + await new MatchView(this.msg).respond(matches[0].match_api_id); }; reactions[emoji.symbols["1234"]] = async () => { util.trackAction(this.msg, "reaction-matches", player.name); @@ -82,16 +82,13 @@ ${emoji.symbols["1234"]} or ${util.usg(this.msg, "vh " + this.ign)} for more*`; } 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) { + let player, matches; + try { + [player, matches] = await Promise.all([ + api.getPlayer(this.ign), + api.getMatches(this.ign) + ]); + } catch (err) { this.response = await util.respond(this.msg, strings.loading(this.ign), this.response); return this.response; diff --git a/views/register.js b/views/register.js index 9943299..1df3bf1 100644 --- a/views/register.js +++ b/views/register.js @@ -19,7 +19,7 @@ module.exports = class extends View { } async text() { - return `You are now registered at VainSocial, ${this.msg.author.mention}.`; + return `You are now registered at VainSocial, @${this.msg.author.tag}.`; } 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*` @@ -33,10 +33,8 @@ module.exports = class extends View { return reactions; } async respond() { - console.log("************************"); - console.log(await this.text()); this.response = await util.respond(this.msg, - await this.text(), this.response); + await this.text() + "\n" + await this.help(), this.response); if (!this.hasButtons) { await util.reactionButtons(this.response, await this.buttons()); -- cgit v1.3.1 From dff1dca205950fd0eaf643b18e3998c924f3f9a5 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 18:37:02 +0200 Subject: try to fix async issue, kick players --- api.js | 19 ++++++++++++++++++- commands/vainsocial/guild_add.js | 14 ++++++++++---- commands/vainsocial/guild_update.js | 2 +- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/api.js b/api.js index c809309..2d61a01 100644 --- a/api.js +++ b/api.js @@ -61,6 +61,16 @@ module.exports.postFE = module.exports.post = async (url, params={}, cachekey=un }); } +// send a DELETE and optionally bust cache +module.exports.deleteFE = module.exports.post = async (url, params={}, cachekey=undefined) => { + if (cachekey) cache.del(cachekey); + return await request.delete(API_FE_URL + url, { + form: params, + json: true, + forever: true + }); +} + module.exports.postBE = module.exports.backend = async (url) => { return await request.post({ uri: API_BE_URL + url, @@ -204,7 +214,14 @@ module.exports.addToGuild = async (token, member) => { user_token: token, member_name: member }, "guild+" + token); - cache.del("guild+" + token); + return membership; +} + +// kick from guild +module.exports.removeFromGuild = async (token, member) => { + const membership = await api.deleteFE("/guild/members/" + member, { + user_token: token + }, "guild+" + token); return membership; } diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index 8fe952c..75eb5f3 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -30,17 +30,23 @@ Register IGNs to your Guild. 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 Promise.each(playersWaiters, async (waiter, idx) => { await api.upsearchPlayer(args[idx]); let success = false; - while (["stats_update", undefined].indexOf(await waiter.next())) { - playersData[args[idx]] = await api.getPlayer(args[idx]); + while (["stats_update", "matches_update", undefined].indexOf( + await waiter.next())) { + try { + playersData[args[idx]] = await api.getPlayer(args[idx]); + success = true; + } catch (err) { + playersData[args[idx]] = undefined; + } await guildAddView.respond(); - success = true; } if (success) { await api.addToGuild(msg.author.id, args[idx]); } }); + await guildAddView.respond("Your Guild members were added."); } }; diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js index 7d36b0e..f0360ea 100644 --- a/commands/vainsocial/guild_update.js +++ b/commands/vainsocial/guild_update.js @@ -42,7 +42,7 @@ Update the match history for all your Guild members. // update all the IGNs const playersWaiters = names.map((name) => api.subscribeUpdates(name)); // create waiter dict & data dict - await Promise.map(playersWaiters, async (waiter, idx) => { + await Promise.each(playersWaiters, async (waiter, idx) => { await api.updatePlayer(names[idx]); let success = false; while (["stats_update", undefined].indexOf(await waiter.next())) { -- cgit v1.3.1 From 7d869892d9ba31117c32686ab8ca7783c0229191 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 19:00:22 +0200 Subject: forgot guild rm cmd file --- commands/vainsocial/guild_rm.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 commands/vainsocial/guild_rm.js diff --git a/commands/vainsocial/guild_rm.js b/commands/vainsocial/guild_rm.js new file mode 100644 index 0000000..6498278 --- /dev/null +++ b/commands/vainsocial/guild_rm.js @@ -0,0 +1,41 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + Promise = require("bluebird"), + api = require("../../api"), + util = require("../../util"), + GuildRmView = require("../../views/guild_progress"); + +module.exports = class AddGuildMemberCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildrm", + aliases: ["vguild-rm", "vgrm", "vgr"], + group: "vainsocial-guild", + memberName: "vainsocial-guildrm", + description: "Remove a member from your Guild.", + details: oneLine` +Remove an IGN from your Guild. +`, + examples: ["vgrm StormCallerSr", "vgrm StormCallerSr shutterfly"], + argsType: "multiple" + }); + } + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-rm"); + let playersData = {}; + const guildRmView = new GuildRmView(msg, playersData); + try { + await Promise.each(args, async (name) => { + await api.removeFromGuild(msg.author.id, name); + await guildRmView.respond(); + }); + } catch (err) { + return await guildRmView.error(err.error.err); + } + await guildRmView.respond("Your Guild members were removed."); + } +}; -- cgit v1.3.1 From 61c2bbf07b047b537efc68cb494b987fbf3f7682 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 19:13:47 +0200 Subject: fix post sending delete --- api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.js b/api.js index 2d61a01..eddac54 100644 --- a/api.js +++ b/api.js @@ -62,7 +62,7 @@ module.exports.postFE = module.exports.post = async (url, params={}, cachekey=un } // send a DELETE and optionally bust cache -module.exports.deleteFE = module.exports.post = async (url, params={}, cachekey=undefined) => { +module.exports.deleteFE = module.exports.delete = async (url, params={}, cachekey=undefined) => { if (cachekey) cache.del(cachekey); return await request.delete(API_FE_URL + url, { form: params, -- cgit v1.3.1 From 343648f0e9faefaf13ffa3e039b92d67970c31fc Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 19:20:19 +0200 Subject: log guild rm --- commands/vainsocial/guild_rm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/commands/vainsocial/guild_rm.js b/commands/vainsocial/guild_rm.js index 6498278..24bad36 100644 --- a/commands/vainsocial/guild_rm.js +++ b/commands/vainsocial/guild_rm.js @@ -34,6 +34,7 @@ Remove an IGN from your Guild. await guildRmView.respond(); }); } catch (err) { + console.error(err); return await guildRmView.error(err.error.err); } await guildRmView.respond("Your Guild members were removed."); -- cgit v1.3.1 From d158b9dabfc8b6c9a6c13e3307be455957b95a98 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 9 May 2017 19:47:50 +0200 Subject: fix progress reports --- commands/vainsocial/guild_add.js | 28 +++++++++++++++++----------- commands/vainsocial/guild_rm.js | 14 ++++++++++---- commands/vainsocial/guild_update.js | 12 +++++------- views/guild_progress.js | 7 ++----- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index 75eb5f3..c1de856 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -26,25 +26,31 @@ Register IGNs to your Guild. } async run(msg, args) { util.trackAction(msg, "vainsocial-guild-add"); - let playersData = {}; + let playersStatus = {}; const playersWaiters = args.map((name) => api.subscribeUpdates(name)), - guildAddView = new GuildAddView(msg, playersData); + guildAddView = new GuildAddView(msg, playersStatus); // create waiter dict & data dict await Promise.each(playersWaiters, async (waiter, idx) => { await api.upsearchPlayer(args[idx]); let success = false; - while (["stats_update", "matches_update", undefined].indexOf( - await waiter.next())) { - try { - playersData[args[idx]] = await api.getPlayer(args[idx]); - success = true; - } catch (err) { - playersData[args[idx]] = undefined; - } - await guildAddView.respond(); + try { + do { + try { + await api.getPlayer(args[idx]); + playersStatus[args[idx]] = "Loading…"; + success = true; + } catch (err) { } + await guildAddView.respond(); + } while (["stats_update", "matches_update", undefined] + .indexOf(await waiter.next())); + } catch (err) { + console.error(err); + playersStatus[args[idx]] = err.error.err; + success = false; } if (success) { await api.addToGuild(msg.author.id, args[idx]); + playersStatus[args[idx]] = "Loaded."; } }); await guildAddView.respond("Your Guild members were added."); diff --git a/commands/vainsocial/guild_rm.js b/commands/vainsocial/guild_rm.js index 24bad36..4857c44 100644 --- a/commands/vainsocial/guild_rm.js +++ b/commands/vainsocial/guild_rm.js @@ -26,12 +26,18 @@ Remove an IGN from your Guild. } async run(msg, args) { util.trackAction(msg, "vainsocial-guild-rm"); - let playersData = {}; - const guildRmView = new GuildRmView(msg, playersData); + let playersStatus = {}; + const guildRmView = new GuildRmView(msg, playersStatus); try { await Promise.each(args, async (name) => { - await api.removeFromGuild(msg.author.id, name); - await guildRmView.respond(); + try { + playersStatus[name] = "Removing…"; + await api.removeFromGuild(msg.author.id, name); + playersStatus[name] = "Removed."; + await guildRmView.respond(); + } catch (err) { + playersStatus[name] = err.error.err; + } }); } catch (err) { console.error(err); diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js index f0360ea..c3163be 100644 --- a/commands/vainsocial/guild_update.js +++ b/commands/vainsocial/guild_update.js @@ -27,11 +27,10 @@ Update the match history for all your Guild members. async run(msg, args) { util.trackAction(msg, "vainsocial-guild-update"); // obj of ign: player - let playersData = {}; + let playersStatus = {}; // collect an array of IGNs let names, guild; - const guildUpdateView = new GuildMembersProgressView(msg, - playersData); + const guildUpdateView = new GuildMembersProgressView(msg, playersStatus); try { guild = await api.getGuild(msg.author.id); names = guild.members.map((m) => m.player.name); @@ -44,15 +43,14 @@ Update the match history for all your Guild members. // create waiter dict & data dict await Promise.each(playersWaiters, async (waiter, idx) => { await api.updatePlayer(names[idx]); - let success = false; while (["stats_update", undefined].indexOf(await waiter.next())) { try { - playersData[names[idx]] = await api.getPlayer(names[idx]); + await api.getPlayer(names[idx]); + playersStatus[names[idx]] = "Loaded."; } catch (err) { - playersData[names[idx]] = undefined; + playersStatus[names[idx]] = "Loading…"; } await guildUpdateView.respond(); - success = true; } }); await guildUpdateView.respond("Your Guild's fame is being updated…"); diff --git a/views/guild_progress.js b/views/guild_progress.js index 163c627..b3fdeb3 100644 --- a/views/guild_progress.js +++ b/views/guild_progress.js @@ -16,13 +16,10 @@ module.exports = class extends View { this.players = players; } - // players: obj, key=ign, value=player + // players: obj, key=ign, value=progress async text(players) { return Object.entries(players).map((tuple) => - (tuple[1] == undefined)? - `Loading ${tuple[0]}…` - : `Loaded ${tuple[0]}.` - ).join("\n"); + `${tuple[0]}: ${tuple[1]}`).join("\n"); } async respond(extra="") { -- cgit v1.3.1 From 2fad80a5350b5c9a1eeaa5062996b63cabace690 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 10 May 2017 19:33:09 +0200 Subject: encode embed urls, fixes players with spaces --- util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.js b/util.js index 0f37918..b86b8a7 100644 --- a/util.js +++ b/util.js @@ -23,7 +23,7 @@ module.exports.vainsocialEmbed = (title, link, command) => { return new Discord.RichEmbed() .setTitle(title) .setColor("#55ADD3") - .setURL(ROOTURL + link + util.track(command)) + .setURL(ROOTURL + encodeURIComponent(link) + util.track(command)) .setAuthor("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png", ROOTURL + util.track(command)) .setFooter("VainSocial" + (PREVIEW? " preview":""), ROOTURL + "images/brands/logo-blue.png") -- cgit v1.3.1 From 870858bedcabd94171d6518d6d5f3eacbb790193 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 11 May 2017 17:37:52 +0200 Subject: guild add: don't fail on single invalid ign --- commands/vainsocial/guild_add.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index c1de856..e3a0008 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -34,10 +34,10 @@ Register IGNs to your Guild. await api.upsearchPlayer(args[idx]); let success = false; try { + playersStatus[args[idx]] = "Loading…"; do { try { await api.getPlayer(args[idx]); - playersStatus[args[idx]] = "Loading…"; success = true; } catch (err) { } await guildAddView.respond(); -- cgit v1.3.1 From 51e9c6d0c58be8f99c166aa7be85342c9a1d5f83 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 11 May 2017 17:46:11 +0200 Subject: guild create: set minimum tag length to 2 --- commands/vainsocial/guild_create.js | 2 +- commands/vainsocial/guild_view.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/vainsocial/guild_create.js b/commands/vainsocial/guild_create.js index c28fe2a..e742609 100644 --- a/commands/vainsocial/guild_create.js +++ b/commands/vainsocial/guild_create.js @@ -32,7 +32,7 @@ Create a Guild with your VainSocial profile as leader. label: "tag", prompt: "Please specify your Guild's tag.", type: "string", - min: 3, + min: 2, max: 4 }, { // TODO use enum diff --git a/commands/vainsocial/guild_view.js b/commands/vainsocial/guild_view.js index c8375d4..28ddad3 100644 --- a/commands/vainsocial/guild_view.js +++ b/commands/vainsocial/guild_view.js @@ -26,7 +26,7 @@ Show a summary of your Guild. label: "name", prompt: "Please specify your Guild's name.", type: "string", - min: 3, + min: 2, default: "?" } ] }); -- cgit v1.3.1 From 3a857d9795d467fcb91910feb23e12ac3c18393c Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 11 May 2017 17:56:56 +0200 Subject: fix tracking failing the bot in DMs --- util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util.js b/util.js index b86b8a7..dccd874 100644 --- a/util.js +++ b/util.js @@ -55,8 +55,8 @@ module.exports.trackAction = (msg, action, ign="") => { user.pageview({ documentPath: action, documentTitle: ign, - campaignSource: msg.guild.id, - campaignMedium: msg.guild.name + campaignSource: (msg.guild? msg.guild.id : 0), + campaignMedium: (msg.guild? msg.guild.name : "private") }).send(); }; -- cgit v1.3.1 From 01a0f341cd19247c9b3d47bf62edefcb0c1545bc Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 11 May 2017 20:08:25 +0200 Subject: guild view: sort by fame, show role; add guild role cmd --- api.js | 20 +++++++++++++++++ commands/vainsocial/guild_rm.js | 2 +- commands/vainsocial/guild_role.js | 47 +++++++++++++++++++++++++++++++++++++++ views/guild.js | 4 ++-- views/simple.js | 24 ++++++++++++++++++++ 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 commands/vainsocial/guild_role.js create mode 100644 views/simple.js diff --git a/api.js b/api.js index eddac54..7f0c35d 100644 --- a/api.js +++ b/api.js @@ -71,6 +71,16 @@ module.exports.deleteFE = module.exports.delete = async (url, params={}, cacheke }); } +// send a PUT and optionally bust cache +module.exports.putFE = module.exports.put = async (url, params={}, cachekey=undefined) => { + if (cachekey) cache.del(cachekey); + return await request.put(API_FE_URL + url, { + form: params, + json: true, + forever: true + }); +} + module.exports.postBE = module.exports.backend = async (url) => { return await request.post({ uri: API_BE_URL + url, @@ -225,6 +235,16 @@ module.exports.removeFromGuild = async (token, member) => { return membership; } +// change a role +module.exports.changeRole = async (token, member, role) => { + const membership = await api.putFE("/guild/members/updateRole", { + user_token: token, + member_name: member, + new_role: role + }, "guild+" + token); + return membership; +} + // recalc fame, block until timeout or points update module.exports.calculateGuild = async (id, token) => { const channel = new Channel(), diff --git a/commands/vainsocial/guild_rm.js b/commands/vainsocial/guild_rm.js index 4857c44..e74aea4 100644 --- a/commands/vainsocial/guild_rm.js +++ b/commands/vainsocial/guild_rm.js @@ -13,7 +13,7 @@ module.exports = class AddGuildMemberCommand extends Commando.Command { constructor(client) { super(client, { name: "vainsocial-guildrm", - aliases: ["vguild-rm", "vgrm", "vgr"], + aliases: ["vguild-rm", "vgrm"], group: "vainsocial-guild", memberName: "vainsocial-guildrm", description: "Remove a member from your Guild.", diff --git a/commands/vainsocial/guild_role.js b/commands/vainsocial/guild_role.js new file mode 100644 index 0000000..b37bb79 --- /dev/null +++ b/commands/vainsocial/guild_role.js @@ -0,0 +1,47 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + Promise = require("bluebird"), + api = require("../../api"), + util = require("../../util"), + SimpleView = require("../../views/simple"); + +module.exports = class RoleGuildMemberCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildrole", + aliases: ["vguild-role", "vgrole", "vgr"], + group: "vainsocial-guild", + memberName: "vainsocial-guildrole", + description: "Change a Guild member's role.", + examples: ["vgrole StormCallerSr Officer", "vgrole shutterfly Leader"], + args: [ { + key: "name", + label: "name", + prompt: "Please specify a Guild member's name.", + type: "string", + min: 2, + default: "?" + }, { + key: "role", + label: "role", + prompt: "Please specify the member's new role.", + type: "string" + } ] + }); + } + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-role"); + const simpleView = new SimpleView(msg); + try { + const member = await api.changeRole(msg.author.id, args.name, args.role); + await simpleView.respond(`Successfully changed ${args.name}'s role to ${args.role}.`); + } catch (err) { + console.log(err); + return await simpleView.error(err.error.err); + } + } +}; diff --git a/views/guild.js b/views/guild.js index 23c2afe..6a8658b 100644 --- a/views/guild.js +++ b/views/guild.js @@ -14,9 +14,9 @@ const GuildOverviewView = module.exports; module.exports = class extends View { async text(members) { // TODO remove when API supports order by fame - members = members.sort((m1, m2) => m1.fame < m2.fame); + members.sort((m1, m2) => m1.fame < m2.fame); return members.map((member) => - `${member.player.name} ${member.fame}`).join("\n"); + `${member.player.name} *${member.status}* ${member.fame}`).join("\n"); } async embed(guild) { diff --git a/views/simple.js b/views/simple.js new file mode 100644 index 0000000..5e8701a --- /dev/null +++ b/views/simple.js @@ -0,0 +1,24 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const View = require("./view"), + util = require("../util"), + api = require("../api"), + strings = require("../strings"), + oneLine = require("common-tags").oneLine; + +const SimpleView = module.exports; + +// just respond with a text +module.exports = class extends View { + async text(txt) { + return txt; + } + + async respond(text) { + this.response = await util.respond(this.msg, + await this.text(text), this.response); + return this.response; + } +} -- cgit v1.3.1 From f37c547a11b1102c8639219c532711b56a40103d Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 11 May 2017 21:18:24 +0200 Subject: add vgmember command --- commands/vainsocial/guild_member.js | 47 +++++++++++++++++++++++++++++++++++++ views/guild.js | 7 +++--- views/guild_member.js | 35 +++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 commands/vainsocial/guild_member.js create mode 100644 views/guild_member.js diff --git a/commands/vainsocial/guild_member.js b/commands/vainsocial/guild_member.js new file mode 100644 index 0000000..2a1bf27 --- /dev/null +++ b/commands/vainsocial/guild_member.js @@ -0,0 +1,47 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + Promise = require("bluebird"), + api = require("../../api"), + util = require("../../util"), + GuildMemberView = require("../../views/guild_member"); + +module.exports = class ViewGuildMemberCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildmember", + aliases: ["vguild-member", "vgm"], + group: "vainsocial-guild", + memberName: "vainsocial-guildmember", + description: "View a Guild member in detail.", + examples: ["vgm StormCallerSr"], + args: [ { + key: "name", + label: "name", + prompt: "Please specify a Guild member's name.", + type: "string", + min: 2, + default: "?" + } ] + }); + } + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-member"); + const guildMemberView = new GuildMemberView(msg); + try { + const guild = await api.getGuild(msg.author.id); + const member = guild.members.filter((m) => m.player.name == args.name)[0]; + if (member == undefined) + throw { err: { error: "Player is not in the Guild." } }; + const player = await api.getPlayer(member.player.name); + const matches = await api.getMatches(player.name); + await guildMemberView.respond(member, player, matches); + } catch (err) { + console.error(err); + return await guildMemberView.error(err.error.err); + } + } +}; diff --git a/views/guild.js b/views/guild.js index 6a8658b..91df6bb 100644 --- a/views/guild.js +++ b/views/guild.js @@ -3,20 +3,21 @@ "use strict"; const View = require("./view"), + GuildMemberView = require("./guild_member"), util = require("../util"), api = require("../api"), strings = require("../strings"), + Promise = require("bluebird"), oneLine = require("common-tags").oneLine; const GuildOverviewView = module.exports; // match detail view module.exports = class extends View { - async text(members) { + text(members) { // TODO remove when API supports order by fame members.sort((m1, m2) => m1.fame < m2.fame); - return members.map((member) => - `${member.player.name} *${member.status}* ${member.fame}`).join("\n"); + return members.map((m) => new GuildMemberView().text(m)).join("\n"); } async embed(guild) { diff --git a/views/guild_member.js b/views/guild_member.js new file mode 100644 index 0000000..d75ffe4 --- /dev/null +++ b/views/guild_member.js @@ -0,0 +1,35 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const View = require("./view"), + PlayerView = require("./player"), + MatchesView = require("./matches"), + util = require("../util"), + api = require("../api"), + strings = require("../strings"), + oneLine = require("common-tags").oneLine; + +const GuildMemberView = module.exports; + +// combined fame + profile + last match +module.exports = class extends View { + text(member) { + return `${member.player.name} | ${member.status} | ${member.fame} VS Fame`; + } + + async embed(member, player, matches) { + const embed = util.vainsocialEmbed(`${member.player.name} - ${member.player.shard_id}`, + "", "vainsocial-guild-memberview") + .addField("Guild Profile", await this.text(member)) + .addField("Player Profile", await new PlayerView().text(player)) + .addField("Last Match", await new MatchesView().text(matches[0])); + return embed; + } + + async respond(member, player, matches) { + this.response = await util.respond(this.msg, + await this.embed(member, player, matches), this.response); + return this.response; + } +} -- cgit v1.3.1 From 2dc9b39806c7842c31f941e7f7060f5e6b18af1c Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 11 May 2017 21:32:10 +0200 Subject: vgrole: don't set default --- commands/vainsocial/guild_role.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/commands/vainsocial/guild_role.js b/commands/vainsocial/guild_role.js index b37bb79..50b88e4 100644 --- a/commands/vainsocial/guild_role.js +++ b/commands/vainsocial/guild_role.js @@ -23,8 +23,7 @@ module.exports = class RoleGuildMemberCommand extends Commando.Command { label: "name", prompt: "Please specify a Guild member's name.", type: "string", - min: 2, - default: "?" + min: 2 }, { key: "role", label: "role", -- cgit v1.3.1 From 1b2255594e3deedf2c8b7ebfd7e4b8c43f65dcd9 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 16:57:38 +0200 Subject: migrate API changes --- api.js | 3 ++- util.js | 6 +----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/api.js b/api.js index 7f0c35d..7dc7b44 100644 --- a/api.js +++ b/api.js @@ -190,6 +190,7 @@ module.exports.upsearchPlayer = async (name) => { } // block until update is completely done +// TODO implement & use module.exports.upsearchPlayerSync = async (name) => { const waiter = await api.subscribeUpdates(name); await api.upsearchPlayer(name); @@ -204,7 +205,7 @@ module.exports.upsearchPlayerSync = async (name) => { module.exports.getMatches = async (name) => { const data = await api.getFE("/player/" + name + "/matches/1.1.1.1", {}, 60 * 60, "matches+" + name); - return data[0].data; + return data.data; } // return single match diff --git a/util.js b/util.js index dccd874..c33d7b7 100644 --- a/util.js +++ b/util.js @@ -145,9 +145,5 @@ module.exports.paginate = function* chunks(arr, pagesize) { module.exports.ignForUser = async (name, user_token) => { // "?" is not accepted as user input, but the default for empty args if (name != "?") return name; - try { - return await api.getUser(user_token); - } catch (err) { - return undefined; - } + return await api.getUser(user_token); } -- cgit v1.3.1 From 4e4b66faf15c05416b807566218e8e6492fd7909 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 16:57:41 +0200 Subject: vgme: fix mention not mentioning --- views/register.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/register.js b/views/register.js index 1df3bf1..b744bd1 100644 --- a/views/register.js +++ b/views/register.js @@ -19,7 +19,7 @@ module.exports = class extends View { } async text() { - return `You are now registered at VainSocial, @${this.msg.author.tag}.`; + return `You are now registered at VainSocial, ${this.msg.author.toString()}.`; } async help() { return `*${emoji.symbols.repeat} or ${util.usg(this.msg, "v")} to view your profile, ${util.usg(this.msg, "vgcreate")} to create a Guild*` -- cgit v1.3.1 From d6d157a67ea2fee48a41dada6eacb9ab6d3756f9 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 17:03:40 +0200 Subject: guild view: order by fame (finally) --- views/guild.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/views/guild.js b/views/guild.js index 91df6bb..15863b0 100644 --- a/views/guild.js +++ b/views/guild.js @@ -15,9 +15,9 @@ const GuildOverviewView = module.exports; // match detail view module.exports = class extends View { text(members) { - // TODO remove when API supports order by fame - members.sort((m1, m2) => m1.fame < m2.fame); - return members.map((m) => new GuildMemberView().text(m)).join("\n"); + // TODO remove when API supports server side sort + return members.sort((m1, m2) => m1.fame < m2.fame).map((m) => + new GuildMemberView().text(m)).join("\n"); } async embed(guild) { -- cgit v1.3.1 From ae977a00308aa0648008c973e27231dc5e323e10 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 17:10:32 +0200 Subject: fix subscription leaks --- api.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api.js b/api.js index 7dc7b44..f5ae41c 100644 --- a/api.js +++ b/api.js @@ -16,7 +16,7 @@ let cache = cacheManager.caching({ ttl: 10 // s }); -const UPDATE_TIMEOUT = parseInt(process.env.UPDATE_TIMEOUT) || 60; // s +const UPDATE_TIMEOUT = parseInt(process.env.UPDATE_TIMEOUT) || 30; // s const API_FE_URL = process.env.API_FE_URL || "http://vainsocial.bot/bot/api", API_MAP_URL = process.env.API_MAP_URL || "http://vainsocial.dev/masters/", @@ -138,7 +138,10 @@ module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => { subscription = subscribe("player." + name, channel); // stop updates after timeout - setTimeout(() => channel.close(), timeout*1000); + setTimeout(() => { + channel.close(); + subscription.unsubscribe(); + }, timeout*1000); let msg; return { next: async () => { @@ -154,16 +157,19 @@ module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => { } if (msg == "search_fail") { subscription.unsubscribe(); + channel.close(); throw { error: { err: "No player found for the provided IGN." } }; } if (msg == Channel.DONE) { subscription.unsubscribe(); + channel.close(); return undefined; } return msg; - } }; + }, stop: async () => channel.close(); + }; } // search an unknown player -- cgit v1.3.1 From 3e4671a4a387045bbb608ee28dcd6f65392c2d98 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 19:11:49 +0200 Subject: use matches_none, don't dupe unsubscribe --- api.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/api.js b/api.js index f5ae41c..0fe9e78 100644 --- a/api.js +++ b/api.js @@ -136,40 +136,45 @@ module.exports.getGamers = async () => module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => { const channel = new Channel(), subscription = subscribe("player." + name, channel); + let subscribed = true; - // stop updates after timeout - setTimeout(() => { + function stop() { + if (!subscribed) return; + subscribed = false; channel.close(); subscription.unsubscribe(); - }, timeout*1000); + clearTimeout(timer); + } + + // stop updates after timeout + const timer = setTimeout(() => stop(), timeout*1000); - let msg; return { next: async () => { + let msg; do msg = await channel.take(); while([Channel.DONE, "search_fail", "search_success", - "stats_update", "matches_update"].indexOf(msg) == -1); + "stats_update", "matches_update", "matches_none"] + .indexOf(msg) == -1); // bust caches - if (["stats_update"].indexOf(msg) != -1) + if (msg == "stats_update") cache.del("player+" + name); - if (["matches_update"].indexOf(msg) != -1) { + if (msg == "matches_update") { cache.del("matches+" + name); cache.del("player+" + name); } + if (msg == "matches_none") stop(); // no new data if (msg == "search_fail") { - subscription.unsubscribe(); - channel.close(); + stop(); throw { error: { err: "No player found for the provided IGN." } }; } if (msg == Channel.DONE) { subscription.unsubscribe(); - channel.close(); return undefined; } return msg; - }, stop: async () => channel.close(); - }; + }, stop: stop }; } // search an unknown player @@ -196,15 +201,12 @@ module.exports.upsearchPlayer = async (name) => { } // block until update is completely done -// TODO implement & use 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); + while ([undefined, "matches_update", "matches_none"] + .indexOf(await waiter.next()) == -1); + waiter.stop(); } // return matches -- cgit v1.3.1 From 29f937a1d97c60334fa0e707150f73e5c3c773bb Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 19:13:34 +0200 Subject: use upsearch sync --- commands/vainsocial/guild_add.js | 12 +++--------- commands/vainsocial/guild_update.js | 15 +++++---------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/commands/vainsocial/guild_add.js b/commands/vainsocial/guild_add.js index e3a0008..bf8f8f6 100644 --- a/commands/vainsocial/guild_add.js +++ b/commands/vainsocial/guild_add.js @@ -31,18 +31,12 @@ Register IGNs to your Guild. guildAddView = new GuildAddView(msg, playersStatus); // create waiter dict & data dict await Promise.each(playersWaiters, async (waiter, idx) => { - await api.upsearchPlayer(args[idx]); let success = false; try { playersStatus[args[idx]] = "Loading…"; - do { - try { - await api.getPlayer(args[idx]); - success = true; - } catch (err) { } - await guildAddView.respond(); - } while (["stats_update", "matches_update", undefined] - .indexOf(await waiter.next())); + await guildAddView.respond(); + await api.upsearchPlayerSync(args[idx]); + success = true; } catch (err) { console.error(err); playersStatus[args[idx]] = err.error.err; diff --git a/commands/vainsocial/guild_update.js b/commands/vainsocial/guild_update.js index c3163be..a61647f 100644 --- a/commands/vainsocial/guild_update.js +++ b/commands/vainsocial/guild_update.js @@ -42,16 +42,11 @@ Update the match history for all your Guild members. const playersWaiters = names.map((name) => api.subscribeUpdates(name)); // create waiter dict & data dict await Promise.each(playersWaiters, async (waiter, idx) => { - await api.updatePlayer(names[idx]); - while (["stats_update", undefined].indexOf(await waiter.next())) { - try { - await api.getPlayer(names[idx]); - playersStatus[names[idx]] = "Loaded."; - } catch (err) { - playersStatus[names[idx]] = "Loading…"; - } - await guildUpdateView.respond(); - } + playersStatus[names[idx]] = "Loading…"; + await guildUpdateView.respond(); + await api.upsearchPlayerSync(names[idx]); + playersStatus[names[idx]] = "Loaded."; + await guildUpdateView.respond(); }); await guildUpdateView.respond("Your Guild's fame is being updated…"); try { -- cgit v1.3.1 From 54565aeccffb0722de554820eb856b23ac3c004f Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 20:13:50 +0200 Subject: fix update subscribe inf loop --- api.js | 1 + commands/vainsocial/me.js | 4 +--- views/register.js | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api.js b/api.js index 0fe9e78..66d52da 100644 --- a/api.js +++ b/api.js @@ -170,6 +170,7 @@ module.exports.subscribeUpdates = (name, timeout=UPDATE_TIMEOUT) => { } }; } if (msg == Channel.DONE) { + subscribed = false; subscription.unsubscribe(); return undefined; } diff --git a/commands/vainsocial/me.js b/commands/vainsocial/me.js index de87b4c..8679a65 100644 --- a/commands/vainsocial/me.js +++ b/commands/vainsocial/me.js @@ -35,10 +35,8 @@ Store your in game name for quicker access to other commands and for Guild manag async run(msg, args) { util.trackAction(msg, "vainsocial-me", args.name); const registerView = new RegisterView(msg, args.name); - await api.upsearchPlayer(args.name); try { - const waiter = api.subscribeUpdates(args.name); - while (await waiter.next() != "stats_update"); + await api.upsearchPlayerSync(args.name); await api.setUser(msg.author.id, args.name); } catch (err) { console.log(err); diff --git a/views/register.js b/views/register.js index b744bd1..a636e84 100644 --- a/views/register.js +++ b/views/register.js @@ -4,6 +4,7 @@ const emoji = require("discord-emoji"), View = require("./view"), + PlayerView = require("./player"), util = require("../util"), api = require("../api"), strings = require("../strings"), -- cgit v1.3.1 From 90f291211703b4d6823c55ca84ba3b1d4127b027 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 12 May 2017 20:14:06 +0200 Subject: vgpin: allow to pin a guild to a server --- api.js | 6 +++++ commands/vainsocial/guild_member.js | 33 +++++++++++++++++++++++--- commands/vainsocial/guild_pin.js | 46 +++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 commands/vainsocial/guild_pin.js diff --git a/api.js b/api.js index 66d52da..12f58c3 100644 --- a/api.js +++ b/api.js @@ -226,6 +226,12 @@ module.exports.getMatch = async (id) => { module.exports.getGuild = async (token) => { return await api.getFE("/guild", { user_token: token }, 60, "guild+" + token); } + +// return a guild +module.exports.getGuildMembersByGuildName = async (name) => { + // TODO caching + return await api.getFE("/guild/" + name + "/members", { }, 0); +} // TODO! cache guilds by guild id, not by user token // add user to guild diff --git a/commands/vainsocial/guild_member.js b/commands/vainsocial/guild_member.js index 2a1bf27..e72fff1 100644 --- a/commands/vainsocial/guild_member.js +++ b/commands/vainsocial/guild_member.js @@ -25,17 +25,44 @@ module.exports = class ViewGuildMemberCommand extends Commando.Command { type: "string", min: 2, default: "?" + }, { + key: "guild", + label: "guild", + prompt: "Please specify a Guild's name.", + type: "string", + min: 2, + default: "?" } ] }); } async run(msg, args) { util.trackAction(msg, "vainsocial-guild-member"); const guildMemberView = new GuildMemberView(msg); + // get IGN or default + let ign; try { - const guild = await api.getGuild(msg.author.id); - const member = guild.members.filter((m) => m.player.name == args.name)[0]; + ign = await util.ignForUser(args.name, msg.author.id); + } catch (err) { + return await guildMemberView.error(strings.unknown(msg)); + } + // get guild: name, server default, self + let guildName = args.guild, guild; + if (guildName == "?") { + guildName = msg.guild.settings.get("default-guild-name"); + } + + try { + let members; + // TODO. + if (guildName == undefined) members = (await api.getGuild(msg.author.id)).members; + else members = await api.getGuildMembersByGuildName(guildName); + console.log(guildName); + if (members.length == 0) + throw { error: { err: "Could not find that Guild." } }; + + const member = members.filter((m) => m.player.name == args.name)[0]; if (member == undefined) - throw { err: { error: "Player is not in the Guild." } }; + throw { error: { err: "Player is not in the Guild." } }; const player = await api.getPlayer(member.player.name); const matches = await api.getMatches(player.name); await guildMemberView.respond(member, player, matches); diff --git a/commands/vainsocial/guild_pin.js b/commands/vainsocial/guild_pin.js new file mode 100644 index 0000000..986666e --- /dev/null +++ b/commands/vainsocial/guild_pin.js @@ -0,0 +1,46 @@ +#!/usr/bin/node +/* jshint esnext:true */ +"use strict"; + +const Commando = require("discord.js-commando"), + oneLine = require("common-tags").oneLine, + util = require("../../util"), + api = require("../../api"), + SimpleView = require("../../views/simple"); + +module.exports = class PinGuildCommand extends Commando.Command { + constructor(client) { + super(client, { + name: "vainsocial-guildpin", + aliases: ["vguild-pin", "vgpin"], + group: "vainsocial-guild", + memberName: "vainsocial-guildpin", + description: "Share your Guild on a server.", + examples: ["vgview"], + + args: [ { + key: "name", + label: "name", + prompt: "Please specify your Guild's name.", + type: "string", + min: 2, + default: "?" + } ] + }); + } + async run(msg, args) { + util.trackAction(msg, "vainsocial-guild-pin"); + const pinView = new SimpleView(msg); + if (!msg.member.hasPermission('ADMINISTRATOR')) + return await pinView.error("You need to be server administrator to pin a GUild."); + let guild; + try { + guild = await api.getGuild(msg.author.id); + } catch (err) { + console.log(err); + return await guildOverviewView.error(err.error.err); + } + msg.guild.settings.set("default-guild-name", guild.name); + await pinView.respond(`Successfully pinned the Guild \`${guild.name}\` to your server.`); + } +}; -- cgit v1.3.1 From f900f7955f60c1d46be359e378c0d15bd8af8c09 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 20 May 2017 10:46:27 +0200 Subject: disable registration, end of beta --- commands/vainsocial/guild_create.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commands/vainsocial/guild_create.js b/commands/vainsocial/guild_create.js index e742609..a8caa6a 100644 --- a/commands/vainsocial/guild_create.js +++ b/commands/vainsocial/guild_create.js @@ -49,6 +49,11 @@ Create a Guild with your VainSocial profile as leader. async run(msg, args) { util.trackAction(msg, "vainsocial-guild-create"); const guildCreateView = new GuildCreateView(msg, msg.author.id); + if (msg.guild.id != 283790513998659585) + return await guildCreateView.error(oneLine` +Guilds creation is currently running in closed beta. +If you want to participate, join our development Discord: https://discord.gg/txTchJY +`); try { await api.post("/guild", { shard_id: args.region, -- cgit v1.3.1 From 3e69301984ac134a6a6dd8ec4f3c3de58e027293 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 20 May 2017 10:54:46 +0200 Subject: about needs rooturl --- commands/vainsocial/about.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands/vainsocial/about.js b/commands/vainsocial/about.js index c452d87..4c6086a 100644 --- a/commands/vainsocial/about.js +++ b/commands/vainsocial/about.js @@ -6,6 +6,8 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, util = require("../../util"); +const ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/"); + module.exports = class ShowAboutCommand extends Commando.Command { constructor(client) { super(client, { -- cgit v1.3.1 From 1881a2ecd48be772fd83b20068fe9946f1365eeb Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 20 May 2017 10:55:37 +0200 Subject: rooturl needs preview env var --- commands/vainsocial/about.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands/vainsocial/about.js b/commands/vainsocial/about.js index 4c6086a..e7941e3 100644 --- a/commands/vainsocial/about.js +++ b/commands/vainsocial/about.js @@ -6,7 +6,8 @@ const Commando = require("discord.js-commando"), oneLine = require("common-tags").oneLine, util = require("../../util"); -const ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/"); +const PREVIEW = process.env.PREVIEW != "false", + ROOTURL = (PREVIEW? "https://preview.vainsocial.com/":"https://vainsocial.com/"); module.exports = class ShowAboutCommand extends Commando.Command { constructor(client) { -- cgit v1.3.1