diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..96ef163 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +__pycache__ +# Docker +docker-compose.yml +.docker + +# Byte-compiled / optimized / DLL files +__pycache__/ +*/__pycache__/ +**/__pycache__/ +*.py[cod] +**/*.py[cod] + + diff --git a/.gitignore b/.gitignore index 5ed6508..609530c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ venv *.db +*.log .env +pid +*pid __pycache__ -.idea \ No newline at end of file +.idea +debug/ diff --git a/Dockerfile b/Dockerfile index 2fb604f..b4e187f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,13 @@ FROM python:3.9-slim WORKDIR /app +RUN groupadd -g 1000 discordbot \ + && useradd -u 1000 -g 1000 discordbot \ + && mkdir /home/discordbot \ + && chown -R discordbot:discordbot /app \ + && chown -R discordbot:discordbot /home/discordbot + +USER discordbot COPY requirements.txt /app/requirements.txt RUN pip install -r requirements.txt -COPY . /app CMD python discord_bot.py diff --git a/db.py b/db.py index 31ae3a9..4ed8bc6 100644 --- a/db.py +++ b/db.py @@ -1,4 +1,4 @@ -from typing import Union, Dict, Optional +from typing import Dict, Optional, Union from sqlite_utils import Database from sqlite_utils.db import NotFoundError @@ -21,7 +21,7 @@ class DiscordDB: self._db.create_table("player", {"id": int, "name": str}, pk="id", not_null={"id", "name"}) if "epic" not in self._db.table_names(): - self._db.create_table("epic", {"id": int, }, pk="id", not_null={"id"}) + self._db.create_table("epic", {"id": int, "fake": bool}, pk="id", not_null={"id"}, defaults={"fake": False}) self._db.vacuum() diff --git a/discord_bot.py b/discord_bot.py index ea73c79..215a03a 100644 --- a/discord_bot.py +++ b/discord_bot.py @@ -18,8 +18,7 @@ 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') +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 @@ -28,29 +27,94 @@ fh.setLevel(logging.DEBUG) logger.addHandler(fh) keep_fds = [fh.stream.fileno()] -os.makedirs('debug', exist_ok=True) +os.makedirs("debug", exist_ok=True) pidfile = "pid" -with open(pidfile, 'w') as f: +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_NAME = os.getenv("DB_NAME", "discord.db") DB = DiscordDB(DB_NAME) -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'} +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"} @@ -67,20 +131,20 @@ def s_to_human(seconds: Union[int, float]) -> str: h = seconds // 3600 m = (seconds - (h * 3600)) // 60 s = seconds % 60 - return f'{h:01d}:{m:02d}:{s:02d}' + 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://erep.lv/battles.json') + 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())) + __last_battle_update_timestamp = __last_battle_response.get("last_updated", int(dt.timestamp())) return __last_battle_response @@ -95,11 +159,11 @@ class MyClient(discord.Client): return int(datetime.datetime.now().timestamp()) async def on_ready(self): - print('Client running') - print('------') + print("Client running") + print("------") 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_epics(self): await self.wait_until_ready() @@ -108,28 +172,27 @@ class MyClient(discord.Client): 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 + 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') and not DB.get_epic(div.get('id')): - with open(f'debug/{self.timestamp}.json', 'wb') as f: + 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) await self.get_channel(DEFAULT_CHANNEL_ID).send( f"{role_mapping[MENTION_MAPPING[div['div']]]} Epic battle! Round time {s_to_human(self.timestamp - battle['start'])}\n" - f"https://www.erepublik.com/en/military/battlefield/{battle['id']}") - DB.add_epic(div.get('id')) + f"https://www.erepublik.com/en/military/battlefield/{battle['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 await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0) except Exception as e: - await self.get_channel(DEFAULT_CHANNEL_ID).send( - f"<@{ADMIN_ID}> Something bad has happened with epic notifier!") logger.error("Discord bot's eRepublik epic watcher died!", exc_info=e) try: - with open(f"{self.timestamp}.json", 'w') as f: + 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) @@ -139,15 +202,15 @@ class MyClient(discord.Client): loop = asyncio.get_event_loop() client = MyClient() -bot = commands.Bot(command_prefix='!') +bot = commands.Bot(command_prefix="!") @bot.event async def on_ready(): - print('Bot loaded') + print("Bot loaded") # print(bot.user.name) # print(bot.user.id) - print('------') + print("------") @bot.command() diff --git a/docker_run.sh b/docker_run.sh new file mode 100755 index 0000000..d3fd990 --- /dev/null +++ b/docker_run.sh @@ -0,0 +1,6 @@ +#!/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 + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7e28eca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.black] +line-length = 140 +target-version = ['py38', 'py39'] + diff --git a/requirements.txt b/requirements.txt index cbff2b6..9f8c44a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ discord.py==1.7.3 requests==2.26.0 -python-dotenv==0.18.0 -sqlite_utils==3.12 +python-dotenv==0.19.0 +sqlite_utils==3.13 diff --git a/run.sh b/run.sh index 8024444..cda8ed5 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,4 @@ #!/bin/bash -python -m venv venv source venv/bin/activate echo "Checking queries..." python -m unittest diff --git a/tests.py b/tests.py index f9f0f6e..ab82175 100644 --- a/tests.py +++ b/tests.py @@ -10,28 +10,28 @@ class TestDatabase(unittest.TestCase): self.db = DiscordDB() def test_member(self): - member = {'id': 1200, 'name': 'username'} + member = {"id": 1200, "name": "username"} self.db.add_member(**member) self.assertEqual(self.db.add_member(**member), member) self.assertRaises(NotFoundError, self.db.get_member, member_id=100) - self.assertEqual(self.db.get_member(member_id=member['id']), member) + self.assertEqual(self.db.get_member(member_id=member["id"]), member) member.update(name="Success") - self.assertTrue(self.db.update_member(member['id'], member['name'])) - self.assertEqual(self.db.get_member(member_id=member['id']), member) + self.assertTrue(self.db.update_member(member["id"], member["name"])) + self.assertEqual(self.db.get_member(member_id=member["id"]), member) def test_player(self): - player = {'id': 1, 'name': 'plato'} - self.assertTrue(self.db.add_player(player['id'], player['name'])) - self.assertFalse(self.db.add_player(player['id'], player['name'])) + player = {"id": 1, "name": "plato"} + self.assertTrue(self.db.add_player(player["id"], player["name"])) + self.assertFalse(self.db.add_player(player["id"], player["name"])) self.assertEqual(self.db.get_player(0), None) - self.assertEqual(self.db.get_player(player['id']), player) + self.assertEqual(self.db.get_player(player["id"]), player) self.assertFalse(self.db.update_player(0, "Error")) player["name"] = "New name" self.assertTrue(self.db.update_player(player["id"], player["name"])) - self.assertEqual(self.db.get_player(player['id']), player) + self.assertEqual(self.db.get_player(player["id"]), player) def test_epic(self): self.assertFalse(self.db.get_epic(123456))