Granularity

changes

Loop fixers
This commit is contained in:
KEriks 2021-08-31 15:12:08 +03:00
parent fc7c57abe5
commit 65cf45e600
12 changed files with 122 additions and 66 deletions

View File

@ -10,4 +10,6 @@ __pycache__/
*.py[cod] *.py[cod]
**/*.py[cod] **/*.py[cod]
.git
venv/

View File

@ -9,8 +9,9 @@ RUN groupadd -g 1000 discordbot \
&& chmod +x /run.sh && chmod +x /run.sh
USER discordbot USER discordbot
ENV PATH=$PATH:/home/discordbot/.local/bin
COPY requirements.txt /app/requirements.txt COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
ENV PYTHONPATH=$PYTHONPATH:/app/dbot ENV PYTHONPATH=$PYTHONPATH:/app/dbot
#CMD python discord_bot.py #CMD python discord_bot.py
ENTRYPOINT ["/usr/local/bin/python", "/app/dbot/discord_bot.py"] ENTRYPOINT ["/usr/local/bin/python", "/app/dbot/main.py"]

View File

@ -1,8 +1,9 @@
import asyncio
import logging import logging
import os import os
import sys import sys
from db import DiscordDB from dbot.db import DiscordDB
APP_NAME = "discord_bot" APP_NAME = "discord_bot"
@ -22,6 +23,7 @@ stream_logger = logging.StreamHandler()
stream_logger.setLevel(logging.DEBUG) stream_logger.setLevel(logging.DEBUG)
stream_logger.setFormatter(formatter) stream_logger.setFormatter(formatter)
logger.addHandler(stream_logger) logger.addHandler(stream_logger)
logger.propagate = False
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)
@ -29,6 +31,7 @@ ADMIN_ID = os.getenv("ADMIN_ID", 220849530730577920)
DB_NAME = os.getenv("DB_NAME", "discord.db") DB_NAME = os.getenv("DB_NAME", "discord.db")
PRODUCTION = bool(os.getenv("PRODUCTION")) PRODUCTION = bool(os.getenv("PRODUCTION"))
DB = DiscordDB(DB_NAME) DB = DiscordDB(DB_NAME)
DB.load_base_data()
MENTION_MAPPING = {1: "D1", 2: "D2", 3: "D3", 4: "D4", 11: "Air"} MENTION_MAPPING = {1: "D1", 2: "D2", 3: "D3", 4: "D4", 11: "Air"}
@ -52,3 +55,5 @@ MESSAGES = dict(
notifications_unset="✅ I won't notify about {} in this channel!", notifications_unset="✅ I won't notify about {} in this channel!",
notifications_set="✅ I will notify about {} in this channel!", notifications_set="✅ I will notify about {} in this channel!",
) )
LOOP = loop = asyncio.get_event_loop()

View File

@ -3,13 +3,14 @@ import sys
from discord import Embed from discord import Embed
from discord.enums import ChannelType from discord.enums import ChannelType
from discord.ext import commands from discord.ext import commands
from erepublik.constants import COUNTRIES
from dbot.base import ADMIN_ID, DB, DIVISION_MAPPING, MESSAGES, NOTIFICATION_KINDS, logger from dbot.base import ADMIN_ID, DB, DIVISION_MAPPING, LOOP, MESSAGES, NOTIFICATION_KINDS, logger
from dbot.utils import check_battles, get_battle_page from dbot.utils import check_battles, get_battle_page
__all__ = ["bot"] __all__ = ["DiscordBot"]
bot = commands.Bot(command_prefix="!") bot = DiscordBot = commands.Bot(command_prefix="!", loop=LOOP)
def _process_member(member): def _process_member(member):
@ -75,12 +76,13 @@ async def control_order_set(ctx, battle_id, side):
except IndexError: except IndexError:
return await ctx.send(MESSAGES["command_failed"]) return await ctx.send(MESSAGES["command_failed"])
DB.set_battle_order(battle_id, side_id) DB.set_battle_order(battle_id, side_id)
return await ctx.send(f"✅ Order has been set! {COUNTIRES[side_id].name} must win") return await ctx.send(f"✅ Order has been set! {COUNTRIES[side_id].name} must win")
return await ctx.send(MESSAGES["nothing_to_do"]) return await ctx.send(MESSAGES["nothing_to_do"])
async def control_order_unset(ctx, battle_id): async def control_order_unset(ctx, battle_id):
if DB.delete_battle_order(battle_id): if DB.delete_battle_order(battle_id):
return await ctx.send(f"✅ Order has been unset!") return await ctx.send("✅ Order has been unset!")
return await ctx.send(MESSAGES["nothing_to_do"]) return await ctx.send(MESSAGES["nothing_to_do"])
@ -89,6 +91,7 @@ async def control_order(ctx, action, *args):
return await control_order_set(ctx, *args) return await control_order_set(ctx, *args)
return await ctx.send(MESSAGES["nothing_to_do"]) return await ctx.send(MESSAGES["nothing_to_do"])
@bot.event @bot.event
async def on_ready(): async def on_ready():
logger.info("Bot loaded") logger.info("Bot loaded")
@ -203,4 +206,3 @@ async def control(ctx: commands.Context, command: str, *args):
async def control_error(ctx, error): async def control_error(ctx, error):
logger.exception(error, exc_info=error) logger.exception(error, exc_info=error)
return await ctx.send(MESSAGES["command_failed"]) return await ctx.send(MESSAGES["command_failed"])

