257 lines
11 KiB
Python
257 lines
11 KiB
Python
import asyncio
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
from json import JSONDecodeError
|
|
from typing import Union
|
|
|
|
import discord
|
|
import feedparser
|
|
import pytz
|
|
import requests
|
|
from constants import UTF_FLAG, events
|
|
from db import DiscordDB
|
|
from discord.ext import commands
|
|
from erepublik.constants import COUNTRIES
|
|
|
|
APP_NAME = "discord_bot"
|
|
|
|
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
|
os.makedirs("debug", exist_ok=True)
|
|
|
|
logger = logging.getLogger(APP_NAME)
|
|
logger.setLevel(logging.DEBUG)
|
|
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
|
|
file_logger = logging.FileHandler("debug/logging.log", "w")
|
|
file_logger.setLevel(logging.WARNING)
|
|
file_logger.setFormatter(formatter)
|
|
logger.addHandler(file_logger)
|
|
|
|
stream_logger = logging.StreamHandler()
|
|
stream_logger.setLevel(logging.DEBUG)
|
|
stream_logger.setFormatter(formatter)
|
|
logger.addHandler(stream_logger)
|
|
|
|
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
|
DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096)
|
|
ADMIN_ID = os.getenv("ADMIN_ID", 220849530730577920)
|
|
DB_NAME = os.getenv("DB_NAME", "discord.db")
|
|
PRODUCTION = bool(os.getenv("PRODUCTION"))
|
|
DB = DiscordDB(DB_NAME)
|
|
|
|
if PRODUCTION:
|
|
logger.warning("Production mode enabled!")
|
|
logger.setLevel(logging.INFO)
|
|
_ts = int(time.time())
|
|
for c_id in COUNTRIES.keys():
|
|
DB.set_rss_feed_timestamp(c_id, _ts)
|
|
del _ts
|
|
|
|
logger.debug(f"Active configs:\nDISCORD_TOKEN='{DISCORD_TOKEN}'\nDEFAULT_CHANNEL_ID='{DEFAULT_CHANNEL_ID}'\nADMIN_ID='{ADMIN_ID}'\nDB_NAME='{DB_NAME}'")
|
|
|
|
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
|
|
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.info("Client running")
|
|
logger.info("------")
|
|
|
|
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_latvian_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})))
|
|
|
|
if country.id == 71 or any("Latvia" in v for v in values.values()):
|
|
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_thumbnail(url="https://www.erepublik.net/images/modules/homepage/logo.png")
|
|
embed.set_footer(text=f"{entry_datetime.strftime('%F %T')} (eRepublik time)")
|
|
|
|
logger.debug(f"Message sent: {text}")
|
|
await self.get_channel(DEFAULT_CHANNEL_ID).send(embed=embed)
|
|
# 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(feed_response.text)
|
|
except (NameError, AttributeError):
|
|
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(text=f"Round time {s_to_human(self.timestamp - battle['start'])}")
|
|
logger.debug(
|
|
f"Epic battle {UTF_FLAG[invader_id]} vs {UTF_FLAG[defender_id]}! "
|
|
f"Round time {s_to_human(self.timestamp - battle['start'])} "
|
|
f"https://www.erepublik.com/en/military/battlefield/{battle['id']}"
|
|
)
|
|
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.info("Bot loaded")
|
|
# print(bot.user.name)
|
|
# print(bot.user.id)
|
|
logger.info("------")
|
|
|
|
|
|
@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
|
|
logger.info("Starting Bot loop")
|
|
loop.create_task(bot.start(DISCORD_TOKEN))
|
|
logger.info("Starting Client loop")
|
|
loop.create_task(client.start(DISCORD_TOKEN))
|
|
loop.run_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|