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]
.git
venv/

View File

@ -9,8 +9,9 @@ RUN groupadd -g 1000 discordbot \
&& chmod +x /run.sh
USER discordbot
ENV PATH=$PATH:/home/discordbot/.local/bin
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
ENV PYTHONPATH=$PYTHONPATH:/app/dbot
#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 os
import sys
from db import DiscordDB
from dbot.db import DiscordDB
APP_NAME = "discord_bot"
@ -22,6 +23,7 @@ stream_logger = logging.StreamHandler()
stream_logger.setLevel(logging.DEBUG)
stream_logger.setFormatter(formatter)
logger.addHandler(stream_logger)
logger.propagate = False
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
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")
PRODUCTION = bool(os.getenv("PRODUCTION"))
DB = DiscordDB(DB_NAME)
DB.load_base_data()
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_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.enums import ChannelType
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
__all__ = ["bot"]
__all__ = ["DiscordBot"]
bot = commands.Bot(command_prefix="!")
bot = DiscordBot = commands.Bot(command_prefix="!", loop=LOOP)
def _process_member(member):
@ -75,12 +76,13 @@ async def control_order_set(ctx, battle_id, side):
except IndexError:
return await ctx.send(MESSAGES["command_failed"])
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"])
async def control_order_unset(ctx, 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"])
@ -89,6 +91,7 @@ async def control_order(ctx, action, *args):
return await control_order_set(ctx, *args)
return await ctx.send(MESSAGES["nothing_to_do"])
@bot.event
async def on_ready():
logger.info("Bot loaded")
@ -203,4 +206,3 @@ async def control(ctx: commands.Context, command: str, *args):
async def control_error(ctx, error):
logger.exception(error, exc_info=error)
return await ctx.send(MESSAGES["command_failed"])

View File

@ -1,41 +1,29 @@
import asyncio
import datetime
import logging
import time
import discord
import feedparser
import pytz
import requests
from constants import events
from erepublik.constants import COUNTRIES
from dbot.base import ADMIN_ID, DB, DB_NAME, DEFAULT_CHANNEL_ID, DISCORD_TOKEN, PRODUCTION, logger
from dbot.bot_commands import bot
from dbot.base import ADMIN_ID, DB, DEFAULT_CHANNEL_ID, LOOP, PRODUCTION, logger
from dbot.constants import events
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 MyClient(discord.Client):
class DiscordClient(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 = timestamp()
async def on_ready(self):
logger.info("Client running")
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_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):
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")):
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(),
url=data["url"],
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)
logger.debug(f"{embed_data=}")
embed.set_footer(text=f"Round time {data['round_time']}")
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)
else:
await self.get_channel(channel_id).send(embed=embed)
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"]):
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']})"
)
DB.add_empty_medal(data["div_id"])
if len(empty_divisions[div]) < 10:
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']})",
)
DB.add_empty_medal(data["div_id"])
for d, e in empty_divisions.items():
if e.fields:
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):
await self.get_channel(channel_id).send(f"<@&{role_id}> empty medals in late rounds!", embed=e)
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")
loop = asyncio.get_event_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()
client = DiscordClient(loop=LOOP)

View File

@ -1,6 +1,8 @@
import logging
import time
from typing import Dict, List, Optional, Union
from erepublik.constants import COUNTRIES
from sqlite_utils import Database
from sqlite_utils.db import NotFoundError
@ -10,6 +12,7 @@ class DiscordDB:
_db: Database
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.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})
for row in self._db.table("member").rows:
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.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:
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)
if "player" not in db_tables:
@ -45,7 +48,7 @@ class DiscordDB:
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["channel"].create_index(("guild_id", "channel_id", "kind"), unique=True)
except:
except Exception:
pass
for row in self._db.table("notification_channel").rows:
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["role_mapping"].add_foreign_key("channel_id", "channel", "id")
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():
if table not in hard_tables:
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("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()
def load_base_data(self):
for country_id in COUNTRIES.keys():
self.set_rss_feed_timestamp(country_id, time.time())
# Player methods
def get_player(self, pid: int) -> Optional[Dict[str, Union[int, str]]]:
@ -158,10 +175,7 @@ class DiscordDB:
:param division_id: int Division ID
:return: bool
"""
try:
return bool(next(self.division.rows_where("division_id = ? and epic = ?", (division_id, True))))
except StopIteration:
return False
return any(self.division.rows_where("division_id = ? and epic = ?", (division_id, True)))
def add_epic(self, division_id: int) -> bool:
"""Register epic in division.
@ -280,7 +294,7 @@ class DiscordDB:
for row in rows:
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):
return False
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):
if battle_id is None:
return list(sef.battleorder.rows)
return list(self.battleorder.rows)
try:
row = next(self.battleorder.rows_where('battle_id = ?', (battle_id,)))
row = next(self.battleorder.rows_where("battle_id = ?", (battle_id,)))
return row
except StopIterationError:
except StopIteration:
return
def delete_battle_order(self, battle_id: int):
bo = self.get_battle_order(battle_id)
if bo:
DB.battleorder.delete(bo['id'])
self.battleorder.delete(bo["id"])
return True
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
from constants import COUNTRIES, events
from dbot.constants import COUNTRIES, events
def main(country):

View File

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

View File

@ -1,4 +1,6 @@
#!/usr/bin/env sh
set -e
isort dbot
black 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
discord.py==1.7.3
eRepublik==0.25.1.5
eRepublik==0.27.0
feedparser==6.0.8
flake8==3.9.2
isort==5.9.3

2
run.sh
View File

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