View File

@ -1,41 +1,29 @@
import asyncio import asyncio
import datetime import datetime
import logging
import time import time
import discord import discord
import feedparser import feedparser
import pytz import pytz
import requests import requests
from constants import events
from erepublik.constants import COUNTRIES from erepublik.constants import COUNTRIES
from dbot.base import ADMIN_ID, DB, DB_NAME, DEFAULT_CHANNEL_ID, DISCORD_TOKEN, PRODUCTION, logger from dbot.base import ADMIN_ID, DB, DEFAULT_CHANNEL_ID, LOOP, PRODUCTION, logger
from dbot.bot_commands import bot from dbot.constants import events
from dbot.utils import check_battles, get_battle_page, timestamp from dbot.utils import check_battles, get_battle_page, timestamp
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}'") class DiscordClient(discord.Client):
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
self.last_event_timestamp = timestamp() self.last_event_timestamp = timestamp()
async def on_ready(self): async def on_ready(self):
logger.info("Client running") logger.info("Client running")
logger.info("------") logger.info("------")
# create the background task and run it in the background
self.bg_task = self.loop.create_task(self.report_battle_events()) self.bg_task = self.loop.create_task(self.report_battle_events())
# self.bg_rss_task = self.loop.create_task(self.report_rss_events()) self.bg_rss_task = self.loop.create_task(self.report_rss_events())
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}")
@ -132,29 +120,33 @@ class MyClient(discord.Client):
} }
for kind, div, data in check_battles(r.get("battles")): for kind, div, data in check_battles(r.get("battles")):
if kind == "epic" and not DB.check_epic(data["div_id"]): if kind == "epic" and not DB.check_epic(data["div_id"]):
embed_data = dict( embed = discord.Embed(
title=" ".join(data["extra"]["intensity_scale"].split("_")).title(), title=" ".join(data["extra"]["intensity_scale"].split("_")).title(),
url=data["url"], url=data["url"],
description=f"Epic battle {' vs '.join(data['sides'])}!\nBattle for {data['region']}, Round {data['zone_id']}", description=f"Epic battle {' vs '.join(data['sides'])}!\nBattle for {data['region']}, Round {data['zone_id']}",
footer=f"Round time {data['round_time']}",
) )
embed = discord.Embed.from_dict(embed_data) embed.set_footer(text=f"Round time {data['round_time']}")
logger.debug(f"{embed_data=}")
for channel_id in DB.get_kind_notification_channel_ids("epic"): for channel_id in DB.get_kind_notification_channel_ids("epic"):
if role_id := DB.get_role_id_for_channel_division(kind="epic", channel_id=channel_id, division=div): role_id = DB.get_role_id_for_channel_division(kind="epic", channel_id=channel_id, division=div)
logger.info(f"Sending epic d{div} message to {channel_id}, role to mention {role_id=}")
if role_id:
await self.get_channel(channel_id).send(f"<@&{role_id}> epic battle detected!", embed=embed) await self.get_channel(channel_id).send(f"<@&{role_id}> epic battle detected!", embed=embed)
else: else:
await self.get_channel(channel_id).send(embed=embed) await self.get_channel(channel_id).send(embed=embed)
DB.add_epic(data["div_id"]) DB.add_epic(data["div_id"])
logger.info(f"{data['div_id']} added to notified list")
if kind == "empty" and data["round_time_s"] >= 85 * 60 and not DB.check_empty_medal(data["div_id"]): if kind == "empty" and data["round_time_s"] >= 85 * 60 and not DB.check_empty_medal(data["div_id"]):
if len(empty_divisions[div]) < 10:
empty_divisions[div].add_field( empty_divisions[div].add_field(
name=f"**Battle for {data['region']} {' '.join(data['sides'])}**", value=f"[R{data['zone_id']} | Time {data['round_time']}]({data['url']})" name=f"**Battle for {data['region']} {' '.join(data['sides'])}**",
value=f"[R{data['zone_id']} | Time {data['round_time']}]({data['url']})",
) )
DB.add_empty_medal(data["div_id"]) DB.add_empty_medal(data["div_id"])
for d, e in empty_divisions.items(): for d, e in empty_divisions.items():
if e.fields: if e.fields:
for channel_id in DB.get_kind_notification_channel_ids("empty"): for channel_id in DB.get_kind_notification_channel_ids("empty"):
logger.info(f"Sending empty {d} message to {channel_id} with {len(e.fields)} battles: {e.fields}")
if role_id := DB.get_role_id_for_channel_division(kind="empty", channel_id=channel_id, division=d): if role_id := DB.get_role_id_for_channel_division(kind="empty", channel_id=channel_id, division=d):
await self.get_channel(channel_id).send(f"<@&{role_id}> empty medals in late rounds!", embed=e) await self.get_channel(channel_id).send(f"<@&{role_id}> empty medals in late rounds!", embed=e)
else: else:
@ -172,18 +164,4 @@ class MyClient(discord.Client):
await self.get_channel(DEFAULT_CHANNEL_ID).send(f"<@{ADMIN_ID}> I've stopped, please restart") await self.get_channel(DEFAULT_CHANNEL_ID).send(f"<@{ADMIN_ID}> I've stopped, please restart")
loop = asyncio.get_event_loop() client = DiscordClient(loop=LOOP)
client = MyClient()
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()

