2019-12-06 01:19:42 -05:00
|
|
|
"""
|
|
|
|
Common tools for the bot.
|
|
|
|
---
|
2021-03-28 13:57:03 -04:00
|
|
|
Copyright (C) 2019-2021 classabbyamp, 0x5c
|
2019-12-06 01:19:42 -05:00
|
|
|
|
2021-06-26 20:23:55 -04:00
|
|
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
2019-12-06 01:19:42 -05:00
|
|
|
"""
|
|
|
|
|
2019-12-07 11:38:34 -05:00
|
|
|
|
2019-12-23 20:51:31 -05:00
|
|
|
import collections
|
2021-03-28 02:38:36 -04:00
|
|
|
import enum
|
2019-12-23 20:51:31 -05:00
|
|
|
import json
|
2020-01-08 02:19:35 -05:00
|
|
|
import re
|
2019-12-07 11:38:34 -05:00
|
|
|
import traceback
|
|
|
|
from datetime import datetime
|
2020-01-08 02:19:35 -05:00
|
|
|
from pathlib import Path
|
2019-12-06 01:19:42 -05:00
|
|
|
from types import SimpleNamespace
|
2020-10-28 23:22:28 -04:00
|
|
|
from typing import Union
|
2019-12-06 01:19:42 -05:00
|
|
|
|
2020-01-27 00:37:52 -05:00
|
|
|
import aiohttp
|
2023-01-13 03:31:42 -05:00
|
|
|
import httpx
|
2020-01-27 00:37:52 -05:00
|
|
|
|
2019-12-07 11:38:34 -05:00
|
|
|
import discord
|
|
|
|
import discord.ext.commands as commands
|
2022-12-30 15:01:48 -05:00
|
|
|
from discord import Emoji, PartialEmoji
|
2019-12-07 11:38:34 -05:00
|
|
|
|
2019-12-08 04:10:19 -05:00
|
|
|
import data.options as opt
|
|
|
|
|
|
|
|
|
2021-03-28 02:38:36 -04:00
|
|
|
__all__ = ["colours", "BoltCats", "Cats", "emojis", "paths", "ImageMetadata", "ImagesGroup",
|
2020-01-08 02:19:35 -05:00
|
|
|
"embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
|
2019-12-07 11:38:34 -05:00
|
|
|
|
|
|
|
|
|
|
|
# --- Common values ---
|
2019-12-06 01:19:42 -05:00
|
|
|
|
2020-01-30 06:55:11 -05:00
|
|
|
colours = SimpleNamespace(
|
|
|
|
good=0x43B581,
|
|
|
|
neutral=0x7289DA,
|
|
|
|
bad=0xF04747,
|
2021-02-28 01:51:19 -05:00
|
|
|
timeout=0xF26522,
|
2020-01-30 06:55:11 -05:00
|
|
|
)
|
|
|
|
|
2021-03-28 02:38:36 -04:00
|
|
|
|
|
|
|
class BoltCats(enum.Enum):
|
2021-04-11 15:20:42 -04:00
|
|
|
OTHER = "Other"
|
2021-03-28 02:38:36 -04:00
|
|
|
INFO = "Bot Information"
|
2021-04-11 15:20:42 -04:00
|
|
|
ADMIN = "Bot Control"
|
2021-03-28 02:38:36 -04:00
|
|
|
|
|
|
|
|
2019-12-06 01:19:42 -05:00
|
|
|
# meow
|
2021-03-28 02:38:36 -04:00
|
|
|
class Cats(enum.Enum):
|
2021-03-28 09:50:51 -04:00
|
|
|
CALC = "Calculators"
|
|
|
|
CODES = "Code References and Tools"
|
2021-03-28 02:38:36 -04:00
|
|
|
FUN = "Fun"
|
2021-03-28 09:50:51 -04:00
|
|
|
LOOKUP = "Information Lookup"
|
2021-03-28 02:38:36 -04:00
|
|
|
REF = "Reference"
|
|
|
|
STUDY = "Exam Study"
|
2021-03-28 09:50:51 -04:00
|
|
|
TIME = "Time and Time Zones"
|
|
|
|
UTILS = "Utilities"
|
2021-03-28 02:38:36 -04:00
|
|
|
WEATHER = "Land and Space Weather"
|
|
|
|
|
2020-01-30 06:55:11 -05:00
|
|
|
|
|
|
|
emojis = SimpleNamespace(
|
|
|
|
check_mark="✅",
|
|
|
|
x="❌",
|
|
|
|
warning="⚠️",
|
|
|
|
question="❓",
|
|
|
|
no_entry="⛔",
|
|
|
|
bangbang="‼️",
|
2021-04-03 18:06:14 -04:00
|
|
|
clock="🕗",
|
|
|
|
stopwatch="⏱",
|
2020-01-30 06:55:11 -05:00
|
|
|
a="🇦",
|
|
|
|
b="🇧",
|
|
|
|
c="🇨",
|
|
|
|
d="🇩",
|
2021-02-28 01:51:19 -05:00
|
|
|
e="🇪",
|
2020-01-30 06:55:11 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
paths = SimpleNamespace(
|
|
|
|
data=Path("./data/"),
|
2021-03-18 09:17:30 -04:00
|
|
|
resources=Path("./data/resources/"),
|
2020-01-30 06:55:11 -05:00
|
|
|
)
|
2019-12-23 20:51:31 -05:00
|
|
|
|
|
|
|
|
|
|
|
# --- Classes ---
|
|
|
|
|
2020-09-24 19:02:12 -04:00
|
|
|
|
2019-12-23 20:51:31 -05:00
|
|
|
class ImageMetadata:
|
|
|
|
"""Represents the metadata of a single image."""
|
|
|
|
def __init__(self, metadata: list):
|
|
|
|
self.filename: str = metadata[0]
|
|
|
|
self.name: str = metadata[1]
|
|
|
|
self.long_name: str = metadata[2]
|
|
|
|
self.description: str = metadata[3]
|
|
|
|
self.source: str = metadata[4]
|
|
|
|
self.emoji: str = metadata[5]
|
|
|
|
|
|
|
|
|
|
|
|
class ImagesGroup(collections.abc.Mapping):
|
|
|
|
"""Represents a group of images, loaded from a meta.json file."""
|
|
|
|
def __init__(self, file_path):
|
|
|
|
self._images = {}
|
|
|
|
self.path = file_path
|
|
|
|
|
|
|
|
with open(file_path, "r") as file:
|
|
|
|
images: dict = json.load(file)
|
|
|
|
for key, imgdata in images.items():
|
|
|
|
self._images[key] = ImageMetadata(imgdata)
|
|
|
|
|
|
|
|
# Wrappers to implement dict-like functionality
|
|
|
|
def __len__(self):
|
|
|
|
return len(self._images)
|
|
|
|
|
2021-03-27 18:00:28 -04:00
|
|
|
def __getitem__(self, key: str) -> ImageMetadata:
|
2019-12-23 20:51:31 -05:00
|
|
|
return self._images[key]
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return iter(self._images)
|
|
|
|
|
|
|
|
# str(): Simply return what it would be for the underlaying dict
|
|
|
|
def __str__(self):
|
|
|
|
return str(self._images)
|
|
|
|
|
2019-12-07 11:38:34 -05:00
|
|
|
|
2020-01-27 00:37:52 -05:00
|
|
|
# --- Exceptions ---
|
|
|
|
|
|
|
|
class BotHTTPError(Exception):
|
|
|
|
"""Raised whan a requests fails (status != 200) in a command."""
|
2023-01-13 03:31:42 -05:00
|
|
|
def __init__(self, response: aiohttp.ClientResponse | httpx.Response):
|
|
|
|
if isinstance(response, aiohttp.ClientResponse):
|
|
|
|
self.status = response.status
|
|
|
|
self.reason = response.reason
|
|
|
|
else:
|
|
|
|
self.status = response.status_code
|
|
|
|
self.reason = response.reason_phrase
|
|
|
|
msg = f"Request failed: {self.status} {self.reason}"
|
2020-01-27 00:37:52 -05:00
|
|
|
super().__init__(msg)
|
|
|
|
self.response = response
|
|
|
|
|
|
|
|
|
2020-01-08 02:19:35 -05:00
|
|
|
# --- Converters ---
|
|
|
|
|
|
|
|
class GlobalChannelConverter(commands.IDConverter):
|
|
|
|
"""Converter to get any bot-acessible channel by ID/mention (global), or name (in current guild only)."""
|
|
|
|
async def convert(self, ctx: commands.Context, argument: str):
|
|
|
|
bot = ctx.bot
|
|
|
|
guild = ctx.guild
|
2020-01-30 06:15:42 -05:00
|
|
|
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
|
2020-01-08 02:19:35 -05:00
|
|
|
result = None
|
|
|
|
if match is None:
|
|
|
|
# not a mention/ID
|
|
|
|
if guild:
|
|
|
|
result = discord.utils.get(guild.text_channels, name=argument)
|
|
|
|
else:
|
|
|
|
raise commands.BadArgument(f"""Channel named "{argument}" not found in this guild.""")
|
|
|
|
else:
|
|
|
|
channel_id = int(match.group(1))
|
|
|
|
result = bot.get_channel(channel_id)
|
|
|
|
if not isinstance(result, (discord.TextChannel, discord.abc.PrivateChannel)):
|
|
|
|
raise commands.BadArgument(f"""Channel "{argument}" not found.""")
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2019-12-07 11:38:34 -05:00
|
|
|
# --- Helper functions ---
|
|
|
|
|
2019-12-16 03:49:34 -05:00
|
|
|
def embed_factory(ctx: commands.Context) -> discord.Embed:
|
|
|
|
"""Creates an embed with neutral colour and standard footer."""
|
|
|
|
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
|
2022-12-30 15:01:48 -05:00
|
|
|
if ctx.author:
|
|
|
|
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar.url))
|
2019-12-16 03:49:34 -05:00
|
|
|
return embed
|
|
|
|
|
|
|
|
|
2019-12-07 11:38:34 -05:00
|
|
|
def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode: bool) -> discord.Embed:
|
|
|
|
"""Creates an Error embed."""
|
|
|
|
if debug_mode:
|
|
|
|
fmtd_ex = traceback.format_exception(exception.__class__, exception, exception.__traceback__)
|
|
|
|
else:
|
|
|
|
fmtd_ex = traceback.format_exception_only(exception.__class__, exception)
|
2019-12-16 03:49:34 -05:00
|
|
|
embed = embed_factory(ctx)
|
2020-01-03 23:03:17 -05:00
|
|
|
embed.title = "⚠️ Error"
|
2020-01-30 06:15:42 -05:00
|
|
|
embed.description = "```\n" + "\n".join(fmtd_ex) + "```"
|
2019-12-16 03:49:34 -05:00
|
|
|
embed.colour = colours.bad
|
2019-12-07 11:38:34 -05:00
|
|
|
return embed
|
2019-12-08 04:10:19 -05:00
|
|
|
|
|
|
|
|
2022-12-30 15:01:48 -05:00
|
|
|
async def add_react(msg: discord.Message, react: Union[Emoji, PartialEmoji, str]):
|
2019-12-08 04:10:19 -05:00
|
|
|
try:
|
|
|
|
await msg.add_reaction(react)
|
|
|
|
except discord.Forbidden:
|
2020-10-28 23:22:28 -04:00
|
|
|
idpath = (f"{msg.guild.id}/" if msg.guild else "") + str(msg.channel.id)
|
|
|
|
print(f"[!!] Missing permissions to add reaction in '{idpath}'!")
|
2019-12-08 04:10:19 -05:00
|
|
|
|
|
|
|
|
|
|
|
# --- Checks ---
|
|
|
|
|
|
|
|
async def check_if_owner(ctx: commands.Context):
|
|
|
|
if ctx.author.id in opt.owners_uids:
|
|
|
|
return True
|
2020-01-03 23:03:17 -05:00
|
|
|
raise commands.NotOwner
|