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))