import os import discord from discord.ext import commands from discord.utils import find from __main__ import send_cmd_help import random, time, datetime import aiohttp import asyncio import re, operator import urllib.request try: from bs4 import BeautifulSoup except: raise RuntimeError("bs4 required: pip install beautifulsoup4") from .utils.dataIO import fileIO from cogs.utils import checks import logging prefix = fileIO("data/red/settings.json", "load")['PREFIXES'][0] help_msg = [ "**No linked account (`{}osuset user [username]`) or not using **`{}command [username] [gamemode]`".format(prefix, prefix), "**No linked account (`{}osuset user [username]`)**".format(prefix) ] modes = ["osu", "taiko", "ctb", "mania"] log = logging.getLogger("red.osu") log.setLevel(logging.INFO) class Osu: """Cog to give osu! stats for all gamemodes.""" def __init__(self, bot): self.bot = bot self.osu_api_key = fileIO("data/osu/apikey.json", "load") self.user_settings = fileIO("data/osu/user_settings.json", "load") self.track = fileIO("data/osu/track.json", "load") self.osu_settings = fileIO("data/osu/osu_settings.json", "load") self.num_max_prof = 8 self.max_map_disp = 3 # ---------------------------- Settings ------------------------------------ @commands.group(pass_context=True) async def osuset(self, ctx): """Where you can define some settings""" if ctx.invoked_subcommand is None: await send_cmd_help(ctx) return @osuset.command(pass_context=True, no_pm=True) @checks.is_owner() async def tracktop(self, ctx, top_num:int): """ Set # of top plays being tracked """ msg = "" if top_num < 1 or top_num > 100: msg = "**Please enter a valid number. (1 - 100)**" else: self.osu_settings["num_track"] = top_num msg = "**Now tracking Top {} Plays.**".format(top_num) fileIO("data/osu/osu_settings.json", "save", self.osu_settings) await self.bot.say(msg) @osuset.command(pass_context=True, no_pm=True) @checks.is_owner() async def displaytop(self, ctx, top_num:int): """ Set # of best plays being displayed in top command """ msg = "" if top_num < 1 or top_num > 10: msg = "**Please enter a valid number. (1 - 10)**" else: self.osu_settings["num_best_plays"] = top_num msg = "**Now Displaying Top {} Plays.**".format(top_num) fileIO("data/osu/osu_settings.json", "save", self.osu_settings) await self.bot.say(msg) @osuset.command(pass_context=True, no_pm=True) @checks.serverowner_or_permissions(administrator=True) async def tracking(self, ctx, toggle=None): """ For disabling tracking on server (enable/disable) """ server = ctx.message.server if server.id not in self.osu_settings: self.osu_settings[server.id] = {} self.osu_settings[server.id]["tracking"] = True status = "" if not toggle: self.osu_settings[server.id]["tracking"] = not self.osu_settings[server.id]["tracking"] if self.osu_settings[server.id]["tracking"]: status = "Enabled" else: status = "Disabled" elif toggle.lower() == "enable": self.osu_settings[server.id]["tracking"] = True status = "Enabled" elif toggle.lower() == "disable": self.osu_settings[server.id]["tracking"] = False status = "Disabled" fileIO("data/osu/osu_settings.json", "save", self.osu_settings) await self.bot.say("**Player Tracking {} on {}.**".format(server.name, status)) @osuset.command(pass_context=True, no_pm=True) @checks.mod_or_permissions(manage_messages=True) async def overview(self, ctx): """ Get an overview of your settings """ server = ctx.message.server user = ctx.message.author em = discord.Embed(description='', colour=user.colour) em.set_author(name="Current Settings for {}".format(server.name), icon_url = server.icon_url) # determine api to use if server.id in self.osu_settings and "api" in self.osu_settings[server.id]: if self.osu_settings[server.id]["api"] == self.osu_settings["type"]["default"]: api = "Official Osu! API" elif self.osu_settings[server.id]["api"] == self.osu_settings["type"]["ripple"]: api = "Ripple API" else: api = "Official Osu! API" # determine if server.id not in self.osu_settings or "tracking" not in self.osu_settings[server.id] or self.osu_settings[server.id]["tracking"] == True: tracking = "Enabled" else: tracking = "Disabled" info = "" info += "**▸ Default API:** {}\n".format(api) info += "**▸ Tracking:** {}\n".format(tracking) if tracking == "Enabled": info += "**▸ Tracking Number:** {}\n".format(self.osu_settings['num_track']) info += "**▸ Top Plays:** {}".format(self.osu_settings['num_best_plays']) em.description = info await self.bot.say(embed = em) @osuset.command(pass_context=True, no_pm=True) @checks.is_owner() async def api(self, ctx, *, choice): """'official' or 'ripple'""" server = ctx.message.server if server.id not in self.osu_settings: self.osu_settings[server.id] = {} if not choice.lower() == "official" and not choice.lower() == "ripple": await self.bot.say("The two choices are `official` and `ripple`") return elif choice.lower() == "official": self.osu_settings[server.id]["api"] = self.osu_settings["type"]["default"] elif choice.lower() == "ripple": self.osu_settings[server.id]["api"] = self.osu_settings["type"]["ripple"] fileIO("data/osu/osu_settings.json", "save", self.osu_settings) await self.bot.say("**Switched to `{}` server as default on `{}`.** :arrows_counterclockwise:".format(choice, server.name)) @osuset.command(pass_context=True, no_pm=True) async def default(self, ctx, mode:str): """ Set your default gamemode """ user = ctx.message.author server = ctx.message.server if mode.lower() in modes: gamemode = modes.index(mode.lower()) elif int(mode) >= 0 and int(mode) <= 3: gamemode = int(mode) else: await self.bot.say("**Please enter a valid gamemode.**") return if user.id in self.user_settings: self.user_settings[user.id]['default_gamemode'] = int(gamemode) await self.bot.say("**`{}`'s default gamemode has been set to `{}`.** :white_check_mark:".format(user.name, modes[gamemode])) fileIO('data/osu/user_settings.json', "save", self.user_settings) else: await self.bot.say(help_msg[1]) @commands.group(pass_context=True) async def osutrack(self, ctx): """Where you can define some settings""" if ctx.invoked_subcommand is None: await send_cmd_help(ctx) return @osuset.command(pass_context=True) @checks.is_owner() async def key(self, ctx): """Sets your osu api key""" await self.bot.whisper("Type your osu! api key. You can reply here.") key = await self.bot.wait_for_message(timeout=30, author=ctx.message.author) if key is None: return else: self.osu_api_key["osu_api_key"] = key.content fileIO("data/osu/apikey.json", "save", self.osu_api_key) await self.bot.whisper("API Key details added. :white_check_mark:") @commands.command(pass_context=True, no_pm=True) async def osu(self, ctx, *username): """Gives osu user(s) stats. Use -ripple/-official to use specific api.""" await self._process_user_info(ctx, username, 0) @commands.command(pass_context=True, no_pm=True) async def osutop(self, ctx, *username): """Gives top osu plays. Use -ripple/-official to use specific api.""" await self._process_user_top(ctx, username, 0) @commands.command(pass_context=True, no_pm=True) async def taiko(self, ctx, *username): """Gives taiko user(s) stats. Use -ripple/-official to use specific api.""" await self._process_user_info(ctx, username, 1) @commands.command(pass_context=True, no_pm=True) async def taikotop(self, ctx, *username): """Gives top taiko plays. Use -ripple/-official to use specific api.""" await self._process_user_top(ctx, username, 1) @commands.command(pass_context=True, no_pm=True) async def ctb(self, ctx, *username): """Gives ctb user(s) stats. Use -ripple/-official to use specific api.""" await self._process_user_info(ctx, username, 2) @commands.command(pass_context=True, no_pm=True) async def ctbtop(self, ctx, *username): """Gives ctb osu plays. Use -ripple/-official to use specific api.""" await self._process_user_top(ctx, username, 2) @commands.command(pass_context=True, no_pm=True) async def mania(self, ctx, *username): """Gives mania user(s) stats. Use -ripple/-official to use specific api.""" await self._process_user_info(ctx, username, 3) @commands.command(pass_context=True, no_pm=True) async def maniatop(self, ctx, *username): """Gives top mania plays. Use -ripple/-official to use specific api.""" await self._process_user_top(ctx, username, 3) @commands.command(pass_context=True, no_pm=True) async def recent(self, ctx, *username): """Gives recent plays of player with respect to user's default gamemode. [p]recent [username] (gamemode:optional)""" await self._process_user_recent(ctx, username) @osuset.command(pass_context=True, no_pm=True) async def user(self, ctx, *, username): """Sets user information given an osu! username""" user = ctx.message.author channel = ctx.message.channel server = user.server key = self.osu_api_key["osu_api_key"] if user.server.id not in self.user_settings: self.user_settings[user.server.id] = {} if not self._check_user_exists(user): try: osu_user = list(await get_user(key, self.osu_settings["type"]["default"], username, 1)) newuser = { "discord_username": user.name, "osu_username": username, "osu_user_id": osu_user[0]["user_id"], "default_gamemode": 0, "ripple_username": "" } self.user_settings[user.id] = newuser fileIO('data/osu/user_settings.json', "save", self.user_settings) await self.bot.say("{}, your account has been linked to osu! username `{}`".format(user.mention, osu_user[0]["username"])) except: await self.bot.say("{} doesn't exist in the osu! database.".format(username)) else: try: osu_user = list(await get_user(key, self.osu_settings["type"]["default"], username, 1)) self.user_settings[user.id]["osu_username"] = username self.user_settings[user.id]["osu_user_id"] = osu_user[0]["user_id"] fileIO('data/osu/user_settings.json', "save", self.user_settings) await self.bot.say("{}, your osu! username has been edited to `{}`".format(user.mention, osu_user[0]["username"])) except: await self.bot.say("{} doesn't exist in the osu! database.".format(username)) # Gets json information to proccess the small version of the image async def _process_user_info(self, ctx, usernames, gamemode:int): key = self.osu_api_key["osu_api_key"] channel = ctx.message.channel user = ctx.message.author server = user.server if not usernames: usernames = [None] # get rid of duplicates usernames = list(set(usernames)) # determine api to use usernames, api = self._determine_api(server, usernames) # gives the final input for osu username final_usernames = [] for username in usernames: test_username = await self._process_username(ctx, username) if test_username != None: final_usernames.append(test_username) # testing if username is osu username all_user_info = [] sequence = [] count_valid = 0 for i in range(len(final_usernames)): userinfo = list(await get_user(key, api, final_usernames[i], gamemode)) # get user info from osu api if userinfo != None and len(userinfo) > 0 and userinfo[0]['pp_raw'] != None: all_user_info.append(userinfo[0]) sequence.append((count_valid, int(userinfo[0]["pp_rank"]))) count_valid = count_valid + 1 else: await self.bot.say("**`{}` has not played enough.**".format(final_usernames[i])) sequence = sorted(sequence, key=operator.itemgetter(1)) all_players = [] for i, pp in sequence: all_players.append(await self._get_user_info(api, server, user, all_user_info[i], gamemode)) disp_num = min(self.num_max_prof, len(all_players)) if disp_num < len(all_players): await self.bot.say("Found {} users, but displaying top {}.".format(len(all_players), disp_num)) for player in all_players[0:disp_num]: await self.bot.say(embed=player) # takes iterable of inputs and determines api, also based on defaults def _determine_api(self, server, inputs): if not inputs or ('-ripple' not in inputs and '-official' not in inputs): # in case not specified if server.id in self.osu_settings and "api" in self.osu_settings[server.id]: if self.osu_settings[server.id]["api"] == self.osu_settings["type"]["default"]: api = self.osu_settings["type"]["default"] elif self.osu_settings[server.id]["api"] == self.osu_settings["type"]["ripple"]: api = self.osu_settings["type"]["ripple"] else: api = self.osu_settings["type"]["default"] elif '-ripple' in inputs: inputs = list(inputs) inputs.remove('-ripple') api = self.osu_settings["type"]["ripple"] elif '-official' in inputs: inputs = list(inputs) inputs.remove('-official') api = self.osu_settings["type"]["default"] if not inputs: inputs = [None] return inputs, api # Gets the user's most recent score async def _process_user_recent(self, ctx, inputs): key = self.osu_api_key["osu_api_key"] channel = ctx.message.channel user = ctx.message.author server = user.server # forced handle gamemode gamemode = -1 inputs = list(inputs) for mode in modes: if len(inputs) >= 2 and mode in inputs: gamemode = self._get_gamemode_number(mode) inputs.remove(mode) elif len(inputs) == 1 and mode == inputs[0]: gamemode = self._get_gamemode_number(mode) inputs.remove(mode) inputs = tuple(inputs) # handle api and username (1) username, api = self._determine_api(server, list(inputs)) username = username[0] # gives the final input for osu username test_username = await self._process_username(ctx, username) if test_username: username = test_username else: return # determines which recent gamemode to display based on user if gamemode == -1: target_id = self._get_discord_id(username, api) if target_id != -1: gamemode = self.user_settings[target_id]['default_gamemode'] elif target_id == -1 and self._check_user_exists(user): gamemode = self.user_settings[user.id]['default_gamemode'] else: gamemode = 0 # get userinfo userinfo = list(await get_user(key, api, username, gamemode)) userrecent = list(await get_user_recent(key, api, username, gamemode)) if not userinfo or not userrecent: await self.bot.say("**`{}` was not found or no recent plays in `{}`.**".format(username, self._get_gamemode(gamemode))) return else: userinfo = userinfo[0] userrecent = userrecent[0] msg, recent_play = await self._get_recent(ctx, api, userinfo, userrecent, gamemode) await self.bot.say(msg, embed=recent_play) def _get_discord_id(self, username:str, api:str): #if api == self.osu_settings["type"]["ripple"]: #name_type = "ripple_username" #else: #name_type = "osu_username" # currently assumes same name name_type = "osu_username" for user_id in self.user_settings.keys(): if self.user_settings[user_id] and username in self.user_settings[user_id][name_type]: return user_id return -1 # Gets information to proccess the top play version of the image async def _process_user_top(self, ctx, username, gamemode: int): key = self.osu_api_key["osu_api_key"] channel = ctx.message.channel user = ctx.message.author server = user.server # determine api to use username, api = self._determine_api(server, list(username)) username = username[0] # gives the final input for osu username test_username = await self._process_username(ctx, username) if test_username: username = test_username else: return # get userinfo userinfo = list(await get_user(key, api, username, gamemode)) userbest = list(await get_user_best(key, api, username, gamemode, self.osu_settings['num_best_plays'])) if userinfo and userbest: msg, top_plays = await self._get_user_top(ctx, api, userinfo[0], userbest, gamemode) await self.bot.say(msg, embed=top_plays) else: await self.bot.say("**`{}` was not found or not enough plays.**".format(username)) ## processes username. probably the worst chunck of code in this project so far. will fix/clean later async def _process_username(self, ctx, username): channel = ctx.message.channel user = ctx.message.author server = user.server key = self.osu_api_key["osu_api_key"] # if nothing is given, must rely on if there's account if not username: if self._check_user_exists(user): username = self.user_settings[user.id]["osu_username"] else: await self.bot.say("It doesn't seem that you have an account linked. Do **{}osuset user [username]**.".format(prefix)) return None # bad practice, but too lazy to make it nice # if it's a discord user, first check to see if they are in database and choose that username # then see if the discord username is a osu username, then try the string itself elif find(lambda m: m.name == username, channel.server.members) is not None: target = find(lambda m: m.name == username, channel.server.members) try: self._check_user_exists(target) username = self.user_settings[target.id]["osu_username"] except: if await get_user(key, self.osu_settings["type"]["default"], username, 0): username = str(target) else: await self.bot.say(help_msg[1]) return # @ implies its a discord user (if not, it will just say user not found in the next section) # if not found, then oh well. elif "@" in username: user_id = re.findall("\d+", username) user_id = user_id[0] if user_id in self.user_settings: username = self.user_settings[user_id]["osu_username"] else: await self.bot.say(help_msg[1]) return else: username = str(username) return username # Checks if user exists def _check_user_exists(self, user): if user.id not in self.user_settings: return False return True def _get_api_name(self, url:str): if url == self.osu_settings["type"]["ripple"]: return "Ripple" else: return "Official" # Gives a small user profile async def _get_user_info(self, api:str, server, server_user, user, gamemode: int): if api == self.osu_settings["type"]["default"]: profile_url ='http://s.ppy.sh/a/{}.png'.format(user['user_id']) pp_country_rank = " ({}#{})".format(user['country'], user['pp_country_rank']) elif api == self.osu_settings["type"]["ripple"]: profile_url = 'http://a.ripple.moe/{}.png'.format(user['user_id']) pp_country_rank = "" flag_url = 'https://new.ppy.sh//images/flags/{}.png'.format(user['country']) gamemode_text = self._get_gamemode(gamemode) try: user_url = 'https://{}/u/{}'.format(api, user['user_id']) em = discord.Embed(description='', colour=server_user.colour) em.set_author(name="{} Profile for {}".format(gamemode_text, user['username']), icon_url = flag_url, url = user_url) em.set_thumbnail(url=profile_url) level_int = int(float(user['level'])) level_percent = float(user['level']) - level_int info = "" info += "**▸ {} Rank:** #{} {}\n".format(self._get_api_name(api), user['pp_rank'], pp_country_rank) info += "**▸ Level:** {} ({:.2f}%)\n".format(level_int, level_percent*100) info += "**▸ Total PP:** {}\n".format(user['pp_raw']) info += "**▸ Playcount:** {}\n".format(user['playcount']) info += "**▸ Hit Accuracy:** {}%".format(user['accuracy'][0:5]) em.description = info if api == self.osu_settings["type"]["default"]: soup = BeautifulSoup(urllib.request.urlopen("https://osu.ppy.sh/u/{}".format(user['user_id'])), "html.parser") timestamps = [] for tag in soup.findAll(attrs={'class': 'timeago'}): timestamps.append(datetime.datetime.strptime(tag.contents[0].strip().replace(" UTC", ""), '%Y-%m-%d %H:%M:%S')) timeago = datetime.datetime(1,1,1) + (datetime.datetime.utcnow() - timestamps[1]) time_ago = "Last Online " if timeago.year-1 != 0: time_ago += "{} Years ".format(timeago.year-1) if timeago.month-1 !=0: time_ago += "{} Months ".format(timeago.month-1) if timeago.day-1 !=0: time_ago += "{} Days ".format(timeago.day-1) if timeago.hour != 0: time_ago += "{} Hours ".format(timeago.hour) if timeago.minute != 0: time_ago += "{} Minutes ".format(timeago.minute) time_ago += "{} Seconds ago".format(timeago.second) em.set_footer(text=time_ago) return em except: return None async def _get_recent(self, ctx, api, user, userrecent, gamemode:int): server_user = ctx.message.author server = ctx.message.server key = self.osu_api_key["osu_api_key"] if api == self.osu_settings["type"]["default"]: profile_url = 'http://s.ppy.sh/a/{}.png'.format(user['user_id']) elif api == self.osu_settings["type"]["ripple"]: profile_url = 'http://a.ripple.moe/{}.png'.format(user['user_id']) flag_url = 'https://new.ppy.sh//images/flags/{}.png'.format(user['country']) # get best plays map information and scores beatmap = list(await get_beatmap(key, api, beatmap_id=userrecent['beatmap_id']))[0] if not userrecent: return ("**No recent score for `{}` in user's default gamemode (`{}`)**".format(user['username'], self._get_gamemode(gamemode)), None) acc = self.calculate_acc(userrecent, gamemode) mods = self.mod_calculation(userrecent['enabled_mods']) if not mods: mods = [] mods.append('No Mod') beatmap_url = 'https://osu.ppy.sh/b/{}'.format(beatmap['beatmap_id']) msg = "**Most Recent {} Play for {}:**".format(self._get_gamemode(gamemode), user['username']) info = "" info += "▸ **Rank:** {} ▸ **Combo:** x{}\n".format(userrecent['rank'], userrecent['maxcombo']) info += "▸ **Score:** {} ▸ **Misses:** {}\n".format(userrecent['score'], userrecent['countmiss']) info += "▸ **Acc:** {:.2f}% ▸ **Stars:** {:.2f}★\n".format(float(acc), float(beatmap['difficultyrating'])) # grab beatmap image page = urllib.request.urlopen(beatmap_url) soup = BeautifulSoup(page.read(), "html.parser") map_image = [x['src'] for x in soup.findAll('img', {'class': 'bmt'})] map_image_url = 'http:{}'.format(map_image[0]).replace(" ","%") em = discord.Embed(description=info, colour=server_user.colour) em.set_author(name="{} [{}] +{}".format(beatmap['title'], beatmap['version'], ",".join(mods)), url = beatmap_url, icon_url = profile_url) em.set_thumbnail(url=map_image_url) em.set_footer(text = "{} On Osu! {} Server".format(userrecent['date'], self._get_api_name(api))) return (msg, em) # Gives a user profile image with some information async def _get_user_top(self, ctx, api, user, userbest, gamemode:int): server_user = ctx.message.author server = ctx.message.server key = self.osu_api_key["osu_api_key"] if api == self.osu_settings["type"]["default"]: profile_url = 'http://s.ppy.sh/a/{}.png'.format(user['user_id']) elif api == self.osu_settings["type"]["ripple"]: profile_url = 'http://a.ripple.moe/{}.png'.format(user['user_id']) gamemode_text = self._get_gamemode(gamemode) # get best plays map information and scores best_beatmaps = [] best_acc = [] for i in range(self.osu_settings['num_best_plays']): beatmap = list(await get_beatmap(key, api, beatmap_id=userbest[i]['beatmap_id']))[0] score = list(await get_scores(key, api, userbest[i]['beatmap_id'], user['user_id'], gamemode))[0] best_beatmaps.append(beatmap) best_acc.append(self.calculate_acc(score,gamemode)) all_plays = [] msg = "**Top {} {} Plays for {}:**".format(self.osu_settings['num_best_plays'], gamemode_text, user['username']) desc = '' for i in range(self.osu_settings['num_best_plays']): mods = self.mod_calculation(userbest[i]['enabled_mods']) if not mods: mods = [] mods.append('No Mod') beatmap_url = 'https://osu.ppy.sh/b/{}'.format(best_beatmaps[i]['beatmap_id']) info = '' info += '***{}. [__{} [{}]__]({}) +{}\n***'.format(i+1, best_beatmaps[i]['title'], best_beatmaps[i]['version'], beatmap_url, ','.join(mods)) info += '▸ **Rank:** {} ▸ **PP:** {:.2f}\n'.format(userbest[i]['rank'], float(userbest[i]['pp'])) info += '▸ **Score:** {} ▸ **Combo:** x{}\n'.format(userbest[i]['score'], userbest[i]['maxcombo']) info += '▸ **Acc:** {:.2f}% ▸ **Stars:** {:.2f}★\n\n'.format(float(best_acc[i]), float(best_beatmaps[i]['difficultyrating'])) desc += info em = discord.Embed(description=desc, colour=server_user.colour) em.set_footer(text = "On Osu! {} Server".format(self._get_api_name(api))) em.set_thumbnail(url=profile_url) return (msg, em) def _get_gamemode(self, gamemode:int): if gamemode == 1: gamemode_text = "Taiko" elif gamemode == 2: gamemode_text = "Catch the Beat!" elif gamemode == 3: gamemode_text = "Osu! Mania" else: gamemode_text = "Osu! Standard" return gamemode_text def _get_gamemode_display(self, gamemode): if gamemode == "osu": gamemode_text = "Osu! Standard" elif gamemode == "ctb": gamemode_text = "Catch the Beat!" elif gamemode == "mania": gamemode_text = "Osu! Mania" elif gamemode == "taiko": gamemode_text = "Taiko" return gamemode_text def _get_gamemode_number(self, gamemode:str): if gamemode == "taiko": gamemode_text = 1 elif gamemode == "ctb": gamemode_text = 2 elif gamemode == "mania": gamemode_text = 3 else: gamemode_text = 0 return int(gamemode_text) def calculate_acc(self, beatmap, gamemode:int): if gamemode == 0: total_unscale_score = float(beatmap['count300']) total_unscale_score += float(beatmap['count100']) total_unscale_score += float(beatmap['count50']) total_unscale_score += float(beatmap['countmiss']) total_unscale_score *=300 user_score = float(beatmap['count300']) * 300.0 user_score += float(beatmap['count100']) * 100.0 user_score += float(beatmap['count50']) * 50.0 elif gamemode == 1: total_unscale_score = float(beatmap['count300']) total_unscale_score += float(beatmap['count100']) total_unscale_score += float(beatmap['countmiss']) total_unscale_score *= 300 user_score = float(beatmap['count300']) * 1.0 user_score += float(beatmap['count100']) * 0.5 user_score *= 300 elif gamemode == 2: total_unscale_score = float(beatmap['count300']) total_unscale_score += float(beatmap['count100']) total_unscale_score += float(beatmap['count50']) total_unscale_score += float(beatmap['countmiss']) total_unscale_score += float(beatmap['countkatu']) user_score = float(beatmap['count300']) user_score += float(beatmap['count100']) user_score += float(beatmap['count50']) elif gamemode == 3: total_unscale_score = float(beatmap['count300']) total_unscale_score += float(beatmap['countgeki']) total_unscale_score += float(beatmap['countkatu']) total_unscale_score += float(beatmap['count100']) total_unscale_score += float(beatmap['count50']) total_unscale_score += float(beatmap['countmiss']) total_unscale_score *=300 user_score = float(beatmap['count300']) * 300.0 user_score += float(beatmap['countgeki']) * 300.0 user_score += float(beatmap['countkatu']) * 200.0 user_score += float(beatmap['count100']) * 100.0 user_score += float(beatmap['count50']) * 50.0 return (float(user_score)/float(total_unscale_score)) * 100.0 # Truncates the text because some titles/versions are too long def truncate_text(self, text): if len(text) > 20: text = text[0:20] + '...' return text # gives a list of the ranked mods given a peppy number lol def mod_calculation(self, number): number = int(number) mod_list = [] mods = ['PF', 'SO', 'FL', 'NC', 'HT', 'RX', 'DT', 'SD', 'HR', 'HD', 'EZ', 'NF'] peppyNumbers = [16384, 4096, 1024, 576, 256, 128, 64, 32, 16, 8, 2, 1] for i in range(len(mods)): if number >= peppyNumbers[i]: number-= peppyNumbers[i] mod_list.append(mods[i]) return mod_list # ---------------------------- Detect Links ------------------------------ # called by listener async def find_link(self, message): if message.author.id == self.bot.user.id: return if "https://" in message.content: # process the the idea from a url in msg all_urls = [] original_message = message.content get_urls = re.findall("(https:\/\/[^\s]+)([ ]\+[A-Za-z][^\s]+)?", original_message) for url in get_urls: all_urls.append(url[0]) # get rid of duplicates all_urls = list(set(all_urls)) if 'https://osu.ppy.sh/u/' in original_message: await self.process_user_url(all_urls, message) if 'https://osu.ppy.sh/s/' in original_message or 'https://osu.ppy.sh/b/' in original_message: await self.process_beatmap(all_urls, message) # processes user input for user profile link async def process_user_url(self, all_urls, message): key = self.osu_api_key["osu_api_key"] server_user = message.author server = message.author.server for url in all_urls: try: if url.find('https://osu.ppy.sh/u/') != -1: user_id = url.replace('https://osu.ppy.sh/u/','') user_info = await get_user(key, self.osu_settings["type"]["default"], user_id, 0) if user_id in self.user_settings: gamemode = self.user_settings[user_id]["default_gamemode"] else: gamemode = 0 em = await self._get_user_info(self.osu_settings["type"]["default"], server, server_user, user_info[0], gamemode) await self.bot.send_message(message.channel, embed = em) except: await self.bot.send_message(message.channel, "That user doesn't exist.") # processes user input for the beatmap async def process_beatmap(self, all_urls, message): key = self.osu_api_key["osu_api_key"] for url in all_urls: try: if url.find('https://osu.ppy.sh/s/') != -1: beatmap_id = url.replace('https://osu.ppy.sh/s/','') beatmap_info = await get_beatmapset(key, self.osu_settings["type"]["default"], beatmap_id) await self.disp_beatmap(message, beatmap_info, url) elif url.find('https://osu.ppy.sh/b/') != -1: beatmap_id = url.replace('https://osu.ppy.sh/b/','') beatmap_info = await get_beatmap(key, self.osu_settings["type"]["default"], beatmap_id) await self.disp_beatmap(message, beatmap_info, url) except: await self.bot.send_message(message.channel, "That beatmap doesn't exist.") # displays the beatmap properly async def disp_beatmap(self, message, beatmap, beatmap_url:str): # process time num_disp = min(len(beatmap), self.max_map_disp) if (len(beatmap)>self.max_map_disp): msg = "Found {} maps, but only displaying {}.\n".format(len(beatmap), self.max_map_disp) else: msg = "Found {} map(s).\n".format(len(beatmap)) beatmap_msg = "" m, s = divmod(int(beatmap[0]['total_length']), 60) tags = beatmap[0]['tags'] if tags == "": tags = "-" desc = ' **Length:** {}:{} **BPM:** {}\n**Tags:** {}\n------------------'.format(m, str(s).zfill(2), beatmap[0]['bpm'], tags) em = discord.Embed(description = desc, colour=0xeeeeee) em.set_author(name="{} - {} by {}".format(beatmap[0]['artist'], beatmap[0]['title'], beatmap[0]['creator']), url=beatmap_url) # sort maps map_order = [] for i in range(num_disp): map_order.append((i,float(beatmap[i]['difficultyrating']))) map_order = sorted(map_order, key=operator.itemgetter(1), reverse=True) for i, diff in map_order: beatmap_info = "" beatmap_info += "**▸Difficulty:** {:.2f}★ **Max Combo:** {}\n".format(float(beatmap[i]['difficultyrating']), beatmap[i]['max_combo']) beatmap_info += "**▸AR:** {} **▸OD:** {} **▸HP:** {} **▸CS:** {}\n".format(beatmap[i]['diff_approach'], beatmap[i]['diff_overall'], beatmap[i]['diff_drain'], beatmap[i]['diff_size']) em.add_field(name = "__{}__\n".format(beatmap[i]['version']), value = beatmap_info, inline = False) page = urllib.request.urlopen(beatmap_url) soup = BeautifulSoup(page.read(), "html.parser") map_image = [x['src'] for x in soup.findAll('img', {'class': 'bmt'})] map_image_url = 'http:{}'.format(map_image[0]).replace(" ", "%") # await self.bot.send_message(message.channel, map_image_url) em.set_thumbnail(url=map_image_url) await self.bot.send_message(message.channel, msg, embed = em) # --------------------- Tracking Section ------------------------------- @osutrack.command(pass_context=True, no_pm=True) async def list(self, ctx): """Check which players are currently tracked""" server = ctx.message.server channel = ctx.message.channel user = ctx.message.author em = discord.Embed(colour=user.colour) em.set_author(name="Osu! Players Currently Tracked in {}".format(server.name), icon_url = server.icon_url) channel_users = {} target_channel = None for username in self.track.keys(): if server.id in self.track[username]["servers"]: target_channel = find(lambda m: m.id == self.track[username]['servers'][server.id]["channel"], server.channels) if target_channel.name not in channel_users: channel_users[target_channel.name] = [] channel_users[target_channel.name].append(username) if target_channel and channel_users[target_channel.name]: channel_users[target_channel.name] = sorted(channel_users[target_channel.name]) for channel_name in channel_users.keys(): em.add_field(name = "__#{} ({})__".format(channel_name, len(channel_users[channel_name])), value = ", ".join(channel_users[channel_name])) else: em.description = "None." await self.bot.say(embed = em) @osutrack.command(pass_context=True, no_pm=True) @checks.mod_or_permissions(manage_messages=True) async def add(self, ctx, *usernames): """Adds a player to track for top scores.""" server = ctx.message.server channel = ctx.message.channel key = self.osu_api_key["osu_api_key"] msg = "" count_add = 0 if usernames == (None): await self.bot.say("Please enter a user") return for username in usernames: userinfo = list(await get_user(key, self.osu_settings["type"]["default"], username, 0)) if not userinfo or len(userinfo) == 0: msg+="`{}` does not exist in the osu! database.\n".format(username) else: if username not in self.track: self.track[username] = {} if server.id not in self.track[username]: self.track[username]["servers"] = {} self.track[username]["servers"][server.id] = {} # add channels that care about the user if "channel" not in self.track[username]["servers"][server.id]: self.track[username]["servers"][server.id]["channel"] = channel.id # add current userinfo if "userinfo" not in self.track[username]: self.track[username]["userinfo"] = {} for mode in modes: self.track[username]["userinfo"][mode] = list(await get_user(key, self.osu_settings["type"]["default"], username, self._get_gamemode_number(mode)))[0] # add last tracked time current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.track[username]["last_check"] = current_time count_add += 1 msg+="**`{}` added. Will now track on `#{}`**\n".format(username, channel.name) else: if server.id in self.track[username]["servers"]: if channel.id == self.track[username]["servers"][server.id]["channel"]: msg+="**Already tracking `{}` on `#{}.`**\n".format(username, channel.name) else: self.track[username]["servers"][server.id]["channel"] = channel.id # add a channel to track count_add += 1 msg+="**`{}` now tracking on `#{}`**\n".format(username, channel.name) else: if server.id not in self.track[username]["servers"]: self.track[username]["servers"][server.id] = {} self.track[username]["servers"][server.id]["channel"] = channel.id # add a channel to track count_add += 1 msg+="**`{}` added. Will now track on `#{}`**\n".format(username, channel.name) fileIO("data/osu/track.json", "save", self.track) if len(msg) > 500: await self.bot.say("**Added `{}` users to tracking on `#{}`.**".format(count_add, channel.name)) else: await self.bot.say(msg) @osutrack.command(pass_context=True, no_pm=True) @checks.mod_or_permissions(manage_messages=True) async def remove(self, ctx, *usernames:str): """Removes a player to track for top scores.""" server = ctx.message.server channel = ctx.message.channel msg = "" count_remove = 0 if usernames == (None): await self.bot.say("Please enter a user") return for username in usernames: if username in self.track and "servers" in self.track[username] and server.id in self.track[username]["servers"]: if channel.id == self.track[username]["servers"][server.id]["channel"]: del self.track[username]["servers"][server.id] if len(self.track[username]["servers"].keys()) == 0: del self.track[username] msg+="**No longer tracking `{}` in `#{}`.**\n".format(username, channel.name) count_remove += 1 fileIO("data/osu/track.json", "save", self.track) else: msg+="**`{}` is not currently being tracked in `#{}`.**\n".format(username, channel.name) else: msg+="**`{}` is not currently being tracked.**\n".format(username) if len(msg) > 500: await self.bot.say("**Removed `{}` users from tracking on `#{}`.**".format(count_remove, channel.name)) else: await self.bot.say(msg) # used to track top plays of specified users async def play_tracker(self): key = self.osu_api_key["osu_api_key"] while self == self.bot.get_cog('Osu'): # get all keys() to grab all current tracking users log.debug("looping through all users") for username in self.track.keys(): log.debug("checking {}".format(username)) # if the user's current top 10 scores are different from new top 10 try: new_plays = {} for mode in modes: new_plays[mode] = await get_user_best(key, self.osu_settings["type"]["default"], username, self._get_gamemode_number(mode), self.osu_settings["num_track"]) # gamemode = word for gamemode in self.track[username]["userinfo"].keys(): log.debug("examining gamemode {}".format(gamemode)) last_check = datetime.datetime.strptime(self.track[username]["last_check"], '%Y-%m-%d %H:%M:%S') new_timestamps = [] for new_play in new_plays[gamemode]: new_timestamps.append(datetime.datetime.strptime(new_play['date'], '%Y-%m-%d %H:%M:%S')) current_info = self.track[username]["userinfo"][gamemode] # user information score_gamemode = self._get_gamemode_display(gamemode) # loop to check what's different for i in range(len(new_timestamps)): if last_check != None and new_timestamps[i] != None and new_timestamps[i] > last_check: #print("Comparing new {} to old {}".format(new_timestamps[i], last_check)) top_play_num = i+1 play = new_plays[gamemode][i] play_map = await get_beatmap(key, self.osu_settings["type"]["default"], play['beatmap_id']) new_user_info = list(await get_user(key, self.osu_settings["type"]["default"], username, self._get_gamemode_number(gamemode))) new_user_info = new_user_info[0] # send appropriate message to channel log.debug("creating top play") if gamemode in self.track[username]["userinfo"]: old_user_info = self.track[username]["userinfo"] em = self._create_top_play(top_play_num, play, play_map, old_user_info[gamemode], new_user_info, score_gamemode) else: old_user_info = None em = self._create_top_play(top_play_num, play, play_map, old_user_info, new_user_info, score_gamemode) log.debug("sending embed") for server_id in self.track[username]['servers'].keys(): server = find(lambda m: m.id == server_id, self.bot.servers) if server_id not in self.osu_settings or "tracking" not in self.osu_settings[server_id] or self.osu_settings[server_id]["tracking"] == True: channel = find(lambda m: m.id == self.track[username]['servers'][server_id]["channel"], server.channels) await self.bot.send_message(channel, embed = em) #print("Setting last changed time to {}".format(new_timestamps[i])) self.track[username]["userinfo"][gamemode] = new_user_info self.track[username]["last_check"] = new_timestamps[i].strftime('%Y-%m-%d %H:%M:%S') fileIO("data/osu/track.json", "save", self.track) break except: log.info("Failed to load top score for".format(username)) log.debug("sleep 60 seconds") await asyncio.sleep(60) def _create_top_play(self, top_play_num, play, beatmap, old_user_info, new_user_info, gamemode): beatmap_url = 'https://osu.ppy.sh/b/{}'.format(play['beatmap_id']) user_url = 'https://{}/u/{}'.format(self.osu_settings["type"]["default"], new_user_info['user_id']) profile_url = 'http://s.ppy.sh/a/{}.png'.format(new_user_info['user_id']) beatmap = beatmap[0] # get infomation log.debug("getting change information") m, s = divmod(int(beatmap['total_length']), 60) mods = self.mod_calculation(play['enabled_mods']) if not mods: mods = [] mods.append('No Mod') em = discord.Embed(description='', colour=0xeeeeee) acc = self.calculate_acc(play, int(beatmap['mode'])) # grab beatmap image log.debug("getting map image") page = urllib.request.urlopen(beatmap_url) soup = BeautifulSoup(page.read(), "html.parser") map_image = [x['src'] for x in soup.findAll('img', {'class': 'bmt'})] map_image_url = 'http:{}'.format(map_image[0]) em.set_thumbnail(url=map_image_url) log.debug("creating embed") em.set_author(name="New #{} for {} in {}".format(top_play_num, new_user_info['username'], gamemode), icon_url = profile_url, url = user_url) info = "" info += "▸ [**__{} [{}]__**]({})\n".format(beatmap['title'], beatmap['version'], beatmap_url) info += "▸ +{} ▸ **{:.2f}%** ▸ **{}** Rank\n".format(','.join(mods), float(acc), play['rank']) info += "▸ **{:.2f}★** ▸ {}:{} ▸ {}bpm\n".format(float(beatmap['difficultyrating']), m, str(s).zfill(2), beatmap['bpm']) if old_user_info != None: dpp = float(new_user_info['pp_raw']) - float(old_user_info['pp_raw']) info += "▸ {} ▸ x{} ▸ **{:.2f}pp (+{:.2f})**\n".format(play['score'], play['maxcombo'], float(play['pp']), dpp) info += "▸ #{} → #{} ({}#{} → #{})".format(old_user_info['pp_rank'], new_user_info['pp_rank'], new_user_info['country'], old_user_info['pp_country_rank'], new_user_info['pp_country_rank']) else: info += "▸ {} ▸ x{} ▸ **{:.2f}pp**\n".format(play['score'], play['maxcombo'], float(play['pp'])) info += "▸ #{} ({}#{})".format(new_user_info['pp_rank'], new_user_info['country'], new_user_info['pp_country_rank']) em.description = info return em ###-------------------------Python wrapper for osu! api------------------------- # Gets the beatmap async def get_beatmap(key, api:str, beatmap_id): url_params = [] url_params.append(parameterize_key(key)) url_params.append(parameterize_id("b", beatmap_id)) async with aiohttp.get(build_request(url_params, "https://{}/api/get_beatmaps?".format(api))) as resp: return await resp.json() # Gets the beatmap set async def get_beatmapset(key, api:str, set_id): url_params = [] url_params.append(parameterize_key(key)) url_params.append(parameterize_id("s", set_id)) async with aiohttp.get(build_request(url_params, "https://{}/api/get_beatmaps?".format(api))) as resp: return await resp.json() # Grabs the scores async def get_scores(key, api:str, beatmap_id, user_id, mode): url_params = [] url_params.append(parameterize_key(key)) url_params.append(parameterize_id("b", beatmap_id)) url_params.append(parameterize_id("u", user_id)) url_params.append(parameterize_mode(mode)) async with aiohttp.get(build_request(url_params, "https://{}/api/get_scores?".format(api))) as resp: return await resp.json() async def get_user(key, api:str, user_id, mode): url_params = [] url_params.append(parameterize_key(key)) url_params.append(parameterize_id("u", user_id)) url_params.append(parameterize_mode(mode)) async with aiohttp.get(build_request(url_params, "https://{}/api/get_user?".format(api))) as resp: return await resp.json() async def get_user_best(key, api:str, user_id, mode, limit): url_params = [] url_params.append(parameterize_key(key)) url_params.append(parameterize_id("u", user_id)) url_params.append(parameterize_mode(mode)) url_params.append(parameterize_limit(limit)) async with aiohttp.get(build_request(url_params, "https://{}/api/get_user_best?".format(api))) as resp: return await resp.json() # Returns the user's ten most recent plays. async def get_user_recent(key, api:str, user_id, mode): url_params = [] url_params.append(parameterize_key(key)) url_params.append(parameterize_id("u", user_id)) url_params.append(parameterize_mode(mode)) async with aiohttp.get(build_request(url_params, "https://{}/api/get_user_recent?".format(api))) as resp: return await resp.json() # Returns the full API request URL using the provided base URL and parameters. def build_request(url_params, url): for param in url_params: url += str(param) if (param != ""): url += "&" return url[:-1] def parameterize_event_days(event_days): if (event_days == ""): event_days = "event_days=1" elif (int(event_days) >= 1 and int(event_days) <= 31): event_days = "event_days=" + str(event_days) else: print("Invalid Event Days") return event_days def parameterize_id(t, id): if (t != "b" and t != "s" and t != "u" and t != "mp"): print("Invalid Type") if (len(str(id)) != 0): return t + "=" + str(id) else: return "" def parameterize_key(key): if (len(key) == 40): return "k=" + key else: print("Invalid Key") def parameterize_limit(limit): ## Default case: 10 scores if (limit == ""): limit = "limit=10" elif (int(limit) >= 1 and int(limit) <= 100): limit = "limit=" + str(limit) else: print("Invalid Limit") return limit def parameterize_mode(mode): ## Default case: 0 (osu!) if (mode == ""): mode = "m=0" elif (int(mode) >= 0 and int(mode) <= 3): mode = "m=" + str(mode) else: print("Invalid Mode") return mode ###-------------------------Setup------------------------- def check_folders(): if not os.path.exists("data/osu"): print("Creating data/osu folder...") os.makedirs("data/osu") def check_files(): osu_api_key = {"osu_api_key" : ""} api_file = "data/osu/apikey.json" if not fileIO(api_file, "check"): print("Adding data/osu/apikey.json...") fileIO(api_file, "save", osu_api_key) else: # consistency check current = fileIO(api_file, "load") if current.keys() != osu_api_key.keys(): for key in system.keys(): if key not in osu_api_key.keys(): current[key] = osu_api_key[key] print("Adding " + str(key) + " field to osu apikey.json") fileIO(api_file, "save", current) # creates file for user backgrounds user_file = "data/osu/user_settings.json" if not fileIO(user_file, "check"): print("Adding data/osu/user_settings.json...") fileIO(user_file, "save", {}) # creates file for player tracking user_file = "data/osu/track.json" if not fileIO(user_file, "check"): print("Adding data/osu/track.json...") fileIO(user_file, "save", {}) # creates file for server to use settings_file = "data/osu/osu_settings.json" if not fileIO(settings_file, "check"): print("Adding data/osu/osu_settings.json...") fileIO(settings_file, "save", { "type": { "default": "osu.ppy.sh", "ripple":"ripple.moe" }, "num_track" : 50, "num_best_plays": 5, }) def setup(bot): check_folders() check_files() n = Osu(bot) loop = asyncio.get_event_loop() loop.create_task(n.play_tracker()) bot.add_listener(n.find_link, "on_message") bot.add_cog(n)