Compare commits
4 commits
1dfa03bc17
...
e71558959c
| Author | SHA1 | Date | |
|---|---|---|---|
| e71558959c | |||
| 52d619377e | |||
| 8ffe964386 | |||
| 18f057793e |
5 changed files with 289 additions and 6 deletions
43
src/bot/cogs/debug.py
Normal file
43
src/bot/cogs/debug.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from bot.types import RichEmbed
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Debug(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def ping(self, ctx: commands.Context):
|
||||||
|
_log.debug("pong!")
|
||||||
|
|
||||||
|
embed: RichEmbed = {
|
||||||
|
"type": "rich",
|
||||||
|
"description": "Pong."
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.send(embed=discord.Embed.from_dict(embed))
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
_log.debug("Adding Debug cog")
|
||||||
|
await bot.add_cog(Debug(bot))
|
||||||
|
|
||||||
|
cog = bot.get_cog(Debug.__name__)
|
||||||
|
|
||||||
|
if cog is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
_log.debug("Loaded Debug cog with the following commands:")
|
||||||
|
_log.debug([c.name for c in cog.get_commands()])
|
||||||
|
|
||||||
|
|
||||||
|
async def teardown(bot: commands.Bot):
|
||||||
|
_log.debug("Removing Debug cog")
|
||||||
|
await bot.remove_cog(Debug.__name__)
|
||||||
|
|
@ -11,11 +11,14 @@ from discord.ext import commands
|
||||||
from bot.env import Environment
|
from bot.env import Environment
|
||||||
|
|
||||||
|
|
||||||
_log: logging.Logger
|
_log = logging.getLogger(__name__)
|
||||||
|
_default_extensions: dict[str, str] = {
|
||||||
|
"Debug": "bot.cogs.debug",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_logging() -> logging.Logger:
|
def setup_logging():
|
||||||
root_logger = logging.getLogger(__name__)
|
root_logger = logging.getLogger()
|
||||||
|
|
||||||
log_level_map = {
|
log_level_map = {
|
||||||
"NOTSET": logging.NOTSET,
|
"NOTSET": logging.NOTSET,
|
||||||
|
|
@ -57,8 +60,6 @@ def setup_logging() -> logging.Logger:
|
||||||
log_error_file_handler.setLevel(logging.ERROR)
|
log_error_file_handler.setLevel(logging.ERROR)
|
||||||
root_logger.addHandler(log_error_file_handler)
|
root_logger.addHandler(log_error_file_handler)
|
||||||
|
|
||||||
return root_logger
|
|
||||||
|
|
||||||
|
|
||||||
def setup_bot() -> commands.Bot:
|
def setup_bot() -> commands.Bot:
|
||||||
intents = discord.Intents.all()
|
intents = discord.Intents.all()
|
||||||
|
|
@ -129,6 +130,17 @@ def register_event_handlers(bot: commands.Bot):
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
_log.info("Ready: Connected to Discord")
|
_log.info("Ready: Connected to Discord")
|
||||||
|
|
||||||
|
_log.info("Loading extensions")
|
||||||
|
for ext_name, ext_path in _default_extensions.items():
|
||||||
|
try:
|
||||||
|
_log.info(f'Loading extension "{ext_name}" ({ext_path})')
|
||||||
|
await bot.load_extension(ext_path)
|
||||||
|
_log.info(f'Loaded extension "{ext_name}" ({ext_path})')
|
||||||
|
except Exception as e:
|
||||||
|
_log.error(
|
||||||
|
f'Failed to load extension "{ext_name}" ({ext_path})', exc_info=e
|
||||||
|
)
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_resumed():
|
async def on_resumed():
|
||||||
_log.info("Resumed: Session with Discord resumed")
|
_log.info("Resumed: Session with Discord resumed")
|
||||||
|
|
@ -144,6 +156,7 @@ def register_event_handlers(bot: commands.Bot):
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_message(message: discord.Message):
|
async def on_message(message: discord.Message):
|
||||||
_log.debug(f"Read message ({message.id = }): {message.content}")
|
_log.debug(f"Read message ({message.id = }): {message.content}")
|
||||||
|
await bot.process_commands(message)
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_command(ctx: commands.Context):
|
async def on_command(ctx: commands.Context):
|
||||||
|
|
@ -181,7 +194,7 @@ async def main():
|
||||||
await bot.start(token=token, reconnect=True)
|
await bot.start(token=token, reconnect=True)
|
||||||
|
|
||||||
|
|
||||||
_log = setup_logging()
|
setup_logging()
|
||||||
_log.debug("Logging initialised")
|
_log.debug("Logging initialised")
|
||||||
|
|
||||||
_log.debug(f"{sys.argv = }")
|
_log.debug(f"{sys.argv = }")
|
||||||
|
|
|
||||||
41
src/bot/types/__init__.py
Normal file
41
src/bot/types/__init__.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
|
from .embed import (
|
||||||
|
Embed,
|
||||||
|
RichEmbed,
|
||||||
|
ImageEmbed,
|
||||||
|
VideoEmbed,
|
||||||
|
GifvEmbed,
|
||||||
|
ArticleEmbed,
|
||||||
|
LinkEmbed,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Embed",
|
||||||
|
"RichEmbed",
|
||||||
|
"ImageEmbed",
|
||||||
|
"VideoEmbed",
|
||||||
|
"GifvEmbed",
|
||||||
|
"ArticleEmbed",
|
||||||
|
"LinkEmbed",
|
||||||
|
"ISO8601Timestamp",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ISO8601Timestamp:
|
||||||
|
def __init__(self, dt: date | datetime) -> None:
|
||||||
|
self.dt = dt
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.dt.isoformat()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ISO8601Timestamp({repr(self.dt)})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def today(cls):
|
||||||
|
return cls(date.today())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: timezone | None):
|
||||||
|
return cls(datetime.now(tz))
|
||||||
166
src/bot/types/embed.py
Normal file
166
src/bot/types/embed.py
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
from typing import Annotated, Literal, TypedDict
|
||||||
|
from annotated_types import Len
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from .timestamp import ISO8601Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedFooterReq(TypedDict):
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedFooterOpt(TypedDict, total=False):
|
||||||
|
icon_url: str
|
||||||
|
proxy_icon_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedFooter(_EmbedFooterReq, _EmbedFooterOpt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedImageReq(TypedDict):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedImageOpt(TypedDict, total=False):
|
||||||
|
proxy_url: str
|
||||||
|
height: int
|
||||||
|
width: int
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedImage(_EmbedImageReq, _EmbedImageOpt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedThumbnailReq(TypedDict):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedThumbnailOpt(TypedDict, total=False):
|
||||||
|
proxy_url: str
|
||||||
|
height: int
|
||||||
|
width: int
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedThumbnail(_EmbedThumbnailReq, _EmbedThumbnailOpt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedVideo(TypedDict, total=False):
|
||||||
|
url: str
|
||||||
|
proxy_url: str
|
||||||
|
height: int
|
||||||
|
width: int
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedProvider(TypedDict, total=False):
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedAuthor(TypedDict, total=False):
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
icon_url: str
|
||||||
|
proxy_icon_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedFieldReq(TypedDict):
|
||||||
|
name: str
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedFieldOpt(TypedDict, total=False):
|
||||||
|
inline: bool
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedField(_EmbedFieldReq, _EmbedFieldOpt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _EmbedBase(TypedDict, total=False):
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
url: str
|
||||||
|
timestamp: ISO8601Timestamp
|
||||||
|
color: discord.Colour | int
|
||||||
|
footer: EmbedFooter
|
||||||
|
image: EmbedImage
|
||||||
|
thumbnail: EmbedThumbnail
|
||||||
|
video: EmbedVideo
|
||||||
|
provider: EmbedProvider
|
||||||
|
author: EmbedAuthor
|
||||||
|
fields: Annotated[list[EmbedField], Len(max_length=25)]
|
||||||
|
|
||||||
|
|
||||||
|
class RichEmbedImpl(TypedDict):
|
||||||
|
type: Literal["rich"]
|
||||||
|
|
||||||
|
|
||||||
|
class RichEmbed(_EmbedBase, RichEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ImageEmbedImpl(TypedDict):
|
||||||
|
type: Literal["image"]
|
||||||
|
|
||||||
|
|
||||||
|
class ImageEmbed(_EmbedBase, ImageEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VideoEmbedImpl(TypedDict):
|
||||||
|
type: Literal["video"]
|
||||||
|
|
||||||
|
|
||||||
|
class VideoEmbed(_EmbedBase, VideoEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GifvEmbedImpl(TypedDict):
|
||||||
|
type: Literal["givf"]
|
||||||
|
|
||||||
|
|
||||||
|
class GifvEmbed(_EmbedBase, GifvEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleEmbedImpl(TypedDict):
|
||||||
|
type: Literal["article"]
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleEmbed(_EmbedBase, ArticleEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LinkEmbedImpl(TypedDict):
|
||||||
|
type: Literal["link"]
|
||||||
|
|
||||||
|
|
||||||
|
class LinkEmbed(_EmbedBase, LinkEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PollResultEmbedImpl(TypedDict):
|
||||||
|
type: Literal["poll_result"]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: There are a bunch of fields for poll results, but I *really*
|
||||||
|
# don't need these at the moment:
|
||||||
|
# https://discord.com/developers/docs/resources/message#embed-fields-by-embed-type-poll-result-embed-fields
|
||||||
|
|
||||||
|
|
||||||
|
class PollResultEmbed(_EmbedBase, PollResultEmbedImpl):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
Embed = (
|
||||||
|
RichEmbed
|
||||||
|
| ImageEmbed
|
||||||
|
| VideoEmbed
|
||||||
|
| GifvEmbed
|
||||||
|
| ArticleEmbed
|
||||||
|
| LinkEmbed
|
||||||
|
| PollResultEmbed
|
||||||
|
)
|
||||||
20
src/bot/types/timestamp.py
Normal file
20
src/bot/types/timestamp.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
class ISO8601Timestamp:
|
||||||
|
def __init__(self, dt: date | datetime) -> None:
|
||||||
|
self.dt = dt
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.dt.isoformat()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ISO8601Timestamp({repr(self.dt)})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def today(cls):
|
||||||
|
return cls(date.today())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls, tz: timezone | None):
|
||||||
|
return cls(datetime.now(tz))
|
||||||
Loading…
Add table
Reference in a new issue