import asyncio import datetime import time import discord import feedparser import pytz import requests from erepublik.constants import COUNTRIES from dbot.base import ADMIN_ID, DB, DEFAULT_CHANNEL_ID, LOOP, PRODUCTION, logger from dbot.constants import events from dbot.utils import check_battles, get_battle_page, timestamp class DiscordClient(discord.Client): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.last_event_timestamp = timestamp() async def on_ready(self): logger.info("Client running") logger.info("------") # create the background task and run it in the background self.bg_task = self.loop.create_task(self.report_battle_events()) self.bg_rss_task = self.loop.create_task(self.report_rss_events()) async def on_error(self, event_method, *args, **kwargs): logger.warning(f"Ignoring exception in {event_method}") async def send_msg(self, channel_id, *args, **kwargs): if PRODUCTION: return self.get_channel(channel_id).send(*args, **kwargs) else: return logger.debug(f"Sending message to: {channel_id}\nArgs: {args}\nKwargs{kwargs}") async def report_rss_events(self): await self.wait_until_ready() feed_response = None while not self.is_closed(): try: for country in COUNTRIES.values(): latest_ts = DB.get_rss_feed_timestamp(country.id) rss_link = f"https://www.erepublik.com/en/main/news/military/all/{country.link}/1/rss" feed_response = requests.get(rss_link) feed_response.raise_for_status() for entry in reversed(feedparser.parse(feed_response.text).entries): entry_ts = time.mktime(entry["published_parsed"]) entry_link = entry["link"] # Check if event timestamp is after latest processed event for country if entry_ts > latest_ts: DB.set_rss_feed_timestamp(country.id, entry_ts) title = text = "" msg = entry["summary"] dont_send = False for kind in events: match = kind.regex.search(msg) if match: values = match.groupdict() # Special case for Dictator/Liberation wars if "invader" in values and not values["invader"]: values["invader"] = values["defender"] # Special case for resource concession if "link" in values: __link = values["link"] entry_link = __link if __link.startswith("http") else f"https://www.erepublik.com{__link}" logger.debug(kind.format.format(**dict(match.groupdict(), **{"current_country": country.name}))) logger.debug(entry_link) is_latvia = country.id == 71 has_latvia = any("Latvia" in v for v in values.values()) if is_latvia or has_latvia: text = kind.format.format(**dict(match.groupdict(), **{"current_country": country.name})) title = kind.name else: dont_send = True break else: logger.warning(f"Unable to parse: {str(entry)}") continue if dont_send: continue entry_datetime = datetime.datetime.fromtimestamp(entry_ts, pytz.timezone("US/Pacific")) embed = discord.Embed(title=title, url=entry_link, description=text) embed.set_author(name=country.name, icon_url=f"https://www.erepublik.com/images/flags/L/{country.link}.gif") embed.set_footer(text=f"{entry_datetime.strftime('%F %T')} (eRepublik time)") logger.debug(f"Message sent: {text}") for channel_id in DB.get_kind_notification_channel_ids("events"): await self.get_channel(channel_id).send(embed=embed) except Exception as e: logger.error("eRepublik event reader ran into a problem!", exc_info=e) try: with open(f"debug/{timestamp()}.rss", "w") as f: f.write(feed_response.text) except (NameError, AttributeError): logger.error("There was no Response object!", exc_info=e) finally: await asyncio.sleep((timestamp() // 300 + 1) * 300 - timestamp()) async def report_battle_events(self): await self.wait_until_ready() while not self.is_closed(): try: r = get_battle_page() if not isinstance(r.get("battles"), dict): sleep_seconds = r.get("last_updated") + 60 - timestamp() await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0) continue desc = "'Empty' medals are being guessed based on the division wall. Expect false-positives!" empty_divisions = { 1: discord.Embed(title="Possibly empty **__last-minute__ D1** medals", description=desc), 2: discord.Embed(title="Possibly empty **__last-minute__ D2** medals", description=desc), 3: discord.Embed(title="Possibly empty **__last-minute__ D3** medals", description=desc), 4: discord.Embed(title="Possibly empty **__last-minute__ D4** medals", description=desc), 11: discord.Embed(title="Possibly empty **__last-minute__ Air** medals", description=desc), } for kind, div, data in check_battles(r.get("battles")): if kind == "epic" and not DB.check_epic(data["div_id"]): embed = discord.Embed( title=" ".join(data["extra"]["intensity_scale"].split("_")).title(), url=data["url"], description=f"Epic battle {' vs '.join(data['sides'])}!\nBattle for {data['region']}, Round {data['zone_id']}", ) embed.set_footer(text=f"Round time {data['round_time']}") for channel_id in DB.get_kind_notification_channel_ids("epic"): role_id = DB.get_role_id_for_channel_division(kind="epic", channel_id=channel_id, division=div) logger.info(f"Sending epic d{div} message to {channel_id}, role to mention {role_id=}") if role_id: await self.get_channel(channel_id).send(f"<@&{role_id}> epic battle detected!", embed=embed) else: await self.get_channel(channel_id).send(embed=embed) DB.add_epic(data["div_id"]) logger.info(f"{data['div_id']} added to notified list") if kind == "empty" and data["round_time_s"] >= 85 * 60 and not DB.check_empty_medal(data["div_id"]): if len(empty_divisions[div]) < 10: empty_divisions[div].add_field( name=f"**Battle for {data['region']} {' '.join(data['sides'])}**", value=f"[R{data['zone_id']} | Time {data['round_time']}]({data['url']})", ) DB.add_empty_medal(data["div_id"]) for d, e in empty_divisions.items(): if e.fields: for channel_id in DB.get_kind_notification_channel_ids("empty"): logger.info(f"Sending empty {d} message to {channel_id} with {len(e.fields)} battles: {e.fields}") if role_id := DB.get_role_id_for_channel_division(kind="empty", channel_id=channel_id, division=d): await self.get_channel(channel_id).send(f"<@&{role_id}> empty medals in late rounds!", embed=e) else: await self.get_channel(channel_id).send(embed=e) sleep_seconds = r.get("last_updated") + 60 - timestamp() await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0) except Exception as e: logger.error("Discord bot's eRepublik epic watcher died!", exc_info=e) try: with open(f"debug/{timestamp()}.json", "w") as f: f.write(f"{r}") except NameError: logger.error("There was no Response object!", exc_info=e) await asyncio.sleep(10) await self.get_channel(DEFAULT_CHANNEL_ID).send(f"<@{ADMIN_ID}> I've stopped, please restart") client = DiscordClient(loop=LOOP)