Refactoring

This commit is contained in:
KEriks 2021-08-04 13:57:16 +03:00 committed by Eriks K
parent 16a5e88232
commit b8b2093c73
13 changed files with 495 additions and 431 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 180
tab_width = 4
[{*.bash,*.sh,*.zsh}]
indent_size = 2
tab_width = 2

View File

@ -8,6 +8,8 @@ RUN groupadd -g 1000 discordbot \
USER discordbot
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
COPY ./run.sh /run.sh
RUN pip install -r requirements.txt && chmod +x /run.sh
CMD python discord_bot.py
#CMD python discord_bot.py
ENTRYPOINT ['/run.sh', 'docker']

255
dbot/constants.py Normal file
View File

@ -0,0 +1,255 @@
import re
from typing import NamedTuple
from erepublik.constants import COUNTRIES
__all__ = ["events", COUNTRIES, "FLAGS", "UTF_FLAG"]
region = r"[\w\(\)\- ']+"
country = r"(Resistance force of )?[\w\(\)\- ]+"
citizen = r"[\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 declared", re.compile(rf"(?P<invader>{country}) declared war on (?P<defender>{country})"), "{invader} declared war against {defender}"),
EventKind(
"War declaration",
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> "
+ rf'<a href="(?P<link>((https?:)?//www\.erepublik\.com)?/en/main/law/(?P<source>{country}/\d+))">has been (?P<result>.*?)</a>'
),
"Resource Concession law between {current_country} and {target} has been {result}",
),
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("WorkTax", re.compile("A new Work Tax was proposed"), "{country} proposed a new Work Tax"),
EventKind("WorkTax", re.compile("The proposal for a new Work Tax was rejected"), "{country} rejected new Work Tax"),
EventKind("Product Tax", re.compile(r"Taxes for (?P<product>[\w ]+) changed"), "{current_country} changed taxes for {product}"),
EventKind("Product Tax", re.compile(r"Tax proposal of tax changes for (?P<product>[\w ]+) were rejected"), "{current_country} rejected new taxes for {product}"),
EventKind("Product Tax", re.compile(r"New taxes for (?P<product>[\w ]+) were proposed"), "{current_country} proposed new taxes for {product}"),
]
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",
}

View File

@ -23,11 +23,15 @@ class DiscordDB:
if "epic" not in self._db.table_names():
self._db.create_table("epic", {"id": int, "fake": bool}, pk="id", not_null={"id"}, defaults={"fake": False})
if "rss_feed" not in self._db.table_names():
self._db.create_table("rss_feed", {"id": int, "timestamp": float}, pk="id", not_null={"id", "timestamp"})
self._db.vacuum()
self.member = self._db.table("member")
self.player = self._db.table("player")
self.epic = self._db.table("epic")
self.rss_feed = self._db.table("rss_feed")
# Player methods
@ -133,3 +137,15 @@ class DiscordDB:
self.epic.insert({"id": division_id})
return True
return False
def get_rss_feed_timestamp(self, country_id: int) -> float:
try:
return self.rss_feed.get(country_id)["timestamp"]
except NotFoundError:
return 0
def set_rss_feed_timestamp(self, country_id: int, timestamp: float):
if self.get_rss_feed_timestamp(country_id):
self.rss_feed.update(country_id, {"timestamp": timestamp})
else:
self.rss_feed.insert({"id": country_id, "timestamp": timestamp})

View File