View File

@ -1,6 +1,8 @@
import logging import logging
import time
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from erepublik.constants import COUNTRIES
from sqlite_utils import Database from sqlite_utils import Database
from sqlite_utils.db import NotFoundError from sqlite_utils.db import NotFoundError
@ -10,6 +12,7 @@ class DiscordDB:
_db: Database _db: Database
def __init__(self, db_name: str = ""): def __init__(self, db_name: str = ""):
self.logger = logging.getLogger(self.__class__.__name__)
self._db = Database(db_name) if db_name else Database(memory=True) self._db = Database(db_name) if db_name else Database(memory=True)
self.initialize() self.initialize()
@ -31,12 +34,12 @@ class DiscordDB:
self._db.create_table("member_tmp", {"name": str, "pm_is_allowed": bool}, pk="id", not_null={"name", "pm_is_allowed"}, defaults={"pm_is_allowed": False}) self._db.create_table("member_tmp", {"name": str, "pm_is_allowed": bool}, pk="id", not_null={"name", "pm_is_allowed"}, defaults={"pm_is_allowed": False})
for row in self._db.table("member").rows: for row in self._db.table("member").rows:
self._db["member_tmp"].insert(row) self._db["member_tmp"].insert(row)
logging.info(f"Moving row {row} to tmp member table") self.logger.info(f"Moving row {row} to tmp member table")
self._db["member"].drop(True) self._db["member"].drop(True)
self._db.create_table("member", {"name": str, "pm_is_allowed": bool}, pk="id", not_null={"name", "pm_is_allowed"}, defaults={"pm_is_allowed": False}) self._db.create_table("member", {"name": str, "pm_is_allowed": bool}, pk="id", not_null={"name", "pm_is_allowed"}, defaults={"pm_is_allowed": False})
for row in self._db.table("member_tmp").rows: for row in self._db.table("member_tmp").rows:
self._db["member"].insert(row) self._db["member"].insert(row)
logging.info(f"Moving row {row} from tmp member table") self.logger.info(f"Moving row {row} from tmp member table")
self._db["member_tmp"].drop(True) self._db["member_tmp"].drop(True)
if "player" not in db_tables: if "player" not in db_tables:
@ -45,7 +48,7 @@ class DiscordDB:
try: try:
self._db.create_table("channel", {"guild_id": int, "channel_id": int, "kind": str}, pk="id", not_null={"guild_id", "channel_id", "kind"}, defaults={"kind": "epic"}) self._db.create_table("channel", {"guild_id": int, "channel_id": int, "kind": str}, pk="id", not_null={"guild_id", "channel_id", "kind"}, defaults={"kind": "epic"})
self._db["channel"].create_index(("guild_id", "channel_id", "kind"), unique=True) self._db["channel"].create_index(("guild_id", "channel_id", "kind"), unique=True)
except: except Exception:
pass pass
for row in self._db.table("notification_channel").rows: for row in self._db.table("notification_channel").rows:
self._db["channel"].insert(row) self._db["channel"].insert(row)
@ -53,17 +56,31 @@ class DiscordDB:
self._db.create_table("role_mapping", {"channel_id": int, "division": int, "role_id": int}, pk="id", not_null={"channel_id", "division", "role_id"}) self._db.create_table("role_mapping", {"channel_id": int, "division": int, "role_id": int}, pk="id", not_null={"channel_id", "division", "role_id"})
self._db["role_mapping"].add_foreign_key("channel_id", "channel", "id") self._db["role_mapping"].add_foreign_key("channel_id", "channel", "id")
self._db["role_mapping"].create_index(("channel_id", "division"), unique=True) self._db["role_mapping"].create_index(("channel_id", "division"), unique=True)
else:
for row in self._db["role_mapping"].rows:
try:
self._db["channel"].get(row["channel_id"])
except NotFoundError:
if any(self._db["channel"].rows_where("channel_id = ?", (row["channel_id"],))):
self._db["role_mapping"].update(row["id"], {"channel_id": next(self._db["channel"].rows_where("channel_id = ?", (row["channel_id"],)))["id"]})
else:
self.logger.warning(f"RoleMapping contained unknown channel id {row['channel_id']}!")
self.logger.warning(f"DELETED:{row=}")
self._db["role_mapping"].delete(row["id"])
for table in self._db.table_names(): for table in self._db.table_names():
if table not in hard_tables: if table not in hard_tables:
self._db.table(table).drop(ignore=True) self._db.table(table).drop(ignore=True)
self._db.create_table("division", {"division_id": int, "epic": bool, "empty": bool}, pk="id", defaults={"epic": False, "empty": False}, not_null={"division_id"}) self._db.create_table("division", {"division_id": int, "epic": bool, "empty": bool}, pk="id", defaults={"epic": False, "empty": False}, not_null={"division_id"})
self._db.create_table("rss_feed", {"timestamp": float}, pk="id", not_null={"timestamp"}) self._db.create_table("rss_feed", {"timestamp": float}, pk="id", not_null={"timestamp"})
self._db.create_table("battleorder", {"battle_id": int, "side": int}, pk="id", not_null={"battle_id","side"}, defaults={"side":71}) self._db.create_table("battleorder", {"battle_id": int, "side": int}, pk="id", not_null={"battle_id", "side"}, defaults={"side": 71})
self._db.vacuum() self._db.vacuum()
def load_base_data(self):
for country_id in COUNTRIES.keys():
self.set_rss_feed_timestamp(country_id, time.time())
# Player methods # Player methods
def get_player(self, pid: int) -> Optional[Dict[str, Union[int, str]]]: def get_player(self, pid: int) -> Optional[Dict[str, Union[int, str]]]:
@ -158,10 +175,7 @@ class DiscordDB:
:param division_id: int Division ID :param division_id: int Division ID
:return: bool :return: bool
""" """
try: return any(self.division.rows_where("division_id = ? and epic = ?", (division_id, True)))
return bool(next(self.division.rows_where("division_id = ? and epic = ?", (division_id, True))))
except StopIteration:
return False
def add_epic(self, division_id: int) -> bool: def add_epic(self, division_id: int) -> bool:
"""Register epic in division. """Register epic in division.
@ -280,7 +294,7 @@ class DiscordDB:
for row in rows: for row in rows:
return row["role_id"] return row["role_id"]
def set_battle_order(self, battle_id:int, side:int): def set_battle_order(self, battle_id: int, side: int):
if self.get_battle_order(battle_id): if self.get_battle_order(battle_id):
return False return False
self.battleorder.insert(dict(battle_id=battle_id, side=side)) self.battleorder.insert(dict(battle_id=battle_id, side=side))
@ -288,16 +302,16 @@ class DiscordDB:
def get_battle_order(self, battle_id: int = None): def get_battle_order(self, battle_id: int = None):
if battle_id is None: if battle_id is None:
return list(sef.battleorder.rows) return list(self.battleorder.rows)
try: try:
row = next(self.battleorder.rows_where('battle_id = ?', (battle_id,))) row = next(self.battleorder.rows_where("battle_id = ?", (battle_id,)))
return row return row
except StopIterationError: except StopIteration:
return return
def delete_battle_order(self, battle_id: int): def delete_battle_order(self, battle_id: int):
bo = self.get_battle_order(battle_id) bo = self.get_battle_order(battle_id)
if bo: if bo:
DB.battleorder.delete(bo['id']) self.battleorder.delete(bo["id"])
return True return True
return False return False

