Report Feed events on Discord
This commit is contained in:
parent
b36f2738aa
commit
16a5e88232
169
discord_bot.py
169
discord_bot.py
@ -6,39 +6,119 @@ 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()
|
||||
logging.basicConfig(level=logging.WARNING, filename="logging.log", format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.propagate = False
|
||||
fh = logging.FileHandler(f"./logging.log", "w")
|
||||
fh.setLevel(logging.DEBUG)
|
||||
logger.addHandler(fh)
|
||||
keep_fds = [fh.stream.fileno()]
|
||||
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)
|
||||
|
||||
pidfile = "pid"
|
||||
with open(pidfile, "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
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",
|
||||
@ -152,19 +232,62 @@ 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(datetime.datetime.now().timestamp())
|
||||
return int(time.time())
|
||||
|
||||
async def on_ready(self):
|
||||
print("Client running")
|
||||
print("------")
|
||||
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()]
|
||||
@ -183,11 +306,13 @@ class MyClient(discord.Client):
|
||||
json.dump(r, f)
|
||||
invader_id = battle["inv"]["id"]
|
||||
defender_id = battle["def"]["id"]
|
||||
await self.get_channel(DEFAULT_CHANNEL_ID).send(
|
||||
f"{role_mapping[MENTION_MAPPING[div['div']]]} Epic battle :{FLAGS[invader_id]}: vs :{FLAGS[defender_id]}:! "
|
||||
f"Round time {s_to_human(self.timestamp - battle['start'])}\n"
|
||||
f"https://www.erepublik.com/en/military/battlefield/{battle['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
|
||||
@ -210,17 +335,17 @@ bot = commands.Bot(command_prefix="!")
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print("Bot loaded")
|
||||
logger.debug("Bot loaded")
|
||||
# print(bot.user.name)
|
||||
# print(bot.user.id)
|
||||
print("------")
|
||||
logger.debug("------")
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def kill(ctx):
|
||||
async def exit(ctx):
|
||||
if ctx.author.id == ADMIN_ID:
|
||||
await ctx.send(f"{ctx.author.mention} Bye!")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
else:
|
||||
await ctx.send(f"Labs mēģinājums! Mani nogalināt var tikai <@{ADMIN_ID}>")
|
||||
|
||||
|
225
map_events.py
Normal file
225
map_events.py
Normal 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)
|
@ -1,4 +1,7 @@
|
||||
discord.py==1.7.3
|
||||
requests==2.26.0
|
||||
python-dotenv==0.19.0
|
||||
sqlite_utils==3.13
|
||||
sqlite_utils==3.14
|
||||
feedparser==6.0.8
|
||||
pytz==2021.1
|
||||
erepublik
|
||||
|
Loading…
x
Reference in New Issue
Block a user