import hashlib import json from pathlib import Path from datetime import datetime, timedelta, timezone import requests import re import asyncio import nextcord import itertools import aiohttp from nextcord.ext import commands, application_checks, tasks from nextcord import TextInputStyle, IntegrationType teams_dict = {} teams_list = [] def get_all_teams(): data = requests.get("https://freecashe.ws/api/teams").json() teams_dict.clear() teams_list.clear() for index in data["items"]: teams_list.append(f"{index["location"]} {index["name"]}") teams_dict.update({f"{index["location"]} {index["name"]}": index["team_id"]}) print(teams_dict) print(teams_list) #all of this code is by evilscientist3, full credit to them HTTP_CACHE_DIR = Path("http_cache") statdict = { "BA":(0.4, 0.35, 0.3, 0.25, 0.2, 0.15), "OBP":(0.475, 0.425, 0.375, 0.325, 0.275, 0.225), "SLG":(0.55, 0.52, 0.46, 0.4, 0.36, 0.33), "OPS":(1.0, 0.9, 0.8, 0.7, 0.6, 0.5), "ERA":(2.75, 3.25, 3.75, 4.75, 5.5, 6.5), "WHIP":(1, 1.1, 1.25, 1.4, 1.5, 1.6) } # whether ascending is GOOD (True) or BAD (False) for figure of merit NGUdict = { "BA":True, "OBP":True, "SLG":True, "OPS":True, "ERA":False, "WHIP":False } # sig figs for each stat sfdict = { "BA":3, "OBP":3, "SLG":3, "OPS":3, "ERA":2, "WHIP":2 } def eval_stat(stat, val): # modifier for if descending is better. in this case logic is reversed; most easily accounted for by multiplying bounds and values by -1 # why bother keeping descending values' bounds positive, you ask? for human readability and ease of modification, I say mod = 1 if NGUdict[stat] else -1 # set of ANSI colour codes. best to worst, one more than bounds colours = [32, 72, 36, 30, 33, 31, 31] # multiply bounds to account for descending bounds = [mod*i for i in statdict[stat]] mval = mod*val # flag for colour being set, as an "else" condition to the entire loop cset = 0 # main loop - checks quality in descending order for i, j in enumerate(bounds): if mval > j: colour = colours[i] cset = 1 break # check if value is below all bounds if cset == 0: colour = colours[-1] # return stat string in correct colour return f"\033[38;5;{colour}m{val:.{sfdict[stat]}f}\033[0m" def stable_str_hash(in_val: str) -> str: return hex(int(hashlib.md5(in_val.encode("utf-8")).hexdigest(), 16))[2:] def get_json(url: str) -> dict: HTTP_CACHE_DIR.mkdir(exist_ok=True) cache = {} cache_file_path = HTTP_CACHE_DIR / f"{stable_str_hash(url)}.json" try: with open(cache_file_path) as cache_file: cache = json.load(cache_file) except FileNotFoundError: pass # Return from cache if the cache entry is less than 5 minutes old now = datetime.now(timezone.utc) if ( url in cache and "__archived_at" in cache[url] and cache[url]["__archived_at"] > (now - timedelta(minutes=5)).isoformat() ): return cache[url] data = requests.get(url).json() cache[url] = data cache[url]["__archived_at"] = now.isoformat() with open(cache_file_path, "w") as cache_file: json.dump(cache, cache_file) return data def teamstats(MY_TEAM_ID): finalstr = "" team_obj = get_json(f"https://mmolb.com/api/team/{MY_TEAM_ID}") for player in team_obj["Players"]: if player['FirstName'] == 'Empty' and player["LastName"] == "Slot": continue player_obj = get_json(f"https://mmolb.com/api/player/{player['PlayerID']}") try: # I'm pretty sure IDs are lexicographically ordered, so we want the # maximum value to get stats for the latest season stats_obj = player_obj["Stats"][max(player_obj["Stats"].keys())] except ValueError: finalstr += (f"No stats for {player["FirstName"]} {player["LastName"]} \n") continue singles = stats_obj.get("singles", 0) doubles = stats_obj.get("doubles", 0) triples = stats_obj.get("triples", 0) home_runs = stats_obj.get("home_runs", 0) hits = singles + doubles + triples + home_runs bb = stats_obj.get("walked", 0) hbp = stats_obj.get("hit_by_pitch", 0) earned_runs = stats_obj.get("earned_runs", 0) hits_allowed = stats_obj.get("hits_allowed", 0) walks = stats_obj.get("walks", 0) try: ab = stats_obj["at_bats"] except KeyError: ba_str = None else: ba = hits / ab ba_str = f"BA: {eval_stat('BA', ba)} ({ab} AB)" try: pa = stats_obj["plate_appearances"] ab = stats_obj["at_bats"] except KeyError: ops_str = None else: obp = (hits + bb + hbp) / pa slg = (singles + 2 * doubles + 3 * triples + 4 * home_runs) / ab ops = obp + slg pa_str = dot_format(pa) ops_str = f"OBP: {eval_stat('OBP', obp)}, SLG: {eval_stat('SLG', slg)}, OPS: {eval_stat('OPS', ops)} ({pa_str} PA)" try: ip = stats_obj["outs"] / 3 except KeyError: era_str = None else: era = 9 * earned_runs / ip whip = (walks + hits_allowed) / ip ip_str = dot_format(ip) era_str = f"ERA {eval_stat('ERA', era)} WHIP {eval_stat('WHIP', whip)} ({ip_str} IP)" stats_str = ", ".join(s for s in [ba_str, ops_str, era_str] if s is not None) if stats_str: finalstr += (f"{player["Position"]} {player["FirstName"]} {player["LastName"]} ") finalstr += "" * (30 - (len(player["Position"]) + len(player["FirstName"]) + len(player["LastName"]))) finalstr += f"{stats_str}\n" return (finalstr if finalstr != "" else "There are no players on this team") def dot_format(in_val: float) -> str: ip_whole = int(in_val) ip_remainder = int((in_val - ip_whole) / 3 * 10) if ip_remainder == 0: ip_str = f"{ip_whole}" else: ip_str = f"{ip_whole}.{ip_remainder}" return ip_str #------- class TeamView(nextcord.ui.View): def __init__(self): super().__init__(timeout=None) @nextcord.ui.button( label="Players", style=nextcord.ButtonStyle.green, custom_id="team:players" ) async def getplayersbutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): await interaction.response.defer(ephemeral=True) loop = asyncio.get_event_loop() ogmsg = interaction.message.embeds embed = ogmsg[0] stats = await loop.run_in_executor(None, teamstats,embed.footer.text) splistats = [stats[i:i+1900] for i in range(0, len(stats), 1900)] #TODO make better as in don't do cutoff for i in splistats: await interaction.followup.send(f"```ansi\n{i}```",ephemeral=True) @nextcord.ui.button( label="Game History", style=nextcord.ButtonStyle.green, custom_id="team:gamehistory" ) async def gamehistorybutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): await interaction.response.defer(ephemeral=True) ogmsg = interaction.message.embeds embed = ogmsg[0] teamid = embed.footer.text data = requests.get(f"https://mmolb.com/api/team/{teamid}").json() history = requests.get(f"https://freecashe.ws/api/games?team={teamid}&season=3").json()["items"] color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"Last ten games for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.set_footer(text=teamid) for index in reversed(list(itertools.islice(history, (len(history)-10 if len(history)-10 > 0 else 0) , len(history)))): if index["away_team_id"] != teamid: awayteamid = index["away_team_id"] otherscore = index["last_update"]["away_score"] ourscore = index["last_update"]["home_score"] else: awayteamid = index["home_team_id"] otherscore = index["last_update"]["home_score"] ourscore = index["last_update"]["away_score"] tempdata = requests.get(f"https://mmolb.com/api/team/{awayteamid}").json() embed.add_field(name=f"vs. {tempdata["Location"]} {tempdata["Name"]} {tempdata["Emoji"]} ({ourscore} - {otherscore})", value=f"[watch]()", inline=False) await interaction.followup.send(embed=embed,ephemeral=True) @nextcord.ui.button( label="Feed", style=nextcord.ButtonStyle.green, custom_id="team:feed" ) async def feedbutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): await interaction.response.defer(ephemeral=True) ogmsg = interaction.message.embeds embed = ogmsg[0] teamid = embed.footer.text async with aiohttp.ClientSession() as session: data = await session.get(f"https://mmolb.com/api/team/{teamid}") data = await data.json() color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"Last ten feed events for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.set_footer(text=teamid) data = data['Feed'] for index in itertools.islice(data, (len(data)-10 if len(data)-10 > 0 else 0) , len(data)): embed.add_field(name=f"{index['emoji']} Day {index['day']} Season {index['season']}", value=index['text'], inline=False) await interaction.followup.send(embed=embed,ephemeral=True) class team(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.updateallteams.start() def cog_unload(self): self.updateallteams.cancel() @nextcord.slash_command( name="team", description="Get information about a team", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def greaterteam(self, interaction: nextcord.Interaction, team: str = nextcord.SlashOption( name = "team", description = "The team", )): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] data = requests.get(f"https://mmolb.com/api/team/{teamid}").json() rankdata = requests.get(f"https://mmolb.com/api/league-top-teams/{data["League"]}").json()["teams"] ranking = 0 for i in rankdata: ranking += 1 if i["_id"] == teamid: break color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"{data["Location"]} {data["Name"]} {data["Emoji"]}", description=f"{data["Motto"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.add_field(name="League", value=f"{dict((v,k) for k,v in self.bot.leagues_dict.items())[data["League"]]}", inline=True) embed.add_field(name="Wins", value=f"{data["Record"]["Regular Season"]["Wins"]}", inline=True) embed.add_field(name="Losses", value=f"{data["Record"]["Regular Season"]["Losses"]}", inline=True) embed.add_field(name="Run Differential", value=f"{data["Record"]["Regular Season"]["RunDifferential"]}", inline=True) embed.add_field(name="Rank", value=f"{ranking}", inline=True) embed.add_field(name="Augments", value=f"{data["Augments"]}", inline=True) embed.add_field(name="Championships", value=f"{data["Championships"]}", inline=True) embed.set_footer(text=teamid) await interaction.edit_original_message(embed=embed,view=TeamView()) @greaterteam.on_autocomplete("team") async def greaterteamac(self, interaction: nextcord.Interaction, team: str): if not team: thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @nextcord.slash_command( name="gamehistory", description="Get the last 10 games played by a team", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def gamehistory(self, interaction: nextcord.Interaction, team: str = nextcord.SlashOption( name = "team", description = "The team" )): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] data = requests.get(f"https://mmolb.com/api/team/{teamid}").json() history = requests.get(f"https://freecashe.ws/api/games?team={teamid}&season=3").json()["items"] color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"Last ten games for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.set_footer(text=teamid) for index in reversed(list(itertools.islice(history, (len(history)-10 if len(history)-10 > 0 else 0) , len(history)))): if index["away_team_id"] != teamid: awayteamid = index["away_team_id"] otherscore = index["last_update"]["away_score"] ourscore = index["last_update"]["home_score"] else: awayteamid = index["home_team_id"] otherscore = index["last_update"]["home_score"] ourscore = index["last_update"]["away_score"] tempdata = requests.get(f"https://mmolb.com/api/team/{awayteamid}").json() embed.add_field(name=f"vs. {tempdata["Location"]} {tempdata["Name"]} {tempdata["Emoji"]} ({ourscore} - {otherscore})", value=f"[watch]()", inline=False) await interaction.edit_original_message(embed=embed) @gamehistory.on_autocomplete("team") async def gamehistoryac(self, interaction: nextcord.Interaction, team: str): if not team: print("we're here") thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @nextcord.slash_command( name="players", description="Get a team's player statistics", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def teamstats(self, interaction: nextcord.Interaction, team: str): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] loop = asyncio.get_event_loop() stats = await loop.run_in_executor(None, teamstats,teamid) splistats = [stats[i:i+1900] for i in range(0, len(stats), 1900)] #TODO make better as in don't do cutoff await interaction.edit_original_message(content=f"```ansi\n{splistats[0]}```") splistats.pop(0) for i in splistats: await interaction.followup.send(f"```ansi\n{i}```") @teamstats.on_autocomplete("team") async def teamstatsac(self, interaction: nextcord.Interaction, team: str): if not team: print("we're here") thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @nextcord.slash_command( name="feed", description="Get the latest entry from the team's feed", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def teamfeed(self, interaction: nextcord.Interaction, team: str): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] async with aiohttp.ClientSession() as session: data = await session.get(f"https://mmolb.com/api/team/{teamid}") data = await data.json() color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"Last ten feed events for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.set_footer(text=teamid) data = data['Feed'] for index in itertools.islice(data, (len(data)-10 if len(data)-10 > 0 else 0) , len(data)): embed.add_field(name=f"{index['emoji']} Day {index['day']} Season {index['season']}", value=index['text'], inline=False) await interaction.edit_original_message(embed=embed) @teamfeed.on_autocomplete("team") async def feedac(self, interaction: nextcord.Interaction, team: str): if not team: print("we're here") thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @tasks.loop(hours=1) async def updateallteams(self): print("Updating teams autocomplete") loop = asyncio.get_event_loop() await loop.run_in_executor(None, get_all_teams) self.bot.teams_list = teams_list self.bot.teams_dict = teams_dict def setup(bot: commands.Bot): bot.add_cog(team(bot))