44
dbot/main.py Normal file
View File

@ -0,0 +1,44 @@
import asyncio
import logging
import time
from erepublik.constants import COUNTRIES
from dbot.base import ADMIN_ID, DB, DB_NAME, DEFAULT_CHANNEL_ID, DISCORD_TOKEN, LOOP, PRODUCTION, logger
from dbot.bot import bot
from dbot.client import client
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}'")
def main():
logger.info("Starting Bot loop")
LOOP.create_task(bot.start(DISCORD_TOKEN))
# asyncio.run(bot.start(DISCORD_TOKEN))
logger.info("Starting Client loop")
# asyncio.run(client.start(DISCORD_TOKEN))
LOOP.create_task(client.start(DISCORD_TOKEN))
LOOP.run_forever()
async def run_main():
logger.info("Starting Client loop")
logger.info("Starting Bot loop")
await asyncio.gather(bot.start(DISCORD_TOKEN), client.start(DISCORD_TOKEN))
logger.info("Loops have finished")
if __name__ == "__main__":
# asyncio.run(run_main())
main()

View File

@ -1,5 +1,6 @@
import feedparser import feedparser
from constants import COUNTRIES, events
from dbot.constants import COUNTRIES, events
def main(country): def main(country):

View File

@ -1,4 +1,11 @@
#!/bin/sh #!/bin/sh
sh ./lint.sh
ret=$?
if test $ret != 0; then
exit 1
fi
docker rm -f discord_bot docker rm -f discord_bot
set -e set -e
docker build --tag discord_epicbot . docker build --tag discord_epicbot .

View File

@ -1,4 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -e
isort dbot isort dbot
black dbot black dbot
flake8 dbot flake8 dbot
PYTHONPATH="$(python -c "import os.path; print(os.path.realpath('$1'))")/dbot" python -m unittest dbot/tests.py

View File

@ -1,6 +1,6 @@
black==21.7b0 black==21.7b0
discord.py==1.7.3 discord.py==1.7.3
eRepublik==0.25.1.5 eRepublik==0.27.0
feedparser==6.0.8 feedparser==6.0.8
flake8==3.9.2 flake8==3.9.2
isort==5.9.3 isort==5.9.3

2
run.sh
View File

@ -12,7 +12,7 @@ if test !$D ; then
disown -h %1 disown -h %1
sleep 10 sleep 10
else else
/usr/local/bin/python /app/discord_bot.py /usr/local/bin/python /app/main.py
fi fi
echo "Done!" echo "Done!"