Package aethersprite
Aethersprite Discord bot/framework
Expand source code
"""Aethersprite Discord bot/framework"""
# stdlib
from importlib import import_module
import logging
from os import environ
from os.path import isfile, sep
from typing import Optional
from random import seed
# 3rd party
import colorlog
from discord import Activity, ActivityType, DMChannel, Intents, Message
from discord.ext.commands import (
Bot,
CheckFailure,
command,
CommandNotFound,
Context,
)
from pretty_help import PrettyHelp
import toml
config = {
"bot": {
"data_folder": ".",
"extensions": ["aethersprite.extensions.base._all"],
"help_command": "aehelp",
"log_level": "INFO",
},
"webapp": {
"proxies": None,
"flask": {
"SERVER_NAME": "localhost",
"SERVER_HOST": "0.0.0.0",
"SERVER_PORT": 5000,
},
},
}
"""Configuration"""
# Load config from file and merge with defaults
config_file = environ.get("AETHERSPRITE_CONFIG", "config.toml")
if isfile(config_file):
config = {**config, **toml.load(config_file)}
data_folder = f"{config['bot']['data_folder']}{sep}"
log = logging.getLogger(__name__)
"""Root logger instance"""
log.setLevel(getattr(logging, config["bot"].get("log_level", "INFO")))
_help = config["bot"].get("help_command", "aehelp")
@command(name=_help, hidden=True)
async def help_proxy(ctx: Context, command: Optional[str] = None):
if command is None:
await ctx.send_help()
else:
await ctx.send_help(command)
class _MyHelp(PrettyHelp):
def __init__(self):
super().__init__(delete_invoke=True)
@property
def invoked_with(self):
command_name = _help
ctx = self.context
if (
ctx is None
or ctx.command is None
or ctx.command.qualified_name != command_name
):
return command_name
return ctx.invoked_with
# colored log output
streamHandler = logging.StreamHandler()
streamHandler.setFormatter(
colorlog.ColoredFormatter(
"{asctime} {log_color}{levelname:<7}{reset} "
"{bold_white}{module}:{funcName}{reset} {cyan}\u00bb{reset} {message}",
style="{",
)
)
log.addHandler(streamHandler)
activity = Activity(name=f"@me {_help}", type=ActivityType.listening)
"""Activity on login"""
intents: Intents = Intents.default()
intents.members = True
intents.message_content = True
_helpcmd = _MyHelp()
def get_prefixes(bot: Bot, message: Message):
from .settings import settings
assert bot.user
user_id = bot.user.id
base = [f"<@!{user_id}> ", f"<@{user_id}> "]
default = [config.get("bot", {}).get("prefix", "!")]
if "prefix" not in settings:
return base + default
prefix = settings["prefix"].get(message) # type: ignore
if prefix is None:
return base + default
return base + [prefix]
bot = Bot(command_prefix=get_prefixes, intents=intents, help_command=_helpcmd)
"""The bot itself"""
@bot.event
async def on_connect():
log.info("Connected to Discord")
@bot.event
async def on_disconnect():
log.info("Disconnected")
@bot.event
async def on_error(method: str, *args, **kwargs):
log.exception(f"Error in method {method}\nargs: {args}\nkwargs: {kwargs}\n")
@bot.event
async def on_command_error(ctx: Context, error: Exception):
"""Suppress command check failures and invalid commands."""
from .extensions.base.alias import Alias
if isinstance(error, CheckFailure):
return
if isinstance(error, CommandNotFound):
if isinstance(ctx.channel, DMChannel):
return
bot: Bot = ctx.bot
cog: Alias | None = bot.get_cog("Alias") # type: ignore
if cog is None:
return
assert ctx.guild
guild = str(ctx.guild.id)
aliases = cog.aliases[guild] if guild in cog.aliases else None
if aliases is None:
return
assert ctx.prefix
name = ctx.message.content.replace(ctx.prefix, "").split(" ")[0].strip()
if name not in aliases:
return
cmd = ctx.bot.get_command(aliases[name])
if cmd is None:
return
ctx.command = cmd
return await ctx.bot.invoke(ctx)
raise error
@bot.event
async def on_ready():
log.info(f"Logged in as {bot.user}")
await bot.change_presence(activity=activity)
@bot.event
async def on_resumed():
log.info("Connection resumed")
async def _load_ext(ext: str, package: str | None = None):
mod = import_module(ext, package)
if hasattr(mod, "META_EXTENSION") and mod.META_EXTENSION:
for child in mod._mods:
await _load_ext(f"..{child}", ext)
return
if not hasattr(mod, "setup"):
return
log.info(f"Bot extension setup: {mod.__name__}")
await bot.load_extension(mod.__name__)
async def entrypoint():
token = config["bot"].get("token", environ.get("DISCORD_TOKEN", None))
# need credentials
assert (
token is not None
), "bot.token not in config and DISCORD_TOKEN not in env variables"
# for any commands or scheduled tasks, etc. that need random numbers
seed()
bot.remove_command("help")
bot.add_command(help_proxy)
# probe extensions for bot hooks
for ext in config["bot"]["extensions"]:
await _load_ext(ext)
if log.level >= logging.DEBUG:
for key, evs in bot.extra_events.items():
out = []
for f in evs:
out.append(f"{f.__module__}:{f.__name__}")
log.debug(f"{key} => {out!r}")
# here we go!
await bot.start(token=token)
import_module(".webapp", __name__)
Sub-modules
aethersprite.authz
-
Authorization module
aethersprite.common
-
Common functions module
aethersprite.emotes
-
Emote constants
aethersprite.extensions
-
Aethersprite extensions
aethersprite.filters
-
Setting filters module
aethersprite.settings
-
Settings module; interfaced with via
aethersprite.extensions.base.settings
… aethersprite.webapp
-
Web application
Global variables
var activity
-
Activity on login
var bot
-
The bot itself
var config
-
Configuration
var log
-
Root logger instance
Functions
async def entrypoint()
-
Expand source code
async def entrypoint(): token = config["bot"].get("token", environ.get("DISCORD_TOKEN", None)) # need credentials assert ( token is not None ), "bot.token not in config and DISCORD_TOKEN not in env variables" # for any commands or scheduled tasks, etc. that need random numbers seed() bot.remove_command("help") bot.add_command(help_proxy) # probe extensions for bot hooks for ext in config["bot"]["extensions"]: await _load_ext(ext) if log.level >= logging.DEBUG: for key, evs in bot.extra_events.items(): out = [] for f in evs: out.append(f"{f.__module__}:{f.__name__}") log.debug(f"{key} => {out!r}") # here we go! await bot.start(token=token)
def get_prefixes(bot: discord.ext.commands.bot.Bot, message: discord.message.Message)
-
Expand source code
def get_prefixes(bot: Bot, message: Message): from .settings import settings assert bot.user user_id = bot.user.id base = [f"<@!{user_id}> ", f"<@{user_id}> "] default = [config.get("bot", {}).get("prefix", "!")] if "prefix" not in settings: return base + default prefix = settings["prefix"].get(message) # type: ignore if prefix is None: return base + default return base + [prefix]
async def on_command_error(ctx: discord.ext.commands.context.Context, error: Exception)
-
Suppress command check failures and invalid commands.
Expand source code
@bot.event async def on_command_error(ctx: Context, error: Exception): """Suppress command check failures and invalid commands.""" from .extensions.base.alias import Alias if isinstance(error, CheckFailure): return if isinstance(error, CommandNotFound): if isinstance(ctx.channel, DMChannel): return bot: Bot = ctx.bot cog: Alias | None = bot.get_cog("Alias") # type: ignore if cog is None: return assert ctx.guild guild = str(ctx.guild.id) aliases = cog.aliases[guild] if guild in cog.aliases else None if aliases is None: return assert ctx.prefix name = ctx.message.content.replace(ctx.prefix, "").split(" ")[0].strip() if name not in aliases: return cmd = ctx.bot.get_command(aliases[name]) if cmd is None: return ctx.command = cmd return await ctx.bot.invoke(ctx) raise error
async def on_connect()
-
Expand source code
@bot.event async def on_connect(): log.info("Connected to Discord")
async def on_disconnect()
-
Expand source code
@bot.event async def on_disconnect(): log.info("Disconnected")
async def on_error(method: str, *args, **kwargs)
-
Expand source code
@bot.event async def on_error(method: str, *args, **kwargs): log.exception(f"Error in method {method}\nargs: {args}\nkwargs: {kwargs}\n")
async def on_ready()
-
Expand source code
@bot.event async def on_ready(): log.info(f"Logged in as {bot.user}") await bot.change_presence(activity=activity)
async def on_resumed()
-
Expand source code
@bot.event async def on_resumed(): log.info("Connection resumed")