import hashlib import json from pathlib import Path from datetime import datetime, timedelta, timezone import requests import asyncio import nextcord import aiohttp 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: async with aiohttp.ClientSession() as session: channel = self.bot.get_channel(channelid) message = await channel.fetch_message(messageid) data = await session.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset}") data = await data.json() basedata = await session.get(f"https://mmolb.com/api/game/{gameid}") basedata = await basedata.json() if basedata["State"] == "Complete": await self.bot.db.execute(f""" DELETE from liveupdate WHERE messageid = {messageid} """) if len(data["entries"]) > 0: data = await session.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset-((7-len(data["entries"]) if offset > 7 else offset))}") data = await data.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[-1024:],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() if message: 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: async with aiohttp.ClientSession() as session: channel = self.bot.get_channel(channelid) data = await session.get(f"https://mmolb.com/api/game/{gameid}/live?after={offset}") data = await data.json() basedata = await session.get(f"https://mmolb.com/api/game/{gameid}") basedata = await basedata.json() if basedata["State"] == "Complete": await self.bot.db.execute(f""" DELETE from liveupdate WHERE channelid = {channelid} AND gameid = '{gameid}' """) if len(data["entries"]) > 0: finalstr = "\n>>> " offsetadd = 0 maysend = False for i in data["entries"]: if "scores" in i['message'] or "homers" in i['message']: finalstr += f"{i['message'].replace(", ","\n").replace(". ","\n").replace("", "").replace("", "\n")}\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"{i['message'].replace("", "**").replace("", "**")}\n" 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") async def spotlightsubscriptionworker(self,serverid,channelid,classic,gameid,data): 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() if test is None: channel = self.bot.get_channel(channelid) if data["State"] == "Complete": return 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) """) 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) return async def teamsubscriptionworker(self,serverid,channelid,teamid,classic): try: async with aiohttp.ClientSession() as session: game = await session.get(f"https://mmolb.com/api/game-by-team/{teamid}") game = await game.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 = await session.get(f"https://mmolb.com/api/game/{gameid}") data = await data.json() channel = self.bot.get_channel(channelid) if data["State"] == "Complete": return 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) """) except KeyError: return 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) return 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() 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,return_exceptions=True) await self.bot.db.commit() game = nextcord.Game(f"Broadcasting {len(worklist)} games") await self.bot.change_presence(status=nextcord.Status.online, activity=game) 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() async with aiohttp.ClientSession() as session: game = await session.get("https://mmolb.com/api/spotlight") game = await game.json() gameid = game["game_id"] data = await session.get(f"https://mmolb.com/api/game/{gameid}") data = await data.json() res = await self.bot.db.execute("SELECT serverid,channelid,classic FROM spotlightsubscriptions") res = await res.fetchall() worklist = [spotlightsubscriptionworker(self,serverid,channelid,classic,gameid,data) for [serverid,channelid,classic] in res] await asyncio.gather(*worklist,return_exceptions=True) await self.bot.db.commit() 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) return @tasks.loop(seconds=120.0) async def checkteamsubscriptions(self): try: await self.bot.wait_until_ready() res = await self.bot.db.execute("SELECT serverid,channelid,teamid,classic FROM teamsubscriptions") res = await res.fetchall() worklist = [teamsubscriptionworker(self,serverid,channelid,teamid,classic) for [serverid,channelid,teamid,classic] in res] await asyncio.gather(*worklist,return_exceptions=True) await self.bot.db.commit() 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))