Report Feed events on Discord

This commit is contained in:
KEriks 2021-08-03 23:39:40 +03:00
parent b36f2738aa
commit 16a5e88232
3 changed files with 376 additions and 23 deletions

View File

@ -6,39 +6,119 @@ import os
import sys import sys
from json import JSONDecodeError from json import JSONDecodeError
from typing import Union from typing import Union
import time
import pytz
import discord import discord
import requests import requests
from discord.ext import commands from discord.ext import commands
from dotenv import load_dotenv from dotenv import load_dotenv
import feedparser
from db import DiscordDB from db import DiscordDB
from map_events import events
APP_NAME = "discord_bot" APP_NAME = "discord_bot"
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0]))) os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
load_dotenv() load_dotenv()
logging.basicConfig(level=logging.WARNING, filename="logging.log", format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
logger.setLevel(logging.DEBUG) formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
logger.propagate = False
fh = logging.FileHandler(f"./logging.log", "w") file_logger = logging.FileHandler(f"./logging.log", "w")
fh.setLevel(logging.DEBUG) file_logger.setLevel(logging.DEBUG)
logger.addHandler(fh) file_logger.setFormatter(formatter)
keep_fds = [fh.stream.fileno()] logger.addHandler(file_logger)
stream_logger = logging.StreamHandler()
stream_logger.setFormatter(formatter)
logger.addHandler(stream_logger)
os.makedirs("debug", exist_ok=True) os.makedirs("debug", exist_ok=True)
pidfile = "pid"
with open(pidfile, "w") as f:
f.write(str(os.getpid()))
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096) DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096)
ADMIN_ID = os.getenv("DEFAULT_CHANNEL_ID", 220849530730577920) ADMIN_ID = os.getenv("DEFAULT_CHANNEL_ID", 220849530730577920)
DB_NAME = os.getenv("DB_NAME", "discord.db") DB_NAME = os.getenv("DB_NAME", "discord.db")
DB = DiscordDB(DB_NAME) 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 = { FLAGS = {
1: "flag_ro", 1: "flag_ro",
9: "flag_br", 9: "flag_br",
@ -152,19 +232,62 @@ class MyClient(discord.Client):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# create the background task and run it in the background # 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_task = self.loop.create_task(self.report_epics())
self.bg_rss_task = self.loop.create_task(self.report_latvian_events())
@property @property
def timestamp(self): def timestamp(self):
return int(datetime.datetime.now().timestamp()) return int(time.time())
async def on_ready(self): async def on_ready(self):
print("Client running") logger.debug("Client running")
print("------") logger.debug("------")
async def on_error(self, event_method, *args, **kwargs): async def on_error(self, event_method, *args, **kwargs):
logger.warning(f"Ignoring exception in {event_method}") 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): async def report_epics(self):
await self.wait_until_ready() await self.wait_until_ready()
roles = [role for role in self.get_guild(300297668553605131).roles if role.name in MENTION_MAPPING.values()] roles = [role for role in self.get_guild(300297668553605131).roles if role.name in MENTION_MAPPING.values()]
@ -183,11 +306,13 @@ class MyClient(discord.Client):
json.dump(r, f) json.dump(r, f)
invader_id = battle["inv"]["id"] invader_id = battle["inv"]["id"]
defender_id = battle["def"]["id"] defender_id = battle["def"]["id"]
await self.get_channel(DEFAULT_CHANNEL_ID).send( embed = discord.Embed(
f"{role_mapping[MENTION_MAPPING[div['div']]]} Epic battle :{FLAGS[invader_id]}: vs :{FLAGS[defender_id]}:! " title=" ".join(div.get("intensity_scale").split("_")).title(),
f"Round time {s_to_human(self.timestamp - battle['start'])}\n" url=f"https://www.erepublik.com/en/military/battlefield/{battle['id']}",
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")) DB.add_epic(div.get("id"))
sleep_seconds = r.get("last_updated") + 60 - self.timestamp sleep_seconds = r.get("last_updated") + 60 - self.timestamp
@ -210,17 +335,17 @@ bot = commands.Bot(command_prefix="!")
@bot.event @bot.event
async def on_ready(): async def on_ready():
print("Bot loaded") logger.debug("Bot loaded")
# print(bot.user.name) # print(bot.user.name)
# print(bot.user.id) # print(bot.user.id)
print("------") logger.debug("------")
@bot.command() @bot.command()
async def kill(ctx): async def exit(ctx):
if ctx.author.id == ADMIN_ID: if ctx.author.id == ADMIN_ID:
await ctx.send(f"{ctx.author.mention} Bye!") await ctx.send(f"{ctx.author.mention} Bye!")
sys.exit(1) sys.exit(0)
else: else:
await ctx.send(f"Labs mēģinājums! Mani nogalināt var tikai <@{ADMIN_ID}>") await ctx.send(f"Labs mēģinājums! Mani nogalināt var tikai <@{ADMIN_ID}>")

225
map_events.py Normal file
View File

@ -0,0 +1,225 @@
import re
from typing import NamedTuple
import feedparser
from erepublik.constants import COUNTRIES
region = "[\w\(\)\- ']+"
country = "(Resistance force of )?[\w\(\)\- ]+"
citizen = "[\w\(\)\-\. \d]+"
class EventKind(NamedTuple):
name: str
regex: re.Pattern
format: str
events = [
EventKind(
"Region attacked",
re.compile(rf"(?P<invader>{country}) attacked (?P<region>{region}), (?P<defender>{country})"),
"{invader} attacked {defender} ({region})",
),
EventKind(
"Region secured",
re.compile(rf"(?P<region>{region}) was secured by (?P<defender>{country}) in the war versus ?(?P<invader>{country})?"),
"{defender} defended {invader}'s attack ({region})",
),
EventKind(
"Region conquered",
re.compile(rf"(?P<region>{region}) was conquered by (?P<invader>{country}) in the war versus ?(?P<defender>{country})?"),
"{invader} conquered {region} from {defender}",
),
EventKind(
"War approved",
re.compile(rf"(?P<invader>{country}) declared war on (?P<defender>{country})"),
"{invader} declared war against {defender}",
),
EventKind(
"War declared",
re.compile(rf"President of (?P<invader>{country}) proposed a war declaration against (?P<defender>{country})"),
"{invader} proposed a war declaration on {defender}",
),
EventKind(
"War rejected",
re.compile(rf"The proposal for declaring war against (?P<defender>{country}) was rejected."),
"{current_country} rejected war declaration on {defender}",
),
EventKind(
"MPP proposed",
re.compile(rf"President of (?P<country>{country}) proposed an alliance with (?P<partner>{country})"),
"{country} proposed MPP with {partner}",
),
EventKind(
"MPP approved",
re.compile(rf"(?P<country>{country}) signed an alliance with (?P<partner>{country})"),
"{country} signed a MPP with {partner}",
),
EventKind(
"MPP rejected",
re.compile(rf"The alliance between (?P<country>{country}) and (?P<partner>{country}) was rejected"),
"MPP between {country} and {partner} was rejected",
),
EventKind(
"Airstrike proposed",
re.compile(rf"President of (?P<invader>{country}) proposed an airstrike against (?P<defender>{country})"),
"{invader} proposed an airstrike against {defender}",
),
EventKind(
"Airstrike approved",
re.compile(rf"(?P<invader>{country}) prepares an airstrike on (?P<defender>{country})"),
"{invader} approved an airstrike against {defender}",
),
EventKind(
"Airstrike rejected",
re.compile(rf"The airstrike on (?P<defender>{country}) was rejected"),
"{current_country} rejected the airstrike against {defender}",
),
EventKind(
"NE proposed",
re.compile(rf"(?P<invader>{country}) has declared (?P<defender>{country}) as a Natural Enemy"),
"{invader} proposed Natural Enemy declaration against {defender}",
),
EventKind(
"NE approved",
re.compile(rf"(?P<defender>{country}) has been proposed as Natural Enemy"),
"{current_country} declared {defender} as Natural Enemy",
),
EventKind(
"NE rejected",
re.compile(rf"(?P<defender>{country}) as new Natural Enemy proposal has been rejected"),
"{current_country} rejected {defender} as Natural Enemy",
),
EventKind(
"NE stopped",
re.compile(rf"(?P<defender>{country}) is no longer a Natural Enemy for (?P<invader>{country})"),
"{invader} removed Natural Enemy from {defender}",
),
EventKind(
"NE cleared", re.compile(rf"(?P<country>{country}) no longer has a Natural Enemy"), "{country} no longer has a Natural Enemy"
),
EventKind("NE reset", re.compile("No Natural Enemy law has been proposed."), "{current_country} has proposed to clear Natural Enemy"),
EventKind(
"Peace proposal",
re.compile(rf"President of (?P<defender>{country}) proposed a peace in the war against (?P<invader>{country})"),
"{defender} proposed peace against {invader}",
),
EventKind(
"Peace proposal",
re.compile(rf"(?P<defender>{country}) proposed peace in the war against (?P<invader>{country})"),
"{defender} proposed peace against {invader}",
),
EventKind(
"Peace approved",
re.compile(rf"(?P<invader>{country}) signed a peace treaty with (?P<defender>{country})"),
"{invader} and {defender} is not in peace",
),
EventKind(
"Peace rejected",
re.compile(rf"The proposed peace treaty between (?P<defender>{country}) and (?P<invader>{country}) was rejected"),
"{defender} and {invader} did not sign a peace treaty",
),
EventKind(
"Embargo proposed",
re.compile(rf"President of (?P<major>{country}) proposed to stop the trade with (?P<minor>{country})"),
"{major} proposed trade embargo against {minor}",
),
EventKind(
"Embargo approved",
re.compile(rf"(?P<major>{country}) stopped trading with (?P<minor>{country})"),
"{major} declared trade ambargo against {minor}",
),
EventKind(
"Donation proposed",
re.compile(rf"A congress donation to (?P<org>{citizen}) was proposed"),
"{current_country} proposed a donation to {org}",
),
EventKind(
"Donation approved",
re.compile(rf"(?P<country>{country}) made a donation to (?P<org>{citizen})"),
"{current_country} approved a donation to {org}",
),
EventKind(
"Donation rejected",
re.compile(rf"The proposal for a congress donation to (?P<org>{citizen}) was rejected"),
"{current_country} rejected a donation to {org}",
),
EventKind(
"RW started",
re.compile(rf"A resistance has started in (?P<region>{region})"),
"Resistance war was opened in {region} ({current_country})",
),
EventKind(
"Res Concession",
re.compile(
rf'A Resource Concession law to //www.erepublik.com<b>(?P<target>{country})</b> <a href="https://www.erepublik.com/en/main/law/(?P<source>{country})/\d+">has been proposed</a>'
),
"{source} proposed resource concession to {target}",
),
EventKind(
"Res Concession",
re.compile(
rf'A Resource Concession law to //www.erepublik.com<b>(?P<target>{country})</b> <a href="https://www.erepublik.com/en/main/law/(?P<source>{country})/\d+">has been approved'
),
"{source} approved resource concession to {target}",
),
EventKind(
"CP impeachment",
re.compile(rf"A president impeachment against (?P<cp>{citizen}) was proposed"),
"Impeachment against {cp} president of {current_country} was proposed",
),
EventKind(
"CP impeachment",
re.compile("The president impeachment proposal has been rejected"),
"Impeachment against president of {current_country} was rejected",
),
EventKind("Minimum Wage", re.compile("A new minimum wage was proposed"), "A new minimum wage in {current_country} was proposed"),
EventKind(
"Minimum Wage",
re.compile("The proposal for a minimum wage change was rejected"),
"The new minimum wage proposal in {current_country} was rejected",
),
EventKind("WorkTax", re.compile(rf"(?P<country>{country}) now has a new Work Tax"), "{country} has new Work Tax"),
EventKind("Product Tax", re.compile(rf"Taxes for (?P<product>[\w ]+) changed"), "{current_country} changed taxes for {product}"),
EventKind(
"Product Tax",
re.compile(rf"Tax proposal of tax changes for (?P<product>[\w ]+) were rejected"),
"{current_country} rejected new taxes for {product}",
),
EventKind(
"Product Tax", re.compile(rf"New taxes for (?P<product>[\w ]+) were proposed"), "{current_country} proposed new taxes for {product}"
),
]
def main(country):
page = 1
has_unknown = False
while True:
for entry in feedparser.parse(f"https://www.erepublik.com/en/main/news/military/all/{country}/{page}/rss").entries:
msg = entry["summary"]
for kind in events:
match = kind.regex.search(msg)
if match:
text = kind.format.format(**dict(match.groupdict(), **{"current_country": country}))
print(f"{kind.name:<20} -||- {text:<80} -||- {entry['link']:<64} -||- {entry['published']}")
break
else:
has_unknown = True
break
else:
page += 1
if page > 5:
break
continue
break
if has_unknown:
print(page, entry)
raise ValueError(msg)
if __name__ == "__main__":
for c in sorted(COUNTRIES.values(), key=lambda _c: _c.id):
if c.id == 71:
main(c.link)
print("Finished", c)

View File

@ -1,4 +1,7 @@
discord.py==1.7.3 discord.py==1.7.3
requests==2.26.0 requests==2.26.0
python-dotenv==0.19.0 python-dotenv==0.19.0
sqlite_utils==3.13 sqlite_utils==3.14
feedparser==6.0.8
pytz==2021.1
erepublik