Major rewrite
This commit is contained in:
parent
2b4ec22349
commit
75186c3728
0
dbot/__init__.py
Normal file
0
dbot/__init__.py
Normal file
54
dbot/base.py
Normal file
54
dbot/base.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from db import DiscordDB
|
||||||
|
|
||||||
|
APP_NAME = "discord_bot"
|
||||||
|
|
||||||
|
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||||
|
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("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)
|
||||||
|
|
||||||
|
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
||||||
|
DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096)
|
||||||
|
ADMIN_ID = os.getenv("ADMIN_ID", 220849530730577920)
|
||||||
|
DB_NAME = os.getenv("DB_NAME", "discord.db")
|
||||||
|
PRODUCTION = bool(os.getenv("PRODUCTION"))
|
||||||
|
DB = DiscordDB(DB_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
MENTION_MAPPING = {1: "D1", 2: "D2", 3: "D3", 4: "D4", 11: "Air"}
|
||||||
|
DIVISION_MAPPING = {v: k for k, v in MENTION_MAPPING.items()}
|
||||||
|
|
||||||
|
NOTIFICATION_KINDS = (
|
||||||
|
"epic",
|
||||||
|
"events",
|
||||||
|
"empty",
|
||||||
|
)
|
||||||
|
|
||||||
|
MESSAGES = dict(
|
||||||
|
not_admin="❌ Only server administrators are allowed to enable notifications!",
|
||||||
|
not_in_pm="❌ Unable to notify in PMs!",
|
||||||
|
command_failed="❌ Command failed!",
|
||||||
|
only_registered_channels="❌ This command is only available from registered channels!",
|
||||||
|
mention_help="❌ Please provide kind, action, division and role to mention! Eg. '{command} epic set d4 @div4' or '{command} empty remove d3'",
|
||||||
|
mention_info="ℹ️ If You want for me to also add division mentions write:\n`!control mention [kind] set [division] [role_mention]`\n"
|
||||||
|
"Example: `!control mention {kind} set d4 @div4` or `!control mention {kind} set air @aviators`",
|
||||||
|
nothing_to_do="ℹ️ Nothing to do here...",
|
||||||
|
notifications_unset="✅ I won't notify about {} in this channel!",
|
||||||
|
notifications_set="✅ I will notify about {} in this channel!",
|
||||||
|
)
|
175
dbot/bot_commands.py
Normal file
175
dbot/bot_commands.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from discord import Embed
|
||||||
|
from discord.enums import ChannelType
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from dbot.base import ADMIN_ID, DB, DIVISION_MAPPING, MESSAGES, NOTIFICATION_KINDS, logger
|
||||||
|
from dbot.utils import check_battles, get_battle_page
|
||||||
|
|
||||||
|
__all__ = ["bot"]
|
||||||
|
|
||||||
|
bot = commands.Bot(command_prefix="!")
|
||||||
|
|
||||||
|
|
||||||
|
async def control_register(ctx, *args):
|
||||||
|
if " ".join(args) == "From AF With Love!":
|
||||||
|
DB.update_member(ctx.author.id, str(ctx.author), True)
|
||||||
|
return await ctx.send("✅ You have been registered and are allowed to issue commands privately! 🥳")
|
||||||
|
return await ctx.send(MESSAGES["command_failed"])
|
||||||
|
|
||||||
|
|
||||||
|
async def control_notify(ctx, kind):
|
||||||
|
if kind == "epic":
|
||||||
|
if DB.add_notification_channel(ctx.guild.id, ctx.channel.id, kind):
|
||||||
|
return await ctx.send(MESSAGES["notifications_set"].format("epic battles"))
|
||||||
|
elif kind == "events":
|
||||||
|
if DB.add_notification_channel(ctx.guild.id, ctx.channel.id, kind):
|
||||||
|
return await ctx.send(MESSAGES["notifications_set"].format("eLatvia's events"))
|
||||||
|
elif kind == "empty":
|
||||||
|
if DB.add_notification_channel(ctx.guild.id, ctx.channel.id, kind):
|
||||||
|
return await ctx.send(MESSAGES["notifications_set"].format("empty medals"))
|
||||||
|
return await ctx.send(MESSAGES["nothing_to_do"])
|
||||||
|
|
||||||
|
|
||||||
|
async def control_unnotify(ctx, kind):
|
||||||
|
if DB.remove_kind_notification_channel(kind, ctx.channel.id):
|
||||||
|
if kind == "epic":
|
||||||
|
return await ctx.send(MESSAGES["notifications_unset"].format("epic battles"))
|
||||||
|
if kind == "events":
|
||||||
|
return await ctx.send(MESSAGES["notifications_unset"].format("eLatvia's notifications"))
|
||||||
|
if kind == "empty":
|
||||||
|
return await ctx.send(MESSAGES["notifications_unset"].format("empty medals"))
|
||||||
|
return await ctx.send(MESSAGES["command_failed"])
|
||||||
|
|
||||||
|
|
||||||
|
async def control_mention_set(ctx, kind: str, division: str, role: str):
|
||||||
|
for guild_role in ctx.guild.roles:
|
||||||
|
if guild_role.mention == role:
|
||||||
|
if not guild_role.mentionable:
|
||||||
|
return await ctx.send(f"❌ Unable to use {role=}, because this role is not globally mentionable!")
|
||||||
|
DB.add_role_mapping_entry(kind, ctx.channel.id, DIVISION_MAPPING[division], guild_role.id)
|
||||||
|
return await ctx.send(f"✅ Success! For {division} epics I will mention {guild_role.mention}")
|
||||||
|
return await ctx.send(MESSAGES["command_failed"])
|
||||||
|
|
||||||
|
|
||||||
|
async def control_mention_remove(ctx, kind: str, division: str):
|
||||||
|
if DB.remove_role_mapping(kind, ctx.channel.id, DIVISION_MAPPING[division]):
|
||||||
|
return await ctx.send(f"✅ I won't mention here any role about {division} events!")
|
||||||
|
return await ctx.send(MESSAGES["nothing_to_do"])
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
logger.info("Bot loaded")
|
||||||
|
# print(bot.user.name)
|
||||||
|
# print(bot.user.id)
|
||||||
|
logger.info("------")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def empty(ctx, division, minutes: int = 60):
|
||||||
|
if not ctx.channel.id == 603527159109124096 or not DB.get_member(ctx.message.author.id).get("pm_is_allowed"):
|
||||||
|
return await ctx.send("Currently unavailable!")
|
||||||
|
try:
|
||||||
|
div = int(division)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
div = dict(D1=1, D2=3, D3=3, D4=4, Air=11)[division.title()]
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return await ctx.send("First argument must be a value from: 1, d1, 2, d2, 3, d3, 4, d4, 11, air!")
|
||||||
|
s_div = {1: "D1", 2: "D2", 3: "D3", 4: "D4", 11: "Air"}[div]
|
||||||
|
embed = Embed(
|
||||||
|
title=f"Possibly empty {s_div} medals",
|
||||||
|
description="'Empty' medals are being guessed based on the division wall. Expect false-positives!",
|
||||||
|
)
|
||||||
|
for kind, div_div, data in check_battles(get_battle_page().get("battles")):
|
||||||
|
if kind == "empty" and div_div == div and data["round_time_s"] >= minutes * 60:
|
||||||
|
embed.add_field(
|
||||||
|
name=f"**Battle for {data['region']} {' '.join(data['sides'])}**",
|
||||||
|
value=f"[R{data['zone_id']} | Time {data['round_time']}]({data['url']})",
|
||||||
|
)
|
||||||
|
if len(embed.fields) >= 10:
|
||||||
|
return await ctx.send(embed=embed)
|
||||||
|
if embed.fields:
|
||||||
|
return await ctx.send(embed=embed)
|
||||||
|
else:
|
||||||
|
return await ctx.send(f"No empty {s_div} medals found")
|
||||||
|
|
||||||
|
|
||||||
|
@empty.error
|
||||||
|
async def division_error(ctx, error):
|
||||||
|
if isinstance(error, (commands.BadArgument, commands.MissingRequiredArgument)):
|
||||||
|
return await ctx.send("❌ Division is mandatory, eg, `!empty [1,2,3,4,11, d1,d2,d3,d4,air, D1,D2,D3,D4,Air] [1-120]`")
|
||||||
|
logger.exception(error, exc_info=error)
|
||||||
|
await ctx.send("❌ Something went wrong! 😔")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def control(ctx: commands.Context, command: str, *args):
|
||||||
|
if not DB.get_member(ctx.author.id):
|
||||||
|
DB.add_member(ctx.author.id, str(ctx.author))
|
||||||
|
if command == "register":
|
||||||
|
return await control_register(ctx, *args)
|
||||||
|
if command in ["notify", "unnotify"]:
|
||||||
|
if ctx.channel.type == ChannelType.private:
|
||||||
|
return await ctx.send(MESSAGES["not_in_pm"])
|
||||||
|
if not ctx.author.guild_permissions.administrator:
|
||||||
|
return await ctx.send(MESSAGES["not_admin"])
|
||||||
|
if not args:
|
||||||
|
return await ctx.send(
|
||||||
|
f"❌ Please provide what kind of notifications You would like to {'en' if command == 'notify' else 'dis'}able! Currently available: {', '.join(NOTIFICATION_KINDS)}"
|
||||||
|
)
|
||||||
|
kind = str(args[0]).lower()
|
||||||
|
if kind not in NOTIFICATION_KINDS:
|
||||||
|
return await ctx.send(f'❌ Notification {kind=} is unknown! Currently available: {", ".join(NOTIFICATION_KINDS)}')
|
||||||
|
if command == "notify":
|
||||||
|
return await control_notify(ctx, kind)
|
||||||
|
if command == "unnotify":
|
||||||
|
return await control_unnotify(ctx, kind)
|
||||||
|
|
||||||
|
if command == "mention":
|
||||||
|
if ctx.channel.type == ChannelType.private:
|
||||||
|
return await ctx.send(MESSAGES["not_in_pm"])
|
||||||
|
if not ctx.author.guild_permissions.administrator:
|
||||||
|
return await ctx.send(MESSAGES["not_admin"])
|
||||||
|
if not args or not 3 <= len(args) <= 4:
|
||||||
|
return await ctx.send(MESSAGES["mention_help"].format(command=command))
|
||||||
|
|
||||||
|
try:
|
||||||
|
kind, action, division, *role = args
|
||||||
|
if role:
|
||||||
|
role = role[0]
|
||||||
|
kind = str(kind).lower()
|
||||||
|
if ctx.channel.id not in DB.get_kind_notification_channel_ids(kind):
|
||||||
|
return await ctx.send(MESSAGES["only_registered_channels"])
|
||||||
|
if kind not in ("epic", "empty"):
|
||||||
|
return await ctx.send(f"❌ {kind=} doesn't support division mentioning!")
|
||||||
|
|
||||||
|
if action not in ("set", "remove"):
|
||||||
|
return await ctx.send(MESSAGES["mention_help"].format(command=command))
|
||||||
|
action = str(action).lower()
|
||||||
|
division = str(division).title()
|
||||||
|
if division not in DIVISION_MAPPING:
|
||||||
|
await ctx.send(f"❌ Unknown {division=}! Available divisions: {', '.join(d.title() for d in DIVISION_MAPPING.keys())}")
|
||||||
|
return await ctx.send(MESSAGES["mention_info"].format(kind=kind))
|
||||||
|
|
||||||
|
if action == "set":
|
||||||
|
return await control_mention_set(ctx, kind, division, role)
|
||||||
|
if action == "remove":
|
||||||
|
return await control_mention_remove(ctx, kind, division)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(str(e), exc_info=e, stacklevel=3)
|
||||||
|
return await ctx.send(MESSAGES["mention_help"].format(command=command))
|
||||||
|
|
||||||
|
if command == "exit":
|
||||||
|
if ctx.author.id == ADMIN_ID:
|
||||||
|
await ctx.send(f"{ctx.author.mention} Bye!")
|
||||||
|
sys.exit(0)
|
||||||
|
return await ctx.send(f"❌ Unknown {command=}!")
|
||||||
|
|
||||||
|
|
||||||
|
@empty.error
|
||||||
|
async def control_error(ctx, error):
|
||||||
|
logger.exception(error, exc_info=error)
|
||||||
|
return await ctx.send(MESSAGES["command_failed"])
|
@ -1,9 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
from typing import NamedTuple
|
from typing import Any, Dict, List, NamedTuple, TypedDict
|
||||||
|
|
||||||
from erepublik.constants import COUNTRIES
|
from erepublik.constants import COUNTRIES
|
||||||
|
|
||||||
__all__ = ["events", COUNTRIES, "FLAGS", "UTF_FLAG"]
|
__all__ = ["events", "COUNTRIES", "UTF_FLAG", "DivisionData"]
|
||||||
|
|
||||||
region = r"[\w\(\)\-& ']+"
|
region = r"[\w\(\)\-& ']+"
|
||||||
country = r"(Resistance force of )?[\w\(\)\- ]+"
|
country = r"(Resistance force of )?[\w\(\)\- ]+"
|
||||||
@ -191,9 +191,9 @@ events = [
|
|||||||
"Res Concession",
|
"Res Concession",
|
||||||
re.compile(
|
re.compile(
|
||||||
rf"A Resource Concession law to //www.erepublik.com<b>(?P<target>{country})</b> "
|
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>'
|
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}",
|
"Resource Concession law between {source} and {target} has been {result}",
|
||||||
),
|
),
|
||||||
EventKind(
|
EventKind(
|
||||||
"cp_impeachment",
|
"cp_impeachment",
|
||||||
@ -258,13 +258,13 @@ events = [
|
|||||||
EventKind(
|
EventKind(
|
||||||
"new_welcome_message_proposed",
|
"new_welcome_message_proposed",
|
||||||
"New Welcome message has been proposed",
|
"New Welcome message has been proposed",
|
||||||
re.compile(r"President of (?P<country>{country}) proposed a new welcome message for new citizens"),
|
re.compile(rf"President of (?P<country>{country}) proposed a new welcome message for new citizens"),
|
||||||
"{country} proposed new welcome message!",
|
"{country} proposed new welcome message!",
|
||||||
),
|
),
|
||||||
EventKind(
|
EventKind(
|
||||||
"new_welcome_message_approved",
|
"new_welcome_message_approved",
|
||||||
"New Welcome message has been approved",
|
"New Welcome message has been approved",
|
||||||
re.compile(r"(?P<country>{country}) now has a new welcoming message for new citizens"),
|
re.compile(rf"(?P<country>{country}) now has a new welcoming message for new citizens"),
|
||||||
"{country} approved new welcome message!",
|
"{country} approved new welcome message!",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -345,79 +345,14 @@ UTF_FLAG = {
|
|||||||
170: "🇳🇬",
|
170: "🇳🇬",
|
||||||
171: "🇨🇺",
|
171: "🇨🇺",
|
||||||
}
|
}
|
||||||
FLAGS = {
|
|
||||||
1: "flag_ro",
|
|
||||||
9: "flag_br",
|
class DivisionData(TypedDict):
|
||||||
10: "flag_it",
|
region: str
|
||||||
11: "flag_fr",
|
round_time: str
|
||||||
12: "flag_de",
|
round_time_s: int
|
||||||
13: "flag_hu",
|
sides: List[str]
|
||||||
14: "flag_cn",
|
url: str
|
||||||
15: "flag_es",
|
zone_id: int
|
||||||
23: "flag_ca",
|
div_id: int
|
||||||
24: "flag_us",
|
extra: Dict[str, Any]
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
174
dbot/db.py
174
dbot/db.py
@ -1,51 +1,51 @@
|
|||||||
from typing import Dict, Optional, Union, List, Tuple
|
from typing import Dict, List, Optional, Union
|
||||||
import logging
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.db import NotFoundError
|
from sqlite_utils.db import NotFoundError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscordDB:
|
class DiscordDB:
|
||||||
_name: str
|
_name: str
|
||||||
_db: Database
|
_db: Database
|
||||||
|
|
||||||
def __init__(self, db_name: str = ""):
|
def __init__(self, db_name: str = ""):
|
||||||
self._name = db_name
|
self._db = Database(db_name) if db_name else Database(memory=True)
|
||||||
if not self._name:
|
|
||||||
self._db = Database(memory=True)
|
|
||||||
else:
|
|
||||||
self._db = Database(self._name)
|
|
||||||
if "member" not in self._db.table_names():
|
|
||||||
self._db.create_table("member", {"id": int, "name": str}, pk="id", not_null={"id", "name"})
|
|
||||||
|
|
||||||
if "player" not in self._db.table_names():
|
self.initialize()
|
||||||
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, "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"})
|
|
||||||
|
|
||||||
if "notification_channel" not in self._db.table_names():
|
|
||||||
self._db.create_table(
|
|
||||||
"notification_channel", {"id": int, "guild_id": int, "channel_id": int, "kind": str}, pk="id", not_null={"id", "guild_id", "channel_id"}, defaults={"kind": "epic"}
|
|
||||||
)
|
|
||||||
self._db["notification_channel"].create_index(("guild_id", "channel_id", "kind"), unique=True)
|
|
||||||
self._db.create_table("role_mapping", {"id": int, "channel_id": int, "division": int, "role_id": int}, pk="id", not_null={"id", "channel_id", "division", "role_id"})
|
|
||||||
self._db["role_mapping"].add_foreign_key("channel_id", "notification_channel", "channel_id")
|
|
||||||
self._db["role_mapping"].create_index(("channel_id", "division"), unique=True)
|
|
||||||
|
|
||||||
self._db.vacuum()
|
|
||||||
|
|
||||||
self.member = self._db.table("member")
|
self.member = self._db.table("member")
|
||||||
self.player = self._db.table("player")
|
self.player = self._db.table("player")
|
||||||
self.epic = self._db.table("epic")
|
self.division = self._db.table("division")
|
||||||
self.rss_feed = self._db.table("rss_feed")
|
self.rss_feed = self._db.table("rss_feed")
|
||||||
self.channel = self._db.table("notification_channel")
|
self.channel = self._db.table("channel")
|
||||||
self.role_mapping = self._db.table("role_mapping")
|
self.role_mapping = self._db.table("role_mapping")
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
hard_tables = ["member", "player", "channel", "role_mapping"]
|
||||||
|
db_tables = self._db.table_names()
|
||||||
|
if "member" not in db_tables:
|
||||||
|
self._db.create_table("member", {"name": str, "pm_is_allowed": bool}, pk="id", not_null={"name", "pm_is_allowed"}, defaults={"pm_is_allowed": False})
|
||||||
|
if "player" not in db_tables:
|
||||||
|
self._db.create_table("player", {"name": str}, pk="id", not_null={"name"})
|
||||||
|
if "notification_channel" in db_tables or "channel" not in db_tables:
|
||||||
|
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)
|
||||||
|
for row in self._db.table("notification_channel").rows:
|
||||||
|
self._db["channel"].insert(**row)
|
||||||
|
if "role_mapping" not in db_tables:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.vacuum()
|
||||||
|
|
||||||
# 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]]]:
|
||||||
@ -87,7 +87,7 @@ class DiscordDB:
|
|||||||
|
|
||||||
# Member methods
|
# Member methods
|
||||||
|
|
||||||
def get_member(self, member_id) -> Dict[str, Union[int, str]]:
|
def get_member(self, member_id) -> Optional[Dict[str, Union[int, str]]]:
|
||||||
"""Get discord member
|
"""Get discord member
|
||||||
|
|
||||||
:param member_id: int Discord Member ID
|
:param member_id: int Discord Member ID
|
||||||
@ -98,56 +98,85 @@ class DiscordDB:
|
|||||||
try:
|
try:
|
||||||
return self.member.get(member_id)
|
return self.member.get(member_id)
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
raise NotFoundError("Member with given id not found")
|
return
|
||||||
|
|
||||||
def add_member(self, id: int, name: str) -> Dict[str, Union[int, str]]:
|
def add_member(self, id: int, name: str, pm_is_allowed: bool = False) -> Dict[str, Union[int, str]]:
|
||||||
"""Add discord member.
|
"""Add discord member.
|
||||||
|
|
||||||
:param id: int Discord member ID
|
:param id: int Discord member ID
|
||||||
:param name: Discord member Name
|
:param name: Discord member Name
|
||||||
|
:param pm_is_allowed: Allow discord member to contact bot through PMs
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.member.insert({"id": id, "name": name})
|
self.member.insert({"id": id, "name": name, "pm_is_allowed": pm_is_allowed})
|
||||||
finally:
|
finally:
|
||||||
return self.member.get(id)
|
return self.member.get(id)
|
||||||
|
|
||||||
def update_member(self, member_id: int, name: str) -> bool:
|
def update_member(self, member_id: int, name: str, pm_is_allowed: bool = None) -> bool:
|
||||||
"""Update discord member"s record
|
"""Update discord member"s record
|
||||||
|
|
||||||
:param member_id: Discord Mention ID
|
:param member_id: Discord Mention ID
|
||||||
:type member_id: int Discord Mention ID
|
:type member_id: int
|
||||||
:param name: Discord user name
|
:param name: Discord user name
|
||||||
:type name: str Discord user name
|
:type name: str
|
||||||
|
:param pm_is_allowed: Is discord user allowed to interact through PMs
|
||||||
|
:type pm_is_allowed: Optional[bool]
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
try:
|
member = self.get_member(member_id)
|
||||||
member = self.get_member(member_id)
|
if member:
|
||||||
except NotFoundError:
|
if pm_is_allowed is None:
|
||||||
member = self.add_member(member_id, name)
|
pm_is_allowed = self.member.get(member_id).get("pm_is_allowed")
|
||||||
self.member.update(member["id"], {"name": name})
|
self.member.update(member["id"], {"name": name, "pm_is_allowed": pm_is_allowed})
|
||||||
|
return True
|
||||||
|
self.add_member(member_id, name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Epic Methods
|
# Epic Methods
|
||||||
|
|
||||||
def get_epic(self, division_id: int) -> Optional[Dict[str, Union[int, str]]]:
|
def check_epic(self, division_id: int) -> bool:
|
||||||
|
"""Check if epic has been registered in the division
|
||||||
|
|
||||||
|
: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
|
||||||
|
|
||||||
|
def add_epic(self, division_id: int) -> bool:
|
||||||
|
"""Register epic in division.
|
||||||
|
|
||||||
|
:param division_id: int Epic division ID
|
||||||
|
:return: bool Epic division added
|
||||||
|
"""
|
||||||
|
if not self.check_epic(division_id):
|
||||||
|
self.division.insert({"division_id": division_id, "epic": True})
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Epic Methods
|
||||||
|
|
||||||
|
def check_empty_medal(self, division_id: int) -> bool:
|
||||||
"""Get Epic division
|
"""Get Epic division
|
||||||
|
|
||||||
:param division_id: int Division ID
|
:param division_id: int Division ID
|
||||||
:return: division id
|
:return: division id
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self.epic.get(division_id)
|
return bool(next(self.division.rows_where("division_id = ? and empty = ?", (division_id, True))))
|
||||||
except NotFoundError:
|
except StopIteration:
|
||||||
return None
|
return False
|
||||||
|
|
||||||
def add_epic(self, division_id: int) -> bool:
|
def add_empty_medal(self, division_id: int) -> bool:
|
||||||
"""Add Epic division.
|
"""Add Epic division.
|
||||||
|
|
||||||
:param division_id: int Epic division ID
|
:param division_id: int Epic division ID
|
||||||
:return: bool Epic division added
|
:return: bool Epic division added
|
||||||
"""
|
"""
|
||||||
if not self.get_epic(division_id):
|
if not self.check_empty_medal(division_id):
|
||||||
self.epic.insert({"id": division_id})
|
self.division.insert({"division_id": division_id, "empty": True})
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -175,7 +204,7 @@ class DiscordDB:
|
|||||||
else:
|
else:
|
||||||
self.rss_feed.insert({"id": country_id, "timestamp": timestamp})
|
self.rss_feed.insert({"id": country_id, "timestamp": timestamp})
|
||||||
|
|
||||||
# RSS Event Methods
|
# Notification methods
|
||||||
|
|
||||||
def add_notification_channel(self, guild_id: int, channel_id: int, kind: str) -> bool:
|
def add_notification_channel(self, guild_id: int, channel_id: int, kind: str) -> bool:
|
||||||
if channel_id in self.get_kind_notification_channel_ids(kind):
|
if channel_id in self.get_kind_notification_channel_ids(kind):
|
||||||
@ -185,31 +214,50 @@ class DiscordDB:
|
|||||||
|
|
||||||
def get_kind_notification_channel_ids(self, kind: str) -> List[int]:
|
def get_kind_notification_channel_ids(self, kind: str) -> List[int]:
|
||||||
channels = [row["channel_id"] for row in self.channel.rows_where("kind = ?", [kind])]
|
channels = [row["channel_id"] for row in self.channel.rows_where("kind = ?", [kind])]
|
||||||
logger.info(f"Found {len(channels)} channels for {kind} kind: {channels}")
|
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
def get_notification_channel_id(self, kind: str, *, guild_id: int = None, channel_id: int = None) -> Optional[int]:
|
||||||
|
if guild_id is None and channel_id is None:
|
||||||
|
raise RuntimeError("Must provide either guild_id or channel_id!")
|
||||||
|
for row in self.channel.rows_where(f"kind = ? and {'guild_id' if guild_id is not None else 'channel_id'} = ?", [kind, guild_id or channel_id]):
|
||||||
|
return row["id"]
|
||||||
|
|
||||||
def remove_kind_notification_channel(self, kind, channel_id) -> bool:
|
def remove_kind_notification_channel(self, kind, channel_id) -> bool:
|
||||||
if channel_id in self.get_kind_notification_channel_ids(kind):
|
if channel_id in self.get_kind_notification_channel_ids(kind):
|
||||||
logger.warning(f"removing channel with id {channel_id}")
|
self.remove_all_channel_role_mappings(channel_id, kind)
|
||||||
self.channel.delete_where("kind = ? and channel_id = ?", (kind, channel_id))
|
self.channel.delete_where("kind = ? and channel_id = ?", (kind, channel_id))
|
||||||
self.remove_role_mappings(channel_id)
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_role_mappings(self, channel_id: int):
|
# Role mapping methods
|
||||||
return self.role_mapping.delete_where("channel_id = ?", (channel_id,))
|
|
||||||
|
|
||||||
def add_role_mapping_entry(self, channel_id: int, division: int, role_id: int) -> bool:
|
def add_role_mapping_entry(self, kind: str, channel_id: int, division: int, role_id: int) -> bool:
|
||||||
|
ch_id = self.get_notification_channel_id(kind, channel_id=channel_id)
|
||||||
if division not in (1, 2, 3, 4, 11):
|
if division not in (1, 2, 3, 4, 11):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
row = next(self.role_mapping.rows_where("channel_id = ? and division = ?", [channel_id, division]))
|
row = next(self.role_mapping.rows_where("channel_id = ? and division = ?", [ch_id, division]))
|
||||||
self.role_mapping.update(row["id"], {"channel_id": channel_id, "division": division, "role_id": role_id})
|
self.role_mapping.update(row["id"], {"channel_id": ch_id, "division": division, "role_id": role_id})
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
self.role_mapping.insert({"channel_id": channel_id, "division": division, "role_id": role_id})
|
self.role_mapping.insert({"channel_id": ch_id, "division": division, "role_id": role_id})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_role_id_for_channel_division(self, channel_id: int, division: int) -> Optional[int]:
|
def remove_all_channel_role_mappings(self, channel_id: int, kind: str):
|
||||||
rows = self.role_mapping.rows_where("channel_id = ? and division = ?", (channel_id, division))
|
ch_id = self.get_notification_channel_id(kind, channel_id=channel_id)
|
||||||
|
for d in (1, 2, 3, 4, 11):
|
||||||
|
self.remove_role_mapping(kind, ch_id, d)
|
||||||
|
|
||||||
|
def remove_role_mapping(self, kind: str, channel_id: int, division_id: int) -> bool:
|
||||||
|
try:
|
||||||
|
ch_id = self.get_notification_channel_id(kind, channel_id=channel_id)
|
||||||
|
row = next(self.role_mapping.rows_where("channel_id = ? and division = ? ", (ch_id, division_id)))
|
||||||
|
self.role_mapping.delete(row["id"])
|
||||||
|
return True
|
||||||
|
except StopIteration:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_role_id_for_channel_division(self, *, kind: str, channel_id: int, division: int) -> Optional[int]:
|
||||||
|
ch_id = self.get_notification_channel_id(kind, channel_id=channel_id)
|
||||||
|
rows = self.role_mapping.rows_where("channel_id = ? and division = ?", (ch_id, division))
|
||||||
for row in rows:
|
for row in rows:
|
||||||
return row["role_id"]
|
return row["role_id"]
|
||||||
|
@ -1,47 +1,18 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from json import JSONDecodeError
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import feedparser
|
import feedparser
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
from discord.ext import commands
|
from constants import events
|
||||||
from erepublik.constants import COUNTRIES
|
from erepublik.constants import COUNTRIES
|
||||||
|
|
||||||
from constants import events
|
from dbot.base import ADMIN_ID, DB, DB_NAME, DEFAULT_CHANNEL_ID, DISCORD_TOKEN, PRODUCTION, logger
|
||||||
from db import DiscordDB
|
from dbot.bot_commands import bot
|
||||||
from dbot.utils import timestamp, check_battles
|
from dbot.utils import check_battles, get_battle_page, timestamp
|
||||||
|
|
||||||
APP_NAME = "discord_bot"
|
|
||||||
|
|
||||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
|
||||||
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("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)
|
|
||||||
|
|
||||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
|
||||||
DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096)
|
|
||||||
ADMIN_ID = os.getenv("ADMIN_ID", 220849530730577920)
|
|
||||||
DB_NAME = os.getenv("DB_NAME", "discord.db")
|
|
||||||
PRODUCTION = bool(os.getenv("PRODUCTION"))
|
|
||||||
DB = DiscordDB(DB_NAME)
|
|
||||||
|
|
||||||
if PRODUCTION:
|
if PRODUCTION:
|
||||||
logger.warning("Production mode enabled!")
|
logger.warning("Production mode enabled!")
|
||||||
@ -53,37 +24,18 @@ if PRODUCTION:
|
|||||||
|
|
||||||
logger.debug(f"Active configs:\nDISCORD_TOKEN='{DISCORD_TOKEN}'\nDEFAULT_CHANNEL_ID='{DEFAULT_CHANNEL_ID}'\nADMIN_ID='{ADMIN_ID}'\nDB_NAME='{DB_NAME}'")
|
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"}
|
|
||||||
|
|
||||||
__last_battle_response = None
|
|
||||||
__last_battle_update_timestamp = 0
|
|
||||||
|
|
||||||
|
|
||||||
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://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()))
|
|
||||||
return __last_battle_response
|
|
||||||
|
|
||||||
|
|
||||||
class MyClient(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
|
# create the background task and run it in the background
|
||||||
self.last_event_timestamp = timestamp()
|
self.last_event_timestamp = timestamp()
|
||||||
self.bg_task = self.loop.create_task(self.report_battle_events())
|
|
||||||
self.bg_rss_task = self.loop.create_task(self.report_rss_events())
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
logger.info("Client running")
|
logger.info("Client running")
|
||||||
logger.info("------")
|
logger.info("------")
|
||||||
|
self.bg_task = self.loop.create_task(self.report_battle_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}")
|
||||||
@ -126,6 +78,7 @@ class MyClient(discord.Client):
|
|||||||
__link = values["link"]
|
__link = values["link"]
|
||||||
entry_link = __link if __link.startswith("http") else f"https://www.erepublik.com{__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})))
|
logger.debug(kind.format.format(**dict(match.groupdict(), **{"current_country": country.name})))
|
||||||
|
logger.debug(entry_link)
|
||||||
is_latvia = country.id == 71
|
is_latvia = country.id == 71
|
||||||
has_latvia = any("Latvia" in v for v in values.values())
|
has_latvia = any("Latvia" in v for v in values.values())
|
||||||
if is_latvia or has_latvia:
|
if is_latvia or has_latvia:
|
||||||
@ -149,8 +102,6 @@ class MyClient(discord.Client):
|
|||||||
logger.debug(f"Message sent: {text}")
|
logger.debug(f"Message sent: {text}")
|
||||||
for channel_id in DB.get_kind_notification_channel_ids("events"):
|
for channel_id in DB.get_kind_notification_channel_ids("events"):
|
||||||
await self.get_channel(channel_id).send(embed=embed)
|
await self.get_channel(channel_id).send(embed=embed)
|
||||||
|
|
||||||
await asyncio.sleep((timestamp() // 300 + 1) * 300 - timestamp())
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("eRepublik event reader ran into a problem!", exc_info=e)
|
logger.error("eRepublik event reader ran into a problem!", exc_info=e)
|
||||||
try:
|
try:
|
||||||
@ -158,7 +109,8 @@ class MyClient(discord.Client):
|
|||||||
f.write(feed_response.text)
|
f.write(feed_response.text)
|
||||||
except (NameError, AttributeError):
|
except (NameError, AttributeError):
|
||||||
logger.error("There was no Response object!", exc_info=e)
|
logger.error("There was no Response object!", exc_info=e)
|
||||||
await asyncio.sleep(10)
|
finally:
|
||||||
|
await asyncio.sleep((timestamp() // 300 + 1) * 300 - timestamp())
|
||||||
|
|
||||||
async def report_battle_events(self):
|
async def report_battle_events(self):
|
||||||
await self.wait_until_ready()
|
await self.wait_until_ready()
|
||||||
@ -176,34 +128,35 @@ class MyClient(discord.Client):
|
|||||||
2: discord.Embed(title="Possibly empty **__last-minute__ D2** medals", description=desc),
|
2: discord.Embed(title="Possibly empty **__last-minute__ D2** medals", description=desc),
|
||||||
3: discord.Embed(title="Possibly empty **__last-minute__ D3** medals", description=desc),
|
3: discord.Embed(title="Possibly empty **__last-minute__ D3** medals", description=desc),
|
||||||
4: discord.Embed(title="Possibly empty **__last-minute__ D4** medals", description=desc),
|
4: discord.Embed(title="Possibly empty **__last-minute__ D4** medals", description=desc),
|
||||||
11: discord.Embed(title="Possibly empty **__last-minute__ Air** medals", description=desc)
|
11: discord.Embed(title="Possibly empty **__last-minute__ Air** medals", description=desc),
|
||||||
}
|
}
|
||||||
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.get_epic(data['div_id']):
|
if kind == "epic" and not DB.check_epic(data["div_id"]):
|
||||||
embed = discord.Embed.from_dict(dict(
|
embed_data = dict(
|
||||||
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']}"
|
footer=f"Round time {data['round_time']}",
|
||||||
))
|
)
|
||||||
logger.debug(f"{embed.title=}, {embed.description=}, {embed.url=}, {embed.footer=}")
|
embed = discord.Embed.from_dict(embed_data)
|
||||||
|
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(channel_id, division=div):
|
if role_id := DB.get_role_id_for_channel_division(kind="epic", channel_id=channel_id, division=div):
|
||||||
await self.get_channel(channel_id).send(f"<@&{role_id}>", 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"])
|
||||||
|
|
||||||
if kind == 'empty' and data['round_time_s'] >= 87 * 60:
|
if kind == "empty" and data["round_time_s"] >= 85 * 60 and not DB.check_empty_medal(data["div_id"]):
|
||||||
empty_divisions[div].add_field(
|
empty_divisions[div].add_field(
|
||||||
name=f"**Battle for {data['region']} {' '.join(data['sides'])}**",
|
name=f"**Battle for {data['region']} {' '.join(data['sides'])}**", value=f"[R{data['zone_id']} | Time {data['round_time']}]({data['url']})"
|
||||||
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():
|
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"):
|
||||||
if role_id := DB.get_role_id_for_channel_division(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}>", embed=e)
|
await self.get_channel(channel_id).send(f"<@&{role_id}> empty medals in late rounds!", embed=e)
|
||||||
else:
|
else:
|
||||||
await self.get_channel(channel_id).send(embed=e)
|
await self.get_channel(channel_id).send(embed=e)
|
||||||
sleep_seconds = r.get("last_updated") + 60 - timestamp()
|
sleep_seconds = r.get("last_updated") + 60 - timestamp()
|
||||||
@ -221,119 +174,6 @@ class MyClient(discord.Client):
|
|||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
client = MyClient()
|
client = MyClient()
|
||||||
bot = commands.Bot(command_prefix="!")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_ready():
|
|
||||||
logger.info("Bot loaded")
|
|
||||||
# print(bot.user.name)
|
|
||||||
# print(bot.user.id)
|
|
||||||
logger.info("------")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def unnotify(ctx, kind: str):
|
|
||||||
if ctx.author.guild_permissions.administrator:
|
|
||||||
channel_id = ctx.channel.id
|
|
||||||
if DB.remove_kind_notification_channel(kind, channel_id):
|
|
||||||
return await ctx.send(f"I wont notify about {kind} in this channel!")
|
|
||||||
return await ctx.send("Nothing to do...")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def notify(ctx, kind: str):
|
|
||||||
if ctx.author.guild_permissions.administrator:
|
|
||||||
guild_id = ctx.guild.id
|
|
||||||
channel_id = ctx.channel.id
|
|
||||||
if kind == "epic":
|
|
||||||
if DB.add_notification_channel(guild_id, channel_id, kind):
|
|
||||||
await ctx.send("I will notify about epics in this channel!")
|
|
||||||
await ctx.send(
|
|
||||||
"If You want for me to also add division mentions write:\n"
|
|
||||||
"`!set_division d1 @role_to_mention`\n"
|
|
||||||
"`!set_division d2 @role_to_mention`\n"
|
|
||||||
"`!set_division d3 @role_to_mention`\n"
|
|
||||||
"`!set_division d4 @role_to_mention`\n"
|
|
||||||
"`!set_division air @role_to_mention`"
|
|
||||||
)
|
|
||||||
elif kind == "events":
|
|
||||||
DB.add_notification_channel(guild_id, channel_id, kind)
|
|
||||||
await ctx.send("I will notify about eLatvia's events in this channel!")
|
|
||||||
elif kind == "empty":
|
|
||||||
DB.add_notification_channel(guild_id, channel_id, kind)
|
|
||||||
await ctx.send("I will notify about empty medals in this channel!")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"Unknown {kind=}")
|
|
||||||
else:
|
|
||||||
return await ctx.send("This command is only available for server administrators")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def set_division(ctx, division: str, role_mention):
|
|
||||||
if not ctx.author.guild_permissions.administrator:
|
|
||||||
return await ctx.send("This command is only available for server administrators")
|
|
||||||
if ctx.channel.id not in DB.get_kind_notification_channel_ids("epic"):
|
|
||||||
return await ctx.send("This command is only available from registered channels!")
|
|
||||||
div_map = dict(D1=1, D2=3, D3=3, D4=4, Air=11)
|
|
||||||
|
|
||||||
if division.title() not in div_map:
|
|
||||||
return await ctx.send(f"Unknown {division=}! Available divisions {', '.join(div_map.keys())}")
|
|
||||||
for role in ctx.guild.roles:
|
|
||||||
if role.mention == role_mention:
|
|
||||||
DB.add_role_mapping_entry(ctx.channel.id, div_map[division.title()], role.id)
|
|
||||||
return await ctx.send(f"Success! For {division.title()} epics I will mention <@&{role.id}>")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"Unable to find the role You mentioned...")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def exit(ctx):
|
|
||||||
if ctx.author.id == ADMIN_ID:
|
|
||||||
await ctx.send(f"{ctx.author.mention} Bye!")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"Labs mēģinājums! Mani nogalināt var tikai <@{ADMIN_ID}>")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def empty(ctx, division):
|
|
||||||
if not ctx.channel.id == 603527159109124096:
|
|
||||||
return await ctx.send("Currently unavailable!")
|
|
||||||
try:
|
|
||||||
div = int(division)
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
div = dict(D1=1, D2=3, D3=3, D4=4, Air=11)[division.title()]
|
|
||||||
except (AttributeError, KeyError) as e:
|
|
||||||
await ctx.send(f"First argument must be a value from: 1, d1, 2, d2, 3, d3, 4, d4, 11, air!")
|
|
||||||
return
|
|
||||||
s_div = {1: "D1", 2: "D2", 3: "D3", 4: "D4", 11: "Air"}[div]
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=f"Possibly empty {s_div} medals",
|
|
||||||
description=f"'Empty' medals are being guessed based on the division wall. Expect false-positives!",
|
|
||||||
)
|
|
||||||
for kind, div_div, data in check_battles(get_battle_page().get('battles')):
|
|
||||||
if kind == 'empty' and div_div == div:
|
|
||||||
embed.add_field(
|
|
||||||
name=f"**Battle for {data['region']} {' '.join(data['sides'])}**",
|
|
||||||
value=f"[R{data['zone_id']} | Time {data['round_time']}]({data['url']})",
|
|
||||||
)
|
|
||||||
if len(embed.fields) >= 10:
|
|
||||||
return await ctx.send(embed=embed)
|
|
||||||
if embed.fields:
|
|
||||||
return await ctx.send(embed=embed)
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"No empty {s_div} medals found")
|
|
||||||
|
|
||||||
|
|
||||||
@empty.error
|
|
||||||
async def division_error(ctx, error):
|
|
||||||
if isinstance(error, (commands.BadArgument, commands.MissingRequiredArgument)):
|
|
||||||
await ctx.send('Division is mandatory, eg, `!empty [1,2,3,4,11, d1,d2,d3,d4,air, D1,D2,D3,D4,Air] [1-120]`')
|
|
||||||
else:
|
|
||||||
await ctx.send('Something went wrong! 😔')
|
|
||||||
logger.exception(error, exc_info=error)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import unittest
|
|
||||||
import re
|
import re
|
||||||
|
import unittest
|
||||||
from sqlite_utils.db import NotFoundError
|
|
||||||
|
|
||||||
from dbot import constants, db
|
from dbot import constants, db
|
||||||
|
|
||||||
@ -11,16 +9,17 @@ class TestDatabase(unittest.TestCase):
|
|||||||
self.db = db.DiscordDB()
|
self.db = db.DiscordDB()
|
||||||
|
|
||||||
def test_member(self):
|
def test_member(self):
|
||||||
member = {"id": 1200, "name": "username"}
|
member = {"id": 1200, "name": "username", "pm_is_allowed": False}
|
||||||
self.db.add_member(**member)
|
self.assertTrue(self.db.add_member(**member))
|
||||||
self.assertEqual(self.db.add_member(**member), member)
|
self.assertEqual(self.db.add_member(**member), member)
|
||||||
|
|
||||||
self.assertRaises(NotFoundError, self.db.get_member, member_id=100)
|
self.assertFalse(self.db.get_member(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")
|
member.update(name="Success")
|
||||||
self.assertTrue(self.db.update_member(member["id"], member["name"]))
|
self.assertTrue(self.db.update_member(member["id"], member["name"]))
|
||||||
self.assertEqual(self.db.get_member(member_id=member["id"]), member)
|
self.assertEqual(self.db.get_member(member_id=member["id"]), member)
|
||||||
|
self.assertTrue(self.db.update_member(100, member["name"]))
|
||||||
|
|
||||||
def test_player(self):
|
def test_player(self):
|
||||||
player = {"id": 1, "name": "plato"}
|
player = {"id": 1, "name": "plato"}
|
||||||
@ -35,31 +34,41 @@ class TestDatabase(unittest.TestCase):
|
|||||||
self.assertEqual(self.db.get_player(player["id"]), player)
|
self.assertEqual(self.db.get_player(player["id"]), player)
|
||||||
|
|
||||||
def test_epic(self):
|
def test_epic(self):
|
||||||
self.assertFalse(self.db.get_epic(123456))
|
self.assertFalse(self.db.check_epic(123456))
|
||||||
self.assertTrue(self.db.add_epic(123456))
|
self.assertTrue(self.db.add_epic(123456))
|
||||||
self.assertFalse(self.db.add_epic(123456))
|
self.assertFalse(self.db.add_epic(123456))
|
||||||
self.assertTrue(self.db.get_epic(123456))
|
self.assertTrue(self.db.check_epic(123456))
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
self.assertFalse(self.db.check_empty_medal(123456))
|
||||||
|
self.assertTrue(self.db.add_empty_medal(123456))
|
||||||
|
self.assertFalse(self.db.add_empty_medal(123456))
|
||||||
|
self.assertTrue(self.db.check_empty_medal(123456))
|
||||||
|
|
||||||
def test_rss_feed(self):
|
def test_rss_feed(self):
|
||||||
self.assertEqual(self.db.get_rss_feed_timestamp(71), 0.0)
|
self.assertEqual(self.db.get_rss_feed_timestamp(71), 0.0)
|
||||||
self.db.set_rss_feed_timestamp(71, 16000000)
|
self.db.set_rss_feed_timestamp(71, 16000000)
|
||||||
self.assertEqual(self.db.get_rss_feed_timestamp(71), 16000000.0)
|
self.assertEqual(self.db.get_rss_feed_timestamp(71), 16000000.0)
|
||||||
|
self.db.set_rss_feed_timestamp(71, 16000001)
|
||||||
|
self.assertEqual(self.db.get_rss_feed_timestamp(71), 16000001.0)
|
||||||
|
|
||||||
def test_channels(self):
|
def test_channels(self):
|
||||||
self.assertTrue(self.db.add_notification_channel(13, 16, "epic"))
|
kind = "epic"
|
||||||
self.assertFalse(self.db.add_notification_channel(13, 16, "epic"))
|
self.assertTrue(self.db.add_notification_channel(13, 16, kind))
|
||||||
self.assertListEqual(self.db.get_kind_notification_channel_ids("epic"), [16])
|
self.assertFalse(self.db.add_notification_channel(13, 16, kind))
|
||||||
self.assertFalse(self.db.add_role_mapping_entry(16, 5, 160003))
|
self.assertListEqual(self.db.get_kind_notification_channel_ids(kind), [16])
|
||||||
self.assertTrue(self.db.add_role_mapping_entry(16, 3, 160003))
|
self.assertFalse(self.db.add_role_mapping_entry(kind, 16, 5, 160003))
|
||||||
self.assertTrue(self.db.add_role_mapping_entry(16, 4, 160003))
|
self.assertTrue(self.db.add_role_mapping_entry(kind, 16, 3, 160003))
|
||||||
self.assertTrue(self.db.add_role_mapping_entry(16, 4, 160004))
|
self.assertTrue(self.db.add_role_mapping_entry(kind, 16, 4, 160003))
|
||||||
self.assertEqual(self.db.get_role_id_for_channel_division(16, 3), 160003)
|
self.assertTrue(self.db.add_role_mapping_entry(kind, 16, 4, 160004))
|
||||||
self.assertEqual(self.db.get_role_id_for_channel_division(16, 4), 160004)
|
self.assertEqual(self.db.get_role_id_for_channel_division(kind=kind, channel_id=16, division=3), 160003)
|
||||||
self.assertTrue(self.db.remove_kind_notification_channel("epic", 16))
|
self.assertEqual(self.db.get_role_id_for_channel_division(kind=kind, channel_id=16, division=4), 160004)
|
||||||
self.assertFalse(self.db.remove_kind_notification_channel("epic", 16))
|
self.assertTrue(self.db.remove_role_mapping(kind, 16, 3))
|
||||||
self.assertFalse(self.db.get_role_id_for_channel_division(16, 3))
|
self.assertTrue(self.db.remove_kind_notification_channel(kind, 16))
|
||||||
self.assertFalse(self.db.get_role_id_for_channel_division(16, 4))
|
self.assertFalse(self.db.remove_kind_notification_channel(kind, 16))
|
||||||
self.assertFalse(self.db.get_role_id_for_channel_division(16, 5))
|
self.assertFalse(self.db.get_role_id_for_channel_division(kind=kind, channel_id=16, division=4))
|
||||||
|
self.assertFalse(self.db.get_role_id_for_channel_division(kind=kind, channel_id=16, division=5))
|
||||||
|
self.assertRaises(RuntimeError, self.db.get_notification_channel_id, "non-existant")
|
||||||
|
|
||||||
|
|
||||||
class TestRegexes(unittest.TestCase):
|
class TestRegexes(unittest.TestCase):
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from json import JSONDecodeError
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Union, Dict, Any, Generator, Tuple, List
|
from typing import Any, Dict, Generator, Tuple, Union
|
||||||
|
|
||||||
from dbot.constants import UTF_FLAG
|
import requests as requests
|
||||||
|
|
||||||
|
from dbot.base import logger
|
||||||
|
from dbot.constants import UTF_FLAG, DivisionData
|
||||||
|
|
||||||
|
LAST_BATTLE_RESPONSE = None
|
||||||
|
LAST_BATTLE_UPDATE_TIMESTAMP = 0
|
||||||
|
|
||||||
|
|
||||||
def timestamp_to_datetime(timestamp: int) -> datetime.datetime:
|
def timestamp_to_datetime(ts: int) -> datetime.datetime:
|
||||||
return datetime.datetime.fromtimestamp(timestamp)
|
return datetime.datetime.fromtimestamp(ts)
|
||||||
|
|
||||||
|
|
||||||
def timestamp() -> int:
|
def timestamp() -> int:
|
||||||
@ -21,7 +28,7 @@ def s_to_human(seconds: Union[int, float]) -> str:
|
|||||||
return f"{h:01d}:{m:02d}:{s:02d}"
|
return f"{h:01d}:{m:02d}:{s:02d}"
|
||||||
|
|
||||||
|
|
||||||
def check_battles(battle_json: Dict[str, Dict[str, Any]]) -> Generator[Tuple[str, int, Dict[str, Union[str, List[str], int, Dict[str, Union[str, int]]]]], None, None]:
|
def check_battles(battle_json: Dict[str, Dict[str, Any]]) -> Generator[Tuple[str, int, DivisionData], None, None]:
|
||||||
for battle in sorted(battle_json.values(), key=itemgetter("start")):
|
for battle in sorted(battle_json.values(), key=itemgetter("start")):
|
||||||
if battle["start"] > timestamp():
|
if battle["start"] > timestamp():
|
||||||
continue
|
continue
|
||||||
@ -29,38 +36,51 @@ def check_battles(battle_json: Dict[str, Dict[str, Any]]) -> Generator[Tuple[str
|
|||||||
invader_flag = UTF_FLAG[battle["inv"]["id"]]
|
invader_flag = UTF_FLAG[battle["inv"]["id"]]
|
||||||
defender_flag = UTF_FLAG[battle["def"]["id"]]
|
defender_flag = UTF_FLAG[battle["def"]["id"]]
|
||||||
for div in battle["div"].values():
|
for div in battle["div"].values():
|
||||||
if div['end']:
|
if div["end"]:
|
||||||
continue
|
continue
|
||||||
division = div['div']
|
division = div["div"]
|
||||||
dom = div["wall"]["dom"]
|
dom = div["wall"]["dom"]
|
||||||
epic = div["epic"]
|
epic = div["epic"]
|
||||||
division_meta_data = dict(
|
division_meta_data = DivisionData(
|
||||||
region=region_name,
|
region=region_name,
|
||||||
round_time=s_to_human(timestamp() - battle["start"]),
|
round_time=s_to_human(timestamp() - battle["start"]),
|
||||||
round_time_s=int(timestamp() - battle["start"]),
|
round_time_s=int(timestamp() - battle["start"]),
|
||||||
sides=[],
|
sides=[],
|
||||||
url=f"https://www.erepublik.com/en/military/battlefield/{battle['id']}",
|
url=f"https://www.erepublik.com/en/military/battlefield/{battle['id']}",
|
||||||
zone_id=battle["zone_id"],
|
zone_id=battle["zone_id"],
|
||||||
div_id=div['id'],
|
div_id=div["id"],
|
||||||
extra={}
|
extra={},
|
||||||
)
|
)
|
||||||
if dom == 50:
|
if dom == 50:
|
||||||
division_meta_data.update(sides=[invader_flag, defender_flag])
|
division_meta_data.update(sides=[invader_flag, defender_flag])
|
||||||
yield 'empty', division, division_meta_data
|
yield "empty", division, division_meta_data
|
||||||
division_meta_data['sides'].clear()
|
division_meta_data["sides"].clear()
|
||||||
if dom == 100:
|
if dom == 100:
|
||||||
division_meta_data.update(sides=[invader_flag if battle["def"]["id"] == div["wall"]["for"] else defender_flag])
|
division_meta_data.update(sides=[invader_flag if battle["def"]["id"] == div["wall"]["for"] else defender_flag])
|
||||||
yield 'empty', division, division_meta_data
|
yield "empty", division, division_meta_data
|
||||||
division_meta_data['sides'].clear()
|
division_meta_data["sides"].clear()
|
||||||
if epic > 1:
|
if epic > 1:
|
||||||
division_meta_data.update(sides=[invader_flag, defender_flag])
|
division_meta_data.update(sides=[invader_flag, defender_flag])
|
||||||
division_meta_data['extra'].update(intensity_scale=div['intensity_scale'],
|
division_meta_data["extra"].update(intensity_scale=div["intensity_scale"], epic_type=epic)
|
||||||
epic_type=epic)
|
yield "epic", division, division_meta_data
|
||||||
yield 'epic', division, division_meta_data
|
division_meta_data["sides"].clear()
|
||||||
division_meta_data['sides'].clear()
|
division_meta_data["extra"].clear()
|
||||||
division_meta_data['extra'].clear()
|
|
||||||
if dom >= 66.8:
|
if dom >= 66.8:
|
||||||
division_meta_data.update(sides=[invader_flag if battle["def"]["id"] == div["wall"]["for"] else defender_flag])
|
division_meta_data.update(sides=[invader_flag if battle["def"]["id"] == div["wall"]["for"] else defender_flag])
|
||||||
yield 'steal', division, division_meta_data
|
yield "steal", division, division_meta_data
|
||||||
division_meta_data['sides'].clear()
|
division_meta_data["sides"].clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
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://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()))
|
||||||
|
return LAST_BATTLE_RESPONSE
|
||||||
|
Loading…
x
Reference in New Issue
Block a user