mirror of
https://github.com/insertapp/mmolbbot.git
synced 2025-07-01 06:17:03 +00:00
401 lines
16 KiB
Python
401 lines
16 KiB
Python
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
from datetime import datetime, timedelta, timezone
|
|
import requests
|
|
import re
|
|
import asyncio
|
|
import nextcord
|
|
import itertools
|
|
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()
|
|
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=2").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](<https://mmolb.com/watch/{index['game_id']}>)", 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=2").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](<https://mmolb.com/watch/{index['game_id']}>)", 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)
|
|
|
|
|
|
|
|
@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)) |