@ -4,197 +4,53 @@ import json
import logging
import os
import sys
import time
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
import pytz
import requests
from constants import UTF_FLAG, events
from db import DiscordDB
from map_events import events
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])))
load_dotenv()
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(f"./logging.log", "w")
file_logger.setLevel(logging.DEBUG)
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)
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)
ADMIN_ID = os.getenv("ADMIN_ID", 220849530730577920)
DB_NAME = os.getenv("DB_NAME", "discord.db")
PRODUCTION = bool(os.getenv("PRODUCTION"))
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",
}
if PRODUCTION:
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"}
@ -232,7 +88,7 @@ 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.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())
@ -241,50 +97,81 @@ class MyClient(discord.Client):
return int(time.time())
async def on_ready(self):
logger.debug("Client running")
logger.debug("------")
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 entry in reversed(feedparser.parse(f"https://www.erepublik.com/en/main/news/military/all/Latvia/0/rss").entries):
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"]
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
# 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})))
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)")
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
await self.get_channel(DEFAULT_CHANNEL_ID).send(embed=embed)
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.send_msg(DEFAULT_CHANNEL_ID, 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(r.text)
except NameError:
f.write(feed_response.text)
except (NameError, AttributeError):
logger.error("There was no Response object!", exc_info=e)
await asyncio.sleep(10)
@ -311,7 +198,12 @@ class MyClient(discord.Client):
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'])}")
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"))
@ -335,10 +227,10 @@ bot = commands.Bot(command_prefix="!")
@bot.event
async def on_ready():
logger.debug("Bot loaded")
logger.info("Bot loaded")
# print(bot.user.name)
# print(bot.user.id)
logger.debug("------")
logger.info("------")
@bot.command()
@ -352,7 +244,9 @@ async def exit(ctx):
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()

40
dbot/map_events.py Normal file
View File

@ -0,0 +1,40 @@
import feedparser
from constants import COUNTRIES, events
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:
values = match.groupdict()
if "invader" in values and not values["invader"]:
values["invader"] = values["defender"]
has_latvia = any("Latvia" in v for v in values.values())
if has_latvia:
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 > 35:
main(c.link)
print("Finished", c)

View File

@ -2,7 +2,7 @@ import unittest
from sqlite_utils.db import NotFoundError
from db import DiscordDB
from dbot.db import DiscordDB
class TestDatabase(unittest.TestCase):
@ -38,3 +38,8 @@ class TestDatabase(unittest.TestCase):
self.assertTrue(self.db.add_epic(123456))
self.assertFalse(self.db.add_epic(123456))
self.assertTrue(self.db.get_epic(123456))
def test_rss_feed(self):
self.assertEqual(self.db.get_rss_feed_timestamp(71), 0.0)
self.db.set_rss_feed_timestamp(71, 16000000)
self.assertEqual(self.db.get_rss_feed_timestamp(71), 16000000.0)

View File

@ -1,6 +1,7 @@
#!/bin/sh
docker rm -f discord_bot
set -e
docker build --tag discord_epicbot .
docker run --detach -v $PWD:/app --restart=always --name discord_bot discord_epicbot
docker run --detach -v ./src:/app -v ./debug:/app/debug --env-file=".env" --restart=always --name discord_bot discord_epicbot

39
logger.py Normal file
View File

@ -0,0 +1,39 @@
import datetime
import json
import logging
import os
import sys
from json import JSONDecodeError
from typing import Union
import time
APP_NAME = "discord_bot"
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
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.WARNING)
file_logger.setFormatter(formatter)
logger.addHandler(file_logger)
stream_logger = logging.StreamHandler()
stream_logger.setLevel(logging.INFO)
stream_logger.setFormatter(formatter)
logger.addHandler(stream_logger)
logger.setLevel(logging.INFO)
def main():
logger.info('Info message')
logger.debug('Debug message')
logger.warning('Warning message')
logger.error('Error message')
logger.critical('Critical message')
if __name__ == "__main__":
main()

View File

@ -1,225 +0,0 @@
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,4 @@
[tool.black]
line-length = 140
line-length = 180
target-version = ['py38', 'py39']

18
run.sh
View File

@ -1,10 +1,18 @@
#!/bin/bash
source venv/bin/activate
D=test "$1" = "docker"
if test !$D ; then
source venv/bin/activate
fi
echo "Checking queries..."
python -m unittest
python -m unittest
echo "Starting Discord bot..."
python discord_bot.py &
disown -h %1
sleep 10
if test !$D ; then
export $(sed ':a;N;$!ba;s/\n/ /g' .env)
python dbot/discord_bot.py
disown -h %1
sleep 10
else
/usr/local/bin/python /app/discord_bot.py
fi
echo "Done!"

15
setup.cfg Normal file
View File

@ -0,0 +1,15 @@
[flake8]
exclude = docs,.git,log,debug,venv
line_length = 180
max-line-length = 180
ignore = D100,D101,D102,D103,E203
[pycodestyle]
line_length = 180
max-line-length = 180
exclude = .git,log,debug,venv, build
[isort]
multi_line_output = 2
line_length = 180