import asyncio import datetime import json import logging import os import sys from json import JSONDecodeError from typing import Union import time import pytz import discord import requests from discord.ext import commands from dotenv import load_dotenv import feedparser from db import DiscordDB from map_events import events APP_NAME = "discord_bot" os.chdir(os.path.abspath(os.path.dirname(sys.argv[0]))) load_dotenv() logger = logging.getLogger(APP_NAME) formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") file_logger = logging.FileHandler(f"./logging.log", "w") file_logger.setLevel(logging.DEBUG) file_logger.setFormatter(formatter) logger.addHandler(file_logger) stream_logger = logging.StreamHandler() stream_logger.setFormatter(formatter) logger.addHandler(stream_logger) os.makedirs("debug", exist_ok=True) DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096) ADMIN_ID = os.getenv("DEFAULT_CHANNEL_ID", 220849530730577920) DB_NAME = os.getenv("DB_NAME", "discord.db") DB = DiscordDB(DB_NAME) UTF_FLAG = { 1: "๐Ÿ‡ท๐Ÿ‡ด", 9: "๐Ÿ‡ง๐Ÿ‡ท", 10: "๐Ÿ‡ฎ๐Ÿ‡น", 11: "๐Ÿ‡ซ๐Ÿ‡ท", 12: "๐Ÿ‡ฉ๐Ÿ‡ช", 13: "๐Ÿ‡ญ๐Ÿ‡บ", 14: "๐Ÿ‡จ๐Ÿ‡ณ", 15: "๐Ÿ‡ช๐Ÿ‡ธ", 23: "๐Ÿ‡จ๐Ÿ‡ฆ", 24: "๐Ÿ‡บ๐Ÿ‡ธ", 26: "๐Ÿ‡ฒ๐Ÿ‡ฝ", 27: "๐Ÿ‡ฆ๐Ÿ‡ท", 28: "๐Ÿ‡ป๐Ÿ‡ช", 29: "๐Ÿ‡ฌ๐Ÿ‡ง", 30: "๐Ÿ‡จ๐Ÿ‡ญ", 31: "๐Ÿ‡ณ๐Ÿ‡ฑ", 32: "๐Ÿ‡ง๐Ÿ‡ช", 33: "๐Ÿ‡ฆ๐Ÿ‡น", 34: "๐Ÿ‡จ๐Ÿ‡ฟ", 35: "๐Ÿ‡ต๐Ÿ‡ฑ", 36: "๐Ÿ‡ธ๐Ÿ‡ฐ", 37: "๐Ÿ‡ณ๐Ÿ‡ด", 38: "๐Ÿ‡ธ๐Ÿ‡ช", 39: "๐Ÿ‡ซ๐Ÿ‡ฎ", 40: "๐Ÿ‡บ๐Ÿ‡ฆ", 41: "๐Ÿ‡ท๐Ÿ‡บ", 42: "๐Ÿ‡ง๐Ÿ‡ฌ", 43: "๐Ÿ‡น๐Ÿ‡ท", 44: "๐Ÿ‡ฌ๐Ÿ‡ท", 45: "๐Ÿ‡ฏ๐Ÿ‡ต", 47: "๐Ÿ‡ฐ๐Ÿ‡ท", 48: "๐Ÿ‡ฎ๐Ÿ‡ณ", 49: "๐Ÿ‡ฎ๐Ÿ‡ฉ", 50: "๐Ÿ‡ฆ๐Ÿ‡บ", 51: "๐Ÿ‡ฟ๐Ÿ‡ฆ", 52: "๐Ÿ‡ฒ๐Ÿ‡ฉ", 53: "๐Ÿ‡ต๐Ÿ‡น", 54: "๐Ÿ‡ฎ๐Ÿ‡ช", 55: "๐Ÿ‡ฉ๐Ÿ‡ฐ", 56: "๐Ÿ‡ฎ๐Ÿ‡ท", 57: "๐Ÿ‡ต๐Ÿ‡ฐ", 58: "๐Ÿ‡ฎ๐Ÿ‡ฑ", 59: "๐Ÿ‡น๐Ÿ‡ญ", 61: "๐Ÿ‡ธ๐Ÿ‡ฎ", 63: "๐Ÿ‡ญ๐Ÿ‡ท", 64: "๐Ÿ‡จ๐Ÿ‡ฑ", 65: "๐Ÿ‡ท๐Ÿ‡ธ", 66: "๐Ÿ‡ฒ๐Ÿ‡พ", 67: "๐Ÿ‡ต๐Ÿ‡ญ", 68: "๐Ÿ‡ธ๐Ÿ‡ฌ", 69: "๐Ÿ‡ง๐Ÿ‡ฆ", 70: "๐Ÿ‡ช๐Ÿ‡ช", 71: "๐Ÿ‡ฑ๐Ÿ‡ป", 72: "๐Ÿ‡ฑ๐Ÿ‡น", 73: "๐Ÿ‡ฐ๐Ÿ‡ต", 74: "๐Ÿ‡บ๐Ÿ‡พ", 75: "๐Ÿ‡ต๐Ÿ‡พ", 76: "๐Ÿ‡ง๐Ÿ‡ด", 77: "๐Ÿ‡ต๐Ÿ‡ช", 78: "๐Ÿ‡จ๐Ÿ‡ด", 79: "๐Ÿ‡ฒ๐Ÿ‡ฐ", 80: "๐Ÿ‡ฒ๐Ÿ‡ช", 81: "๐Ÿ‡น๐Ÿ‡ผ", 82: "๐Ÿ‡จ๐Ÿ‡พ", 83: "๐Ÿ‡ง๐Ÿ‡พ", 84: "๐Ÿ‡ณ๐Ÿ‡ฟ", 164: "๐Ÿ‡ธ๐Ÿ‡ฆ", 165: "๐Ÿ‡ช๐Ÿ‡ฌ", 166: "๐Ÿ‡ฆ๐Ÿ‡ช", 167: "๐Ÿ‡ฆ๐Ÿ‡ฑ", 168: "๐Ÿ‡ฌ๐Ÿ‡ช", 169: "๐Ÿ‡ฆ๐Ÿ‡ฒ", 170: "๐Ÿ‡ณ๐Ÿ‡ฌ", 171: "๐Ÿ‡จ๐Ÿ‡บ", } FLAGS = { 1: "flag_ro", 9: "flag_br", 10: "flag_it", 11: "flag_fr", 12: "flag_de", 13: "flag_hu", 14: "flag_cn", 15: "flag_es", 23: "flag_ca", 24: "flag_us", 26: "flag_mx", 27: "flag_ar", 28: "flag_ve", 29: "flag_gb", 30: "flag_ch", 31: "flag_nl", 32: "flag_be", 33: "flag_at", 34: "flag_cz", 35: "flag_pl", 36: "flag_sk", 37: "flag_no", 38: "flag_se", 39: "flag_fi", 40: "flag_ua", 41: "flag_ru", 42: "flag_bg", 43: "flag_tr", 44: "flag_gr", 45: "flag_jp", 47: "flag_kr", 48: "flag_in", 49: "flag_id", 50: "flag_au", 51: "flag_za", 52: "flag_md", 53: "flag_pt", 54: "flag_ie", 55: "flag_de", 56: "flag_ir", 57: "flag_pk", 58: "flag_il", 59: "flag_th", 61: "flag_si", 63: "flag_hr", 64: "flag_cl", 65: "flag_rs", 66: "flag_my", 67: "flag_ph", 68: "flag_sg", 69: "flag_ba", 70: "flag_ee", 71: "flag_lv", 72: "flag_lt", 73: "flag_kp", 74: "flag_uy", 75: "flag_py", 76: "flag_bo", 77: "flag_pe", 78: "flag_co", 79: "flag_mk", 80: "flag_me", 81: "flag_tw", 82: "flag_cy", 83: "flag_by", 84: "flag_nz", 164: "flag_sa", 165: "flag_eg", 166: "flag_ae", 167: "flag_al", 168: "flag_ge", 169: "flag_am", 170: "flag_ng", 171: "flag_cu", } MENTION_MAPPING = {1: "D1", 2: "D2", 3: "D3", 4: "D4", 11: "Air"} __last_battle_response = None __last_battle_update_timestamp = 0 def timestamp_to_datetime(timestamp: int) -> datetime.datetime: return datetime.datetime.fromtimestamp(timestamp) def s_to_human(seconds: Union[int, float]) -> str: seconds = int(seconds) h = seconds // 3600 m = (seconds - (h * 3600)) // 60 s = seconds % 60 return f"{h:01d}:{m:02d}:{s:02d}" def get_battle_page(): global __last_battle_update_timestamp, __last_battle_response if int(datetime.datetime.now().timestamp()) >= __last_battle_update_timestamp + 60: dt = datetime.datetime.now() r = requests.get("https://www.erepublik.com/en/military/campaignsJson/list") try: __last_battle_response = r.json() except JSONDecodeError: logger.warning("Received non json response from erep.lv/battles.json!") return get_battle_page() __last_battle_update_timestamp = __last_battle_response.get("last_updated", int(dt.timestamp())) return __last_battle_response class MyClient(discord.Client): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # create the background task and run it in the background self.last_event_timestamp = self.timestamp - 43200 self.bg_task = self.loop.create_task(self.report_epics()) self.bg_rss_task = self.loop.create_task(self.report_latvian_events()) @property def timestamp(self): return int(time.time()) async def on_ready(self): logger.debug("Client running") logger.debug("------") async def on_error(self, event_method, *args, **kwargs): logger.warning(f"Ignoring exception in {event_method}") async def report_latvian_events(self): await self.wait_until_ready() while not self.is_closed(): try: for entry in reversed(feedparser.parse(f"https://www.erepublik.com/en/main/news/military/all/Latvia/0/rss").entries): entry_ts = time.mktime(entry["published_parsed"]) if entry_ts > self.last_event_timestamp: msg = entry["summary"] title = "" for kind in events: match = kind.regex.search(msg) if match: text = kind.format.format(**dict(match.groupdict(), **{"current_country": "Latvia"})) title = kind.name break else: has_unknown = True title = "Unable to parse" logger.warning(f"Unable to parse: {str(entry)}") text = msg self.last_event_timestamp = entry_ts 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="eLatvia", icon_url="https://www.erepublik.com/images/flags/L/Latvia.gif") embed.set_thumbnail(url="https://www.erepublik.net/images/modules/homepage/logo.png") embed.set_footer(text=f"{entry_datetime.strftime('%F %T')} (eRepublik time)") await self.get_channel(DEFAULT_CHANNEL_ID).send(embed=embed) await asyncio.sleep((self.timestamp // 300 + 1) * 300 - self.timestamp) except Exception as e: logger.error("eRepublik event reader ran into a problem!", exc_info=e) try: with open(f"debug/{self.timestamp}.rss", "w") as f: f.write(r.text) except NameError: logger.error("There was no Response object!", exc_info=e) await asyncio.sleep(10) async def report_epics(self): await self.wait_until_ready() roles = [role for role in self.get_guild(300297668553605131).roles if role.name in MENTION_MAPPING.values()] role_mapping = {role.name: role.mention for role in roles} while not self.is_closed(): try: r = get_battle_page() if not isinstance(r.get("battles"), dict): sleep_seconds = r.get("last_updated") + 60 - self.timestamp await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0) continue for bid, battle in r.get("battles", {}).items(): for div in battle.get("div", {}).values(): if div.get("epic") > 1 and not DB.get_epic(div.get("id")): with open(f"debug/{self.timestamp}.json", "w") as f: json.dump(r, f) invader_id = battle["inv"]["id"] defender_id = battle["def"]["id"] embed = discord.Embed( title=" ".join(div.get("intensity_scale").split("_")).title(), url=f"https://www.erepublik.com/en/military/battlefield/{battle['id']}", description=f"Epic battle {UTF_FLAG[invader_id]} vs {UTF_FLAG[defender_id]}!", ) embed.set_footer(f"Round time {s_to_human(self.timestamp - battle['start'])}") await self.get_channel(DEFAULT_CHANNEL_ID).send(f"{role_mapping[MENTION_MAPPING[div['div']]]}", embed=embed) DB.add_epic(div.get("id")) sleep_seconds = r.get("last_updated") + 60 - self.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/{self.timestamp}.json", "w") as f: f.write(r.text) 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") loop = asyncio.get_event_loop() client = MyClient() bot = commands.Bot(command_prefix="!") @bot.event async def on_ready(): logger.debug("Bot loaded") # print(bot.user.name) # print(bot.user.id) logger.debug("------") @bot.command() async def exit(ctx): if ctx.author.id == ADMIN_ID: await ctx.send(f"{ctx.author.mention} Bye!") sys.exit(0) else: await ctx.send(f"Labs mฤ“ฤฃinฤjums! Mani nogalinฤt var tikai <@{ADMIN_ID}>") def main(): global loop loop.create_task(bot.start(DISCORD_TOKEN)) loop.create_task(client.start(DISCORD_TOKEN)) loop.run_forever() if __name__ == "__main__": main()