import hashlib
import json
from pathlib import Path
from datetime import datetime, timedelta, timezone
import requests
import asyncio
import nextcord
import aiosqlite as sqlite3
from nextcord.ext import commands, application_checks, tasks
from nextcord import TextInputStyle, IntegrationType
import timeit
timings = []
async def livegameworker(self,serverid,userid,channelid,messageid,gameid,offset):
begin = timeit.default_timer()
lastserverid = serverid
try:
channel = self.bot.get_channel(channelid)
message = await channel.fetch_message(messageid)
data = requests.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset}").json()
if len(data["entries"]) > 0:
data = requests.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset-((7-len(data["entries"]) if offset > 7 else offset))}").json()
basedata = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
finalstr = ""
offsetadd = 0
for i in data["entries"]:
finalstr += f"\n{i['message'].replace("", "**").replace("", "**")}"
offsetadd += 1
if i["event"] == "Recordkeeping":
await self.bot.db.execute(f"""
DELETE from liveupdate WHERE messageid = {messageid}
""")
#await self.bot.db.commit()
color = tuple(int(basedata["HomeTeamColor"][i:i+2], 16) for i in (0, 2, 4))
embed = nextcord.Embed(title=f"{basedata["AwayTeamName"]} {basedata["AwayTeamEmoji"]} **{data["entries"][-1]["away_score"]}** vs {basedata["HomeTeamName"]} {basedata["HomeTeamEmoji"]} **{data["entries"][-1]["home_score"]}**",
description=f"{"Bottom" if data["entries"][-1]["inning_side"] == 1 else "Top"} of the {data["entries"][-1]["inning"]}",
colour = nextcord.Color.from_rgb(color[0], color[1], color[2]))
embed.set_footer(text=gameid)
match data["entries"][-1]["balls"]:
case 1:
embed.add_field(name="Balls",value="🔴â•â•")
case 2:
embed.add_field(name="Balls",value="🔴🔴â•")
case 3:
embed.add_field(name="Balls",value="🔴🔴🔴")
case _:
embed.add_field(name="Balls",value="â•â•â•")
match data["entries"][-1]["strikes"]:
case 1:
embed.add_field(name="Strikes",value="🔴â•")
case 2:
embed.add_field(name="Strikes",value="🔴🔴")
case _:
embed.add_field(name="Strikes",value="â•â•")
match data["entries"][-1]["outs"]:
case 1:
embed.add_field(name="Outs",value="🔴â•")
case 2:
embed.add_field(name="Outs",value="🔴🔴")
case _:
embed.add_field(name="Outs",value="â•â•")
embed.add_field(name="Batting",value=data["entries"][-1]["batter"], inline=True)
embed.add_field(name="Pitching",value=data["entries"][-1]["pitcher"], inline=True)
embed.add_field(name="On Deck",value=data["entries"][-1]["on_deck"], inline=True)
embed.add_field(name=f"Last 7 events",value=finalstr,inline=False)
embed.set_thumbnail(f"https://insertapp.net/mmolbbot/assets/diamond_{data["entries"][-1]["on_1b"]}_{data["entries"][-1]["on_2b"]}_{data["entries"][-1]["on_3b"]}.png")
embed.set_footer(text=f"Embed too big? Need historical data? consider classic mode.")
await message.edit(content="",embed=embed)
await self.bot.db.execute(f"""
UPDATE liveupdate set offset = {offset+offsetadd} WHERE messageid = '{messageid}'
""")
except Exception as e:
await self.bot.db.execute(f"""
DELETE from liveupdate WHERE messageid = {messageid}
""")
#await self.bot.db.commit()
await message.edit(f"An error occoured in this live update\n{e}")
warning = self.bot.get_channel(1365478368555827270)
await warning.send(e)
except nextcord.Forbidden:
warning = self.bot.get_channel(1365478368555827270)
await self.bot.db.execute("DELETE from teamsubscriptions WHERE serverid = ?", (lastserverid,))
await self.bot.db.execute("DELETE from liveupdate WHERE serverid = ?", (lastserverid,))
await self.bot.db.execute("DELETE from spotlightsubscriptions WHERE serverid = ?", (lastserverid,))
await self.bot.db.commit()
await warning.send(f"Deleted {lastserverid} from the database due to 403 error")
async def classiclivegameworker(self,serverid,userid,channelid,gameid,offset):
begin = timeit.default_timer()
lastserverid = serverid
try:
channel = self.bot.get_channel(channelid)
data = requests.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset}").json()
if len(data["entries"]) > 0:
#data = requests.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset-((7-len(data["entries"]) if offset > 7 else offset))}").json()
basedata = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
finalstr = ""
offsetadd = 0
maysend = False
for i in data["entries"]:
if "scores" in i['message'] or "homers" in i['message']:
finalstr += f"\n>>> {i['message'].replace(", ","\n").replace(". ","\n").replace("", "").replace("", "\n")}"
maysend = True
offsetadd += 1
if i["event"] == "Recordkeeping":
await self.bot.db.execute(f"""
DELETE from liveupdate WHERE channelid = {channelid} AND gameid = '{gameid}'
""")
maysend = True
finalstr += f"\n> {i['message'].replace("", "**").replace("", "**")}"
#await self.bot.db.commit()
if maysend:
if data["entries"][-1]["inning_side"] == 1:
await channel.send(f"Bottom of the {data["entries"][-1]["inning"]} | {basedata["AwayTeamName"]} {basedata["AwayTeamEmoji"]} {data["entries"][-1]["away_score"]} vs {basedata["HomeTeamName"]} {basedata["HomeTeamEmoji"]} **{data["entries"][-1]["home_score"]}**{finalstr}")
else:
await channel.send(f"Top of the {data["entries"][-1]["inning"]} | {basedata["AwayTeamName"]} {basedata["AwayTeamEmoji"]} **{data["entries"][-1]["away_score"]}** vs {basedata["HomeTeamName"]} {basedata["HomeTeamEmoji"]} {data["entries"][-1]["home_score"]}{finalstr}")
await self.bot.db.execute(f"""
UPDATE liveupdate set offset = {offset+offsetadd} WHERE channelid = '{channelid}' AND gameid = '{gameid}' AND classic = 1
""")
except Exception as e:
warning = self.bot.get_channel(1365478368555827270)
await warning.send(e)
except nextcord.Forbidden:
warning = self.bot.get_channel(1365478368555827270)
await self.bot.db.execute("DELETE from teamsubscriptions WHERE serverid = ?", (lastserverid,))
await self.bot.db.execute("DELETE from liveupdate WHERE serverid = ?", (lastserverid,))
await self.bot.db.execute("DELETE from spotlightsubscriptions WHERE serverid = ?", (lastserverid,))
await self.bot.db.commit()
await warning.send(f"Deleted {lastserverid} from the database due to 403 error")
class liveupdate(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.updatelivegames.start()
self.checkspotlightsubscriptions.start()
self.checkteamsubscriptions.start()
def cog_unload(self):
self.updatelivegames.cancel()
self.checkspotlightsubscriptions.cancel()
self.checkteamsubscriptions.cancel()
@nextcord.slash_command(
name="spotlightgame",
description="Watch the spotlight game",
integration_types=[
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
)
async def spotlightwatch(self, interaction: nextcord.Interaction,classic: str = nextcord.SlashOption(description ="Follow the game using classic format, best for a group watch", choices={"Yes": "True", "No": "False"})):
await interaction.response.defer()
game = requests.get("https://mmolb.com/api/spotlight").json()
gameid = game["game_id"]
data = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
if data["State"] == "Complete":
await interaction.edit_original_message(content="The spotlight game has already concluded")
return
if classic == "True":
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({interaction.guild_id}, {interaction.user.id}, {interaction.channel_id}, NULL, "{gameid}", {len(data["EventLog"])}, 1)
""")
await interaction.edit_original_message(content=f"Classic live update created!")
else:
await interaction.edit_original_message(content=f"{data["AwayTeamName"]} {data["AwayTeamEmoji"]} **{data["EventLog"][-1]["away_score"]}** vs {data["HomeTeamName"]} {data["HomeTeamEmoji"]} **{data["EventLog"][-1]["home_score"]}**")
message = await interaction.original_message()
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({interaction.guild_id}, {interaction.user.id}, {interaction.channel_id}, {message.id}, "{gameid}", {len(data["EventLog"])}, 0)
""")
await self.bot.db.commit()
@nextcord.slash_command(name="subscribe", description="Subscribe to...",
integration_types=[
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
default_member_permissions=nextcord.Permissions(manage_channels=True),)
async def subscribe(self, interaction: nextcord.Interaction):
#This is never called
pass
@subscribe.subcommand(
name="spotlight",
description="Subscribe to every spotlight game, sending them in this channel",
)
async def spotlightsubscribe(self, interaction: nextcord.Interaction):
await interaction.response.defer()
try:
game = requests.get("https://mmolb.com/api/spotlight").json()
gameid = game["game_id"]
data = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
await interaction.edit_original_message(content=f"{data["AwayTeamName"]} {data["AwayTeamEmoji"]} **{data["EventLog"][-1]["away_score"]}** vs {data["HomeTeamName"]} {data["HomeTeamEmoji"]} **{data["EventLog"][-1]["home_score"]}**")
message = await interaction.original_message()
if data["State"] != "Complete":
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({interaction.guild_id}, {interaction.user.id}, {interaction.channel_id}, {message.id}, "{gameid}", {len(data["EventLog"])}, 0)
""")
except KeyError:
await interaction.edit_original_message(content="This channel is now subscribed to updates")
await self.bot.db.execute(f"""
INSERT INTO spotlightsubscriptions VALUES
({interaction.guild_id}, {interaction.channel_id}, 0)
""")
await self.bot.db.commit()
await interaction.followup.send(content="From now on every spotlight game will be sent in this channel", ephemeral=True)
@subscribe.subcommand(
name="spotlightclassic",
description="Subscribe to every spotlight game using an old familar format, best if you want historial data",
)
async def spotlightclassicsubscribe(self, interaction: nextcord.Interaction):
await interaction.response.defer()
await self.bot.db.execute(f"""
INSERT INTO spotlightsubscriptions VALUES
({interaction.guild_id}, {interaction.channel_id}, 1)
""")
await self.bot.db.commit()
await interaction.edit_original_message(content="This channel is now subscribed to updates")
@subscribe.subcommand(
name="team",
description="Subscribe to every game for the specified team, sending them in this channel",
)
async def teamsubscribe(self, interaction: nextcord.Interaction, team: str):
if team not in self.bot.teams_dict:
await interaction.response.send_message("Invalid Team!", ephemeral=True)
await interaction.response.defer()
teamid = self.bot.teams_dict[team]
try:
game = requests.get(f"https://mmolb.com/api/game-by-team/{teamid}").json()
gameid = game["game_id"]
data = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
await interaction.edit_original_message(content=f"{data["AwayTeamName"]} {data["AwayTeamEmoji"]} **{data["EventLog"][-1]["away_score"]}** vs {data["HomeTeamName"]} {data["HomeTeamEmoji"]} **{data["EventLog"][-1]["home_score"]}**")
message = await interaction.original_message()
if data["State"] != "Complete":
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({interaction.guild_id}, {interaction.user.id}, {interaction.channel_id}, {message.id}, "{gameid}", {len(data["EventLog"])}, 0)
""")
except KeyError:
await interaction.edit_original_message(content="This channel is now subscribed to updates")
await self.bot.db.execute(f"""
INSERT INTO teamsubscriptions VALUES
({interaction.guild_id}, {interaction.channel_id}, "{teamid}", 0)
""")
await self.bot.db.commit()
await interaction.followup.send(content="From now on every game for the specified team will be sent in this channel", ephemeral=True)
@teamsubscribe.on_autocomplete("team")
async def teamsubscribeac(self, interaction: nextcord.Interaction, team: str):
if not team:
thanksdiscord = self.bot.teams_list[:20]
await interaction.response.send_autocomplete(thanksdiscord)
return
closestteam = [name for name in self.bot.teams_list if name.lower().startswith(team.lower())]
thanksdiscord = closestteam[:20]
await interaction.response.send_autocomplete(thanksdiscord)
@subscribe.subcommand(
name="classic",
description="Subscribe to a team using an old familar format, best if you want historial data",
)
async def classicteamsubscribe(self, interaction: nextcord.Interaction, team: str):
if team not in self.bot.teams_dict:
await interaction.response.send_message("Invalid Team!", ephemeral=True)
await interaction.response.defer()
teamid = self.bot.teams_dict[team]
await interaction.edit_original_message(content="This channel is now subscribed to updates")
await self.bot.db.execute(f"""
INSERT INTO teamsubscriptions VALUES
({interaction.guild_id}, {interaction.channel_id}, "{teamid}", 1)
""")
await self.bot.db.commit()
@classicteamsubscribe.on_autocomplete("team")
async def classicteamsubscribeac(self, interaction: nextcord.Interaction, team: str):
if not team:
thanksdiscord = self.bot.teams_list[:20]
await interaction.response.send_autocomplete(thanksdiscord)
return
closestteam = [name for name in self.bot.teams_list if name.lower().startswith(team.lower())]
thanksdiscord = closestteam[:20]
await interaction.response.send_autocomplete(thanksdiscord)
@nextcord.slash_command(
name="watch",
description="Get live updates on a game",
integration_types=[
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
)
async def liveupdatecreate(self, interaction: nextcord.Interaction, gameid: str, classic: str = nextcord.SlashOption(description ="Follow the game using classic format, best for a group watch", choices={"Yes": "True", "No": "False"})):
data = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
if data["State"] == "Complete":
await interaction.response.send_message("That game has already concluded", ephemeral=True)
return
if classic == "True":
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({interaction.guild_id}, {interaction.user.id}, {interaction.channel_id}, NULL, "{gameid}", {len(data["EventLog"])}, 1)
""")
await interaction.response.send_message(f"Classic live update created!", ephemeral=True)
else:
await interaction.response.send_message(f"{data["AwayTeamName"]} {data["AwayTeamEmoji"]} **{data["EventLog"][-1]["away_score"]}** vs {data["HomeTeamName"]} {data["HomeTeamEmoji"]} **{data["EventLog"][-1]["home_score"]}**")
message = await interaction.original_message()
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({interaction.guild_id}, {interaction.user.id}, {interaction.channel_id}, {message.id}, "{gameid}", {len(data["EventLog"])}, 0)
""")
await self.bot.db.commit()
@nextcord.slash_command(name="unsubscribe", description="Unsubscribe to...",
integration_types=[
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
default_member_permissions=nextcord.Permissions(manage_channels=True),
)
async def unsubscribe(self, interaction: nextcord.Interaction):
#This is never called
pass
@unsubscribe.subcommand(
name="spotlight",
description="Unsubscribe to spotlight game updates in this channel",
)
async def unsubscribespotlightgame(self, interaction: nextcord.Interaction):
await self.bot.db.execute(f"""
DELETE from spotlightsubscriptions WHERE channelid = {interaction.channel_id}
""")
await self.bot.db.commit()
await interaction.response.send_message("If existent, it has been removed")
@unsubscribe.subcommand(
name="team",
description="Unsubscribe to team game updates in this channel",
)
async def unsubscribeteamgame(self, interaction: nextcord.Interaction, team: str):
if team not in self.bot.teams_dict:
await interaction.response.send_message("Invalid Team!", ephemeral=True)
await interaction.response.defer()
teamid = self.bot.teams_dict[team]
await self.bot.db.execute(f"""
DELETE from teamsubscriptions WHERE channelid = ? AND teamid = ?
""",(interaction.channel_id,teamid))
await self.bot.db.commit()
await interaction.edit_original_message(content="If existent, it has been removed")
@unsubscribeteamgame.on_autocomplete("team")
async def unsubscribeteamgameac(self, interaction: nextcord.Interaction, team: str):
if not team:
thanksdiscord = self.bot.teams_list[:20]
await interaction.response.send_autocomplete(thanksdiscord)
return
closestteam = [name for name in self.bot.teams_list if name.lower().startswith(team.lower())]
thanksdiscord = closestteam[:20]
await interaction.response.send_autocomplete(thanksdiscord)
@nextcord.slash_command(
name="liveupdatesdelete",
description="DEBUG: Delete a subscribed update",
integration_types=[
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
)
async def liveupdatedelete(self, interaction: nextcord.Interaction, messageid: str):
await interaction.response.defer(ephemeral=True)
await self.bot.db.execute(f"""
DELETE from liveupdate WHERE messageid = ?
""", (int(messageid),))
await self.bot.db.commit()
await interaction.edit_original_message(content="stopped updates for message") #TODO This will be a button
@tasks.loop(seconds=30.0)
async def updatelivegames(self):
warning = self.bot.get_channel(1365478368555827270)
try:
begin = timeit.default_timer()
await self.bot.wait_until_ready()
print("updating live games")
res = await self.bot.db.execute("SELECT serverid,userid,channelid,messageid,gameid,offset FROM liveupdate WHERE classic = 0")
res = await res.fetchall()
worklist = [livegameworker(self,serverid,userid,channelid,messageid,gameid,offset) for [serverid,userid,channelid,messageid,gameid,offset] in res]
res = await self.bot.db.execute("SELECT serverid,userid,channelid,gameid,offset FROM liveupdate WHERE classic = 1")
res = await res.fetchall()
worklist = worklist + [classiclivegameworker(self,serverid,userid,channelid,gameid,offset) for [serverid,userid,channelid,gameid,offset] in res]
await asyncio.gather(*worklist)
await self.bot.db.commit()
timings.append(timeit.default_timer()-begin)
if len(timings) >= 12:
finalstr = "Last 12 timings:"
for i in timings:
finalstr += f"\n{i}s"
await warning.send(finalstr)
timings.clear()
except Exception as e:
#warning = self.bot.get_channel(1365478368555827270)
await warning.send(e)
print(e)
@tasks.loop(seconds=120.0)
async def checkspotlightsubscriptions(self):
try:
await self.bot.wait_until_ready()
print("refreshing spotlight subscriptions")
game = requests.get("https://mmolb.com/api/spotlight").json()
gameid = game["game_id"]
res = await self.bot.db.execute("SELECT serverid,channelid,classic FROM spotlightsubscriptions")
res = await res.fetchall()
print(res)
for [serverid,channelid,classic] in res:
try:
check = await self.bot.db.execute("SELECT serverid,userid,channelid,messageid,gameid,offset FROM liveupdate WHERE channelid = ? AND gameid = ? AND classic = ?", (channelid,gameid,classic))
test = await check.fetchone()
print(test)
if test is None:
print("True")
data = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
channel = self.bot.get_channel(channelid)
if data["State"] == "Complete":
continue
check = await self.bot.db.execute("SELECT serverid,userid,channelid,messageid,gameid,offset FROM liveupdate WHERE channelid = ? AND gameid = ? AND classic = ?", (channelid,gameid,classic))
test = await check.fetchone()
if test is None: #no idea why it has to have two checks
if classic == 0:
message = await channel.send(content=f"{data["AwayTeamName"]} {data["AwayTeamEmoji"]} **{data["EventLog"][-1]["away_score"]}** vs {data["HomeTeamName"]} {data["HomeTeamEmoji"]} **{data["EventLog"][-1]["home_score"]}**")
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({channel.guild.id}, {self.bot.application_id}, {channelid}, {message.id}, "{gameid}", {len(data["EventLog"])}, 0)
""")
else:
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({channel.guild.id}, {self.bot.application_id}, {channelid}, NULL, "{gameid}", {len(data["EventLog"])}, 1)
""")
await self.bot.db.commit()
else:
print("false")
except sqlite3.OperationalError:
await self.bot.db.close()
self.checkspotlightsubscriptions.cancel()
self.checkteamsubscriptions.cancel()
warning = self.bot.get_channel(1365478368555827270)
await warning.send(f"<@{self.bot.owner_id}> database is locked!")
except Exception as e:
warning = self.bot.get_channel(1365478368555827270)
await warning.send(f"Ignoring exception in spotlight check: {e}")
print(e)
continue
except KeyError:
pass
except Exception as e:
#I know this is bad practice but these loops must be running
print(e)
warning = self.bot.get_channel(1365478368555827270)
await warning.send(e)
print(e)
return
@tasks.loop(seconds=120.0)
async def checkteamsubscriptions(self):
try:
print("refreshing team subscriptions")
await self.bot.wait_until_ready()
res = await self.bot.db.execute("SELECT serverid,channelid,teamid,classic FROM teamsubscriptions")
res = await res.fetchall()
print(res)
for [serverid,channelid,teamid,classic] in res:
try:
game = requests.get(f"https://mmolb.com/api/game-by-team/{teamid}").json()
gameid = game["game_id"]
check = await self.bot.db.execute("SELECT serverid,userid,channelid,messageid,gameid,offset FROM liveupdate WHERE channelid = ? AND gameid = ? AND classic = ?", (channelid,gameid,classic))
test = await check.fetchone()
if test is None:
data = requests.get(f"https://mmolb.com/api/game/{gameid}").json()
channel = self.bot.get_channel(channelid)
if data["State"] == "Complete":
continue
check = await self.bot.db.execute("SELECT serverid,userid,channelid,messageid,gameid,offset FROM liveupdate WHERE channelid = ? AND gameid = ? AND classic = ?", (channelid,gameid,classic))
test = await check.fetchone()
if test is None: #no idea why it has to have two checks
if classic == 0:
message = await channel.send(content=f"{data["AwayTeamName"]} {data["AwayTeamEmoji"]} **{data["EventLog"][-1]["away_score"]}** vs {data["HomeTeamName"]} {data["HomeTeamEmoji"]} **{data["EventLog"][-1]["home_score"]}**")
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({channel.guild.id}, {self.bot.application_id}, {channelid}, {message.id}, "{gameid}", {len(data["EventLog"])}, 0)
""")
else:
await self.bot.db.execute(f"""
INSERT INTO liveupdate VALUES
({channel.guild.id}, {self.bot.application_id}, {channelid}, NULL, "{gameid}", {len(data["EventLog"])}, 1)
""")
await self.bot.db.commit()
except KeyError:
continue
except sqlite3.OperationalError:
await self.bot.db.close()
self.checkspotlightsubscriptions.cancel()
self.checkteamsubscriptions.cancel()
warning = self.bot.get_channel(1365478368555827270)
await warning.send(f"<@{self.bot.owner_id}> database is locked!")
except Exception as e:
warning = self.bot.get_channel(1365478368555827270)
await warning.send(f"Ignoring exception in {teamid}: {e}")
print(e)
continue
except Exception as e:
#I know this is bad practice but these loops must be running
warning = self.bot.get_channel(1365478368555827270)
await warning.send(e)
print(e)
return
def setup(bot: commands.Bot):
bot.add_cog(liveupdate(bot))