Fix typing everywhere, fetch error channel after bot is ready

pull/176/head
Stijn De Clercq 2023-07-08 15:38:17 +02:00
parent feccb88cfd
commit cdfcd094a4
23 changed files with 245 additions and 132 deletions

View File

@ -25,7 +25,7 @@ class Currency(commands.Cog):
super().__init__() super().__init__()
self.client = client self.client = client
@commands.command(name="award") @commands.command(name="award") # type: ignore[arg-type]
@commands.check(is_owner) @commands.check(is_owner)
async def award( async def award(
self, self,
@ -49,7 +49,9 @@ class Currency(commands.Cog):
bank = await crud.get_bank(session, ctx.author.id) bank = await crud.get_bank(session, ctx.author.id)
embed = discord.Embed(title=f"{ctx.author.display_name}'s Bank", colour=discord.Colour.blue()) embed = discord.Embed(title=f"{ctx.author.display_name}'s Bank", colour=discord.Colour.blue())
embed.set_thumbnail(url=ctx.author.avatar.url)
if ctx.author.avatar is not None:
embed.set_thumbnail(url=ctx.author.avatar.url)
embed.add_field(name="Interest level", value=bank.interest_level) embed.add_field(name="Interest level", value=bank.interest_level)
embed.add_field(name="Capacity level", value=bank.capacity_level) embed.add_field(name="Capacity level", value=bank.capacity_level)
@ -57,7 +59,9 @@ class Currency(commands.Cog):
await ctx.reply(embed=embed, mention_author=False) await ctx.reply(embed=embed, mention_author=False)
@bank.group(name="upgrade", aliases=["u", "upgrades"], case_insensitive=True, invoke_without_command=True) @bank.group( # type: ignore[arg-type]
name="upgrade", aliases=["u", "upgrades"], case_insensitive=True, invoke_without_command=True
)
async def bank_upgrades(self, ctx: commands.Context): async def bank_upgrades(self, ctx: commands.Context):
"""List the upgrades you can buy & their prices.""" """List the upgrades you can buy & their prices."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -77,7 +81,7 @@ class Currency(commands.Cog):
await ctx.reply(embed=embed, mention_author=False) await ctx.reply(embed=embed, mention_author=False)
@bank_upgrades.command(name="capacity", aliases=["c"]) @bank_upgrades.command(name="capacity", aliases=["c"]) # type: ignore[arg-type]
async def bank_upgrade_capacity(self, ctx: commands.Context): async def bank_upgrade_capacity(self, ctx: commands.Context):
"""Upgrade the capacity level of your bank.""" """Upgrade the capacity level of your bank."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -88,7 +92,7 @@ class Currency(commands.Cog):
await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False) await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False)
await self.client.reject_message(ctx.message) await self.client.reject_message(ctx.message)
@bank_upgrades.command(name="interest", aliases=["i"]) @bank_upgrades.command(name="interest", aliases=["i"]) # type: ignore[arg-type]
async def bank_upgrade_interest(self, ctx: commands.Context): async def bank_upgrade_interest(self, ctx: commands.Context):
"""Upgrade the interest level of your bank.""" """Upgrade the interest level of your bank."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -99,7 +103,7 @@ class Currency(commands.Cog):
await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False) await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False)
await self.client.reject_message(ctx.message) await self.client.reject_message(ctx.message)
@bank_upgrades.command(name="rob", aliases=["r"]) @bank_upgrades.command(name="rob", aliases=["r"]) # type: ignore[arg-type]
async def bank_upgrade_rob(self, ctx: commands.Context): async def bank_upgrade_rob(self, ctx: commands.Context):
"""Upgrade the rob level of your bank.""" """Upgrade the rob level of your bank."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -110,7 +114,7 @@ class Currency(commands.Cog):
await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False) await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False)
await self.client.reject_message(ctx.message) await self.client.reject_message(ctx.message)
@commands.hybrid_command(name="dinks") @commands.hybrid_command(name="dinks") # type: ignore[arg-type]
async def dinks(self, ctx: commands.Context): async def dinks(self, ctx: commands.Context):
"""Check your Didier Dinks.""" """Check your Didier Dinks."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -118,7 +122,7 @@ class Currency(commands.Cog):
plural = pluralize("Didier Dink", bank.dinks) plural = pluralize("Didier Dink", bank.dinks)
await ctx.reply(f"**{ctx.author.display_name}** has **{bank.dinks}** {plural}.", mention_author=False) await ctx.reply(f"**{ctx.author.display_name}** has **{bank.dinks}** {plural}.", mention_author=False)
@commands.command(name="invest", aliases=["deposit", "dep"]) @commands.command(name="invest", aliases=["deposit", "dep"]) # type: ignore[arg-type]
async def invest(self, ctx: commands.Context, amount: typing.Annotated[typing.Union[str, int], abbreviated_number]): async def invest(self, ctx: commands.Context, amount: typing.Annotated[typing.Union[str, int], abbreviated_number]):
"""Invest `amount` Didier Dinks into your bank. """Invest `amount` Didier Dinks into your bank.
@ -144,7 +148,7 @@ class Currency(commands.Cog):
f"**{ctx.author.display_name}** has invested **{invested}** {plural}.", mention_author=False f"**{ctx.author.display_name}** has invested **{invested}** {plural}.", mention_author=False
) )
@commands.hybrid_command(name="nightly") @commands.hybrid_command(name="nightly") # type: ignore[arg-type]
async def nightly(self, ctx: commands.Context): async def nightly(self, ctx: commands.Context):
"""Claim nightly Didier Dinks.""" """Claim nightly Didier Dinks."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:

View File

@ -13,7 +13,7 @@ class DebugCog(commands.Cog):
self.client = client self.client = client
@overrides @overrides
async def cog_check(self, ctx: commands.Context) -> bool: async def cog_check(self, ctx: commands.Context) -> bool: # type:ignore[override]
return await self.client.is_owner(ctx.author) return await self.client.is_owner(ctx.author)
@commands.command(aliases=["Dev"]) @commands.command(aliases=["Dev"])

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, Union, cast
import discord import discord
from discord import app_commands from discord import app_commands
@ -17,6 +17,7 @@ from didier.exceptions import expect
from didier.menus.bookmarks import BookmarkSource from didier.menus.bookmarks import BookmarkSource
from didier.utils.discord import colours from didier.utils.discord import colours
from didier.utils.discord.assets import get_author_avatar, get_user_avatar from didier.utils.discord.assets import get_author_avatar, get_user_avatar
from didier.utils.discord.channels import NON_MESSAGEABLE_CHANNEL_TYPES
from didier.utils.discord.constants import Limits from didier.utils.discord.constants import Limits
from didier.utils.timer import Timer from didier.utils.timer import Timer
from didier.utils.types.datetime import localize, str_to_date, tz_aware_now from didier.utils.types.datetime import localize, str_to_date, tz_aware_now
@ -60,9 +61,19 @@ class Discord(commands.Cog):
event = await events.get_event_by_id(session, event_id) event = await events.get_event_by_id(session, event_id)
if event is None: if event is None:
return await self.client.log_error(f"Unable to find event with id {event_id}", log_to_discord=True) return await self.client.log_error(f"Unable to find event with id {event_id}.", log_to_discord=True)
channel = self.client.get_channel(event.notification_channel) channel = self.client.get_channel(event.notification_channel)
if channel is None:
return await self.client.log_error(
f"Unable to fetch channel for event `#{event_id}` (id `{event.notification_channel}`)."
)
if isinstance(channel, NON_MESSAGEABLE_CHANNEL_TYPES):
return await self.client.log_error(
f"Channel for event `#{event_id}` (id `{event.notification_channel}`) is not messageable."
)
human_readable_time = localize(event.timestamp).strftime("%A, %B %d %Y - %H:%M") human_readable_time = localize(event.timestamp).strftime("%A, %B %d %Y - %H:%M")
embed = discord.Embed(title=event.name, colour=discord.Colour.blue()) embed = discord.Embed(title=event.name, colour=discord.Colour.blue())
@ -81,7 +92,7 @@ class Discord(commands.Cog):
self.client.loop.create_task(self.timer.update()) self.client.loop.create_task(self.timer.update())
@commands.group(name="birthday", aliases=["bd", "birthdays"], case_insensitive=True, invoke_without_command=True) @commands.group(name="birthday", aliases=["bd", "birthdays"], case_insensitive=True, invoke_without_command=True)
async def birthday(self, ctx: commands.Context, user: discord.User = None): async def birthday(self, ctx: commands.Context, user: Optional[discord.User] = None):
"""Command to check the birthday of `user`. """Command to check the birthday of `user`.
Not passing an argument for `user` will show yours instead. Not passing an argument for `user` will show yours instead.
@ -98,8 +109,10 @@ class Discord(commands.Cog):
day, month = leading("0", str(birthday.birthday.day)), leading("0", str(birthday.birthday.month)) day, month = leading("0", str(birthday.birthday.day)), leading("0", str(birthday.birthday.month))
return await ctx.reply(f"{name or 'Your'} birthday is set to **{day}/{month}**.", mention_author=False) return await ctx.reply(f"{name or 'Your'} birthday is set to **{day}/{month}**.", mention_author=False)
@birthday.command(name="set", aliases=["config"]) @birthday.command(name="set", aliases=["config"]) # type: ignore[arg-type]
async def birthday_set(self, ctx: commands.Context, day: str, user: Optional[discord.User] = None): async def birthday_set(
self, ctx: commands.Context, day: str, user: Optional[Union[discord.User, discord.Member]] = None
):
"""Set your birthday to `day`. """Set your birthday to `day`.
Parsing of the `day`-argument happens in the following order: `DD/MM/YYYY`, `DD/MM/YY`, `DD/MM`. Parsing of the `day`-argument happens in the following order: `DD/MM/YYYY`, `DD/MM/YY`, `DD/MM`.
@ -113,6 +126,9 @@ class Discord(commands.Cog):
if user is None: if user is None:
user = ctx.author user = ctx.author
# Please Mypy
user = cast(Union[discord.User, discord.Member], user)
try: try:
default_year = 2001 default_year = 2001
date = str_to_date(day, formats=["%d/%m/%Y", "%d/%m/%y", "%d/%m"]) date = str_to_date(day, formats=["%d/%m/%Y", "%d/%m/%y", "%d/%m"])
@ -141,7 +157,7 @@ class Discord(commands.Cog):
""" """
# No label: shortcut to display bookmarks # No label: shortcut to display bookmarks
if label is None: if label is None:
return await self.bookmark_search(ctx, query=None) return await self.bookmark_search(ctx, query=None) # type: ignore[arg-type]
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
result = expect( result = expect(
@ -151,7 +167,7 @@ class Discord(commands.Cog):
) )
await ctx.reply(result.jump_url, mention_author=False) await ctx.reply(result.jump_url, mention_author=False)
@bookmark.command(name="create", aliases=["new"]) @bookmark.command(name="create", aliases=["new"]) # type: ignore[arg-type]
async def bookmark_create(self, ctx: commands.Context, label: str, message: Optional[discord.Message]): async def bookmark_create(self, ctx: commands.Context, label: str, message: Optional[discord.Message]):
"""Create a new bookmark for message `message` with label `label`. """Create a new bookmark for message `message` with label `label`.
@ -182,7 +198,7 @@ class Discord(commands.Cog):
# Label isn't allowed # Label isn't allowed
return await ctx.reply(f"Bookmarks cannot be named `{label}`.", mention_author=False) return await ctx.reply(f"Bookmarks cannot be named `{label}`.", mention_author=False)
@bookmark.command(name="delete", aliases=["rm"]) @bookmark.command(name="delete", aliases=["rm"]) # type: ignore[arg-type]
async def bookmark_delete(self, ctx: commands.Context, bookmark_id: str): async def bookmark_delete(self, ctx: commands.Context, bookmark_id: str):
"""Delete the bookmark with id `bookmark_id`. """Delete the bookmark with id `bookmark_id`.
@ -207,7 +223,7 @@ class Discord(commands.Cog):
return await ctx.reply(f"Successfully deleted bookmark `#{bookmark_id_int}`.", mention_author=False) return await ctx.reply(f"Successfully deleted bookmark `#{bookmark_id_int}`.", mention_author=False)
@bookmark.command(name="search", aliases=["list", "ls"]) @bookmark.command(name="search", aliases=["list", "ls"]) # type: ignore[arg-type]
async def bookmark_search(self, ctx: commands.Context, *, query: Optional[str] = None): async def bookmark_search(self, ctx: commands.Context, *, query: Optional[str] = None):
"""Search through the list of bookmarks. """Search through the list of bookmarks.
@ -236,7 +252,7 @@ class Discord(commands.Cog):
modal = CreateBookmark(self.client, message.jump_url) modal = CreateBookmark(self.client, message.jump_url)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@commands.hybrid_command(name="events") @commands.hybrid_command(name="events") # type: ignore[arg-type]
@app_commands.rename(event_id="id") @app_commands.rename(event_id="id")
@app_commands.describe(event_id="The id of the event to fetch. If not passed, all events are fetched instead.") @app_commands.describe(event_id="The id of the event to fetch. If not passed, all events are fetched instead.")
async def events(self, ctx: commands.Context, event_id: Optional[int] = None): async def events(self, ctx: commands.Context, event_id: Optional[int] = None):
@ -276,16 +292,16 @@ class Discord(commands.Cog):
embed.add_field( embed.add_field(
name="Timer", value=discord.utils.format_dt(result_event.timestamp, style="R"), inline=True name="Timer", value=discord.utils.format_dt(result_event.timestamp, style="R"), inline=True
) )
embed.add_field(
name="Channel", channel = self.client.get_channel(result_event.notification_channel)
value=self.client.get_channel(result_event.notification_channel).mention, if channel is not None and not isinstance(channel, NON_MESSAGEABLE_CHANNEL_TYPES):
inline=False, embed.add_field(name="Channel", value=channel.mention, inline=False)
)
embed.description = result_event.description embed.description = result_event.description
return await ctx.reply(embed=embed, mention_author=False) return await ctx.reply(embed=embed, mention_author=False)
@commands.group(name="github", aliases=["gh", "git"], case_insensitive=True, invoke_without_command=True) @commands.group(name="github", aliases=["gh", "git"], case_insensitive=True, invoke_without_command=True)
async def github_group(self, ctx: commands.Context, user: Optional[discord.User] = None): async def github_group(self, ctx: commands.Context, user: Optional[Union[discord.User, discord.Member]] = None):
"""Show a user's GitHub links. """Show a user's GitHub links.
If no user is provided, this shows your links instead. If no user is provided, this shows your links instead.
@ -293,6 +309,9 @@ class Discord(commands.Cog):
# Default to author # Default to author
user = user or ctx.author user = user or ctx.author
# Please Mypy
user = cast(Union[discord.User, discord.Member], user)
embed = discord.Embed(colour=colours.github_white(), title="GitHub Links") embed = discord.Embed(colour=colours.github_white(), title="GitHub Links")
embed.set_author(name=user.display_name, icon_url=get_user_avatar(user)) embed.set_author(name=user.display_name, icon_url=get_user_avatar(user))
@ -324,7 +343,7 @@ class Discord(commands.Cog):
return await ctx.reply(embed=embed, mention_author=False) return await ctx.reply(embed=embed, mention_author=False)
@github_group.command(name="add", aliases=["create", "insert"]) @github_group.command(name="add", aliases=["create", "insert"]) # type: ignore[arg-type]
async def github_add(self, ctx: commands.Context, link: str): async def github_add(self, ctx: commands.Context, link: str):
"""Add a new link into the database.""" """Add a new link into the database."""
# Remove wrapping <brackets> which can be used to escape Discord embeds # Remove wrapping <brackets> which can be used to escape Discord embeds
@ -339,7 +358,7 @@ class Discord(commands.Cog):
await self.client.confirm_message(ctx.message) await self.client.confirm_message(ctx.message)
return await ctx.reply(f"Successfully inserted link `#{gh_link.github_link_id}`.", mention_author=False) return await ctx.reply(f"Successfully inserted link `#{gh_link.github_link_id}`.", mention_author=False)
@github_group.command(name="delete", aliases=["del", "remove", "rm"]) @github_group.command(name="delete", aliases=["del", "remove", "rm"]) # type: ignore[arg-type]
async def github_delete(self, ctx: commands.Context, link_id: str): async def github_delete(self, ctx: commands.Context, link_id: str):
"""Delete the link with it `link_id` from the database. """Delete the link with it `link_id` from the database.
@ -411,7 +430,7 @@ class Discord(commands.Cog):
await message.add_reaction("📌") await message.add_reaction("📌")
return await interaction.response.send_message("📌", ephemeral=True) return await interaction.response.send_message("📌", ephemeral=True)
@commands.hybrid_command(name="snipe") @commands.hybrid_command(name="snipe") # type: ignore[arg-type]
async def snipe(self, ctx: commands.Context): async def snipe(self, ctx: commands.Context):
"""Publicly shame people when they edit or delete one of their messages. """Publicly shame people when they edit or delete one of their messages.

View File

@ -28,7 +28,7 @@ class Fun(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client = client self.client = client
@commands.hybrid_command(name="clap") @commands.hybrid_command(name="clap") # type: ignore[arg-type]
async def clap(self, ctx: commands.Context, *, text: str): async def clap(self, ctx: commands.Context, *, text: str):
"""Clap a message with emojis for extra dramatic effect""" """Clap a message with emojis for extra dramatic effect"""
chars = list(filter(lambda c: c in constants.EMOJI_MAP, text)) chars = list(filter(lambda c: c in constants.EMOJI_MAP, text))
@ -50,10 +50,7 @@ class Fun(commands.Cog):
meme = await generate_meme(self.client.http_session, result, fields) meme = await generate_meme(self.client.http_session, result, fields)
return meme return meme
@commands.hybrid_command( @commands.hybrid_command(name="dadjoke", aliases=["dad", "dj"]) # type: ignore[arg-type]
name="dadjoke",
aliases=["dad", "dj"],
)
async def dad_joke(self, ctx: commands.Context): async def dad_joke(self, ctx: commands.Context):
"""Why does Yoda's code always crash? Because there is no try.""" """Why does Yoda's code always crash? Because there is no try."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -83,13 +80,13 @@ class Fun(commands.Cog):
return await self.memegen_ls_msg(ctx) return await self.memegen_ls_msg(ctx)
if fields is None: if fields is None:
return await self.memegen_preview_msg(ctx, template) return await self.memegen_preview_msg(ctx, template) # type: ignore[arg-type]
async with ctx.typing(): async with ctx.typing():
meme = await self._do_generate_meme(template, shlex.split(fields)) meme = await self._do_generate_meme(template, shlex.split(fields))
return await ctx.reply(meme, mention_author=False) return await ctx.reply(meme, mention_author=False)
@memegen_msg.command(name="list", aliases=["ls"]) @memegen_msg.command(name="list", aliases=["ls"]) # type: ignore[arg-type]
async def memegen_ls_msg(self, ctx: commands.Context): async def memegen_ls_msg(self, ctx: commands.Context):
"""Get a list of all available meme templates. """Get a list of all available meme templates.
@ -100,14 +97,14 @@ class Fun(commands.Cog):
await MemeSource(ctx, results).start() await MemeSource(ctx, results).start()
@memegen_msg.command(name="preview", aliases=["p"]) @memegen_msg.command(name="preview", aliases=["p"]) # type: ignore[arg-type]
async def memegen_preview_msg(self, ctx: commands.Context, template: str): async def memegen_preview_msg(self, ctx: commands.Context, template: str):
"""Generate a preview for the meme template `template`, to see how the fields are structured.""" """Generate a preview for the meme template `template`, to see how the fields are structured."""
async with ctx.typing(): async with ctx.typing():
meme = await self._do_generate_meme(template, []) meme = await self._do_generate_meme(template, [])
return await ctx.reply(meme, mention_author=False) return await ctx.reply(meme, mention_author=False)
@memes_slash.command(name="generate") @memes_slash.command(name="generate") # type: ignore[arg-type]
async def memegen_slash(self, interaction: discord.Interaction, template: str): async def memegen_slash(self, interaction: discord.Interaction, template: str):
"""Generate a meme.""" """Generate a meme."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -116,7 +113,7 @@ class Fun(commands.Cog):
modal = GenerateMeme(self.client, result) modal = GenerateMeme(self.client, result)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@memes_slash.command(name="preview") @memes_slash.command(name="preview") # type: ignore[arg-type]
@app_commands.describe(template="The meme template to use in the preview.") @app_commands.describe(template="The meme template to use in the preview.")
async def memegen_preview_slash(self, interaction: discord.Interaction, template: str): async def memegen_preview_slash(self, interaction: discord.Interaction, template: str):
"""Generate a preview for a meme, to see how the fields are structured.""" """Generate a preview for a meme, to see how the fields are structured."""
@ -134,7 +131,7 @@ class Fun(commands.Cog):
"""Autocompletion for the 'template'-parameter""" """Autocompletion for the 'template'-parameter"""
return self.client.database_caches.memes.get_autocomplete_suggestions(current) return self.client.database_caches.memes.get_autocomplete_suggestions(current)
@app_commands.command() @app_commands.command() # type: ignore[arg-type]
@app_commands.describe(message="The text to convert.") @app_commands.describe(message="The text to convert.")
async def mock(self, interaction: discord.Interaction, message: str): async def mock(self, interaction: discord.Interaction, message: str):
"""Mock a message. """Mock a message.
@ -158,7 +155,7 @@ class Fun(commands.Cog):
return await interaction.followup.send(mock(message)) return await interaction.followup.send(mock(message))
@commands.hybrid_command(name="xkcd") @commands.hybrid_command(name="xkcd") # type: ignore[arg-type]
@app_commands.rename(comic_id="id") @app_commands.rename(comic_id="id")
async def xkcd(self, ctx: commands.Context, comic_id: Optional[int] = None): async def xkcd(self, ctx: commands.Context, comic_id: Optional[int] = None):
"""Fetch comic `#id` from xkcd. """Fetch comic `#id` from xkcd.

View File

@ -159,6 +159,9 @@ class CustomHelpCommand(commands.MinimalHelpCommand):
Code in codeblocks is ignored, as it is used to create examples. Code in codeblocks is ignored, as it is used to create examples.
""" """
description = command.help description = command.help
if description is None:
return ""
codeblocks = re_find_all(r"\n?```.*?```", description, flags=re.DOTALL) codeblocks = re_find_all(r"\n?```.*?```", description, flags=re.DOTALL)
# Regex borrowed from https://stackoverflow.com/a/59843498/13568999 # Regex borrowed from https://stackoverflow.com/a/59843498/13568999
@ -198,13 +201,10 @@ class CustomHelpCommand(commands.MinimalHelpCommand):
return None return None
async def _filter_cogs(self, cogs: list[commands.Cog]) -> list[commands.Cog]: async def _filter_cogs(self, cogs: list[Optional[commands.Cog]]) -> list[commands.Cog]:
"""Filter the list of cogs down to all those that the user can see""" """Filter the list of cogs down to all those that the user can see"""
async def _predicate(cog: Optional[commands.Cog]) -> bool: async def _predicate(cog: commands.Cog) -> bool:
if cog is None:
return False
# Remove cogs that we never want to see in the help page because they # Remove cogs that we never want to see in the help page because they
# don't contain commands, or shouldn't be visible at all # don't contain commands, or shouldn't be visible at all
if not cog.get_commands(): if not cog.get_commands():
@ -220,7 +220,7 @@ class CustomHelpCommand(commands.MinimalHelpCommand):
return True return True
# Filter list of cogs down # Filter list of cogs down
filtered_cogs = [cog for cog in cogs if await _predicate(cog)] filtered_cogs = [cog for cog in cogs if cog is not None and await _predicate(cog)]
return list(sorted(filtered_cogs, key=lambda cog: cog.qualified_name)) return list(sorted(filtered_cogs, key=lambda cog: cog.qualified_name))
def _get_flags_class(self, command: commands.Command) -> Optional[Type[PosixFlags]]: def _get_flags_class(self, command: commands.Command) -> Optional[Type[PosixFlags]]:

View File

@ -1,6 +1,6 @@
import inspect import inspect
import os import os
from typing import Optional from typing import Any, Optional, Union
from discord.ext import commands from discord.ext import commands
@ -76,18 +76,24 @@ class Meta(commands.Cog):
if command_name is None: if command_name is None:
return await ctx.reply(repo_home, mention_author=False) return await ctx.reply(repo_home, mention_author=False)
command: Optional[Union[commands.HelpCommand, commands.Command]]
src: Any
if command_name == "help": if command_name == "help":
command = self.client.help_command command = self.client.help_command
if command is None:
return await ctx.reply(f"Found no command named `{command_name}`.", mention_author=False)
src = type(self.client.help_command) src = type(self.client.help_command)
filename = inspect.getsourcefile(src) filename = inspect.getsourcefile(src)
else: else:
command = self.client.get_command(command_name) command = self.client.get_command(command_name)
if command is None:
return await ctx.reply(f"Found no command named `{command_name}`.", mention_author=False)
src = command.callback.__code__ src = command.callback.__code__
filename = src.co_filename filename = src.co_filename
if command is None:
return await ctx.reply(f"Found no command named `{command_name}`.", mention_author=False)
lines, first_line = inspect.getsourcelines(src) lines, first_line = inspect.getsourcelines(src)
if filename is None: if filename is None:

View File

@ -22,7 +22,7 @@ class Other(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client = client self.client = client
@commands.hybrid_command(name="corona", aliases=["covid", "rona"]) @commands.hybrid_command(name="corona", aliases=["covid", "rona"]) # type: ignore[arg-type]
async def covid(self, ctx: commands.Context, country: str = "Belgium"): async def covid(self, ctx: commands.Context, country: str = "Belgium"):
"""Show Covid-19 info for a specific country. """Show Covid-19 info for a specific country.
@ -43,7 +43,7 @@ class Other(commands.Cog):
"""Autocompletion for the 'country'-parameter""" """Autocompletion for the 'country'-parameter"""
return autocomplete_country(value)[:25] return autocomplete_country(value)[:25]
@commands.hybrid_command( @commands.hybrid_command( # type: ignore[arg-type]
name="define", aliases=["ud", "urban"], description="Look up the definition of a word on the Urban Dictionary" name="define", aliases=["ud", "urban"], description="Look up the definition of a word on the Urban Dictionary"
) )
async def define(self, ctx: commands.Context, *, query: str): async def define(self, ctx: commands.Context, *, query: str):
@ -55,7 +55,7 @@ class Other(commands.Cog):
mention_author=False, mention_author=False,
) )
@commands.hybrid_command(name="google", description="Google search") @commands.hybrid_command(name="google", description="Google search") # type: ignore[arg-type]
@app_commands.describe(query="Search query") @app_commands.describe(query="Search query")
async def google(self, ctx: commands.Context, *, query: str): async def google(self, ctx: commands.Context, *, query: str):
"""Show the Google search results for `query`. """Show the Google search results for `query`.
@ -71,7 +71,7 @@ class Other(commands.Cog):
embed = GoogleSearch(results).to_embed() embed = GoogleSearch(results).to_embed()
await ctx.reply(embed=embed, mention_author=False) await ctx.reply(embed=embed, mention_author=False)
@commands.hybrid_command(name="inspire", description="Generate an InspiroBot quote.") @commands.hybrid_command(name="inspire", description="Generate an InspiroBot quote.") # type: ignore[arg-type]
async def inspire(self, ctx: commands.Context): async def inspire(self, ctx: commands.Context):
"""Generate an [InspiroBot](https://inspirobot.me/) quote.""" """Generate an [InspiroBot](https://inspirobot.me/) quote."""
async with ctx.typing(): async with ctx.typing():
@ -82,7 +82,7 @@ class Other(commands.Cog):
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
return await get_link_by_name(session, name.lower()) return await get_link_by_name(session, name.lower())
@commands.command(name="Link", aliases=["Links"]) @commands.command(name="Link", aliases=["Links"]) # type: ignore[arg-type]
async def link_msg(self, ctx: commands.Context, name: str): async def link_msg(self, ctx: commands.Context, name: str):
"""Get the link to the resource named `name`.""" """Get the link to the resource named `name`."""
link = await self._get_link(name) link = await self._get_link(name)
@ -92,7 +92,7 @@ class Other(commands.Cog):
target_message = await self.client.get_reply_target(ctx) target_message = await self.client.get_reply_target(ctx)
await target_message.reply(link.url, mention_author=False) await target_message.reply(link.url, mention_author=False)
@app_commands.command(name="link") @app_commands.command(name="link") # type: ignore[arg-type]
@app_commands.describe(name="The name of the resource") @app_commands.describe(name="The name of the resource")
async def link_slash(self, interaction: discord.Interaction, name: str): async def link_slash(self, interaction: discord.Interaction, name: str):
"""Get the link to something.""" """Get the link to something."""

View File

@ -42,7 +42,7 @@ class Owner(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client = client self.client = client
async def cog_check(self, ctx: commands.Context) -> bool: async def cog_check(self, ctx: commands.Context) -> bool: # type: ignore[override]
"""Global check for every command in this cog """Global check for every command in this cog
This means that we don't have to add is_owner() to every single command separately This means that we don't have to add is_owner() to every single command separately
@ -102,7 +102,7 @@ class Owner(commands.Cog):
async def add_msg(self, ctx: commands.Context): async def add_msg(self, ctx: commands.Context):
"""Command group for [add X] message commands""" """Command group for [add X] message commands"""
@add_msg.command(name="Alias") @add_msg.command(name="Alias") # type: ignore[arg-type]
async def add_alias_msg(self, ctx: commands.Context, command: str, alias: str): async def add_alias_msg(self, ctx: commands.Context, command: str, alias: str):
"""Add a new alias for a custom command""" """Add a new alias for a custom command"""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -116,7 +116,7 @@ class Owner(commands.Cog):
await ctx.reply("There is already a command with this name.") await ctx.reply("There is already a command with this name.")
await self.client.reject_message(ctx.message) await self.client.reject_message(ctx.message)
@add_msg.command(name="Custom") @add_msg.command(name="Custom") # type: ignore[arg-type]
async def add_custom_msg(self, ctx: commands.Context, name: str, *, response: str): async def add_custom_msg(self, ctx: commands.Context, name: str, *, response: str):
"""Add a new custom command""" """Add a new custom command"""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -127,7 +127,7 @@ class Owner(commands.Cog):
await ctx.reply("There is already a command with this name.") await ctx.reply("There is already a command with this name.")
await self.client.reject_message(ctx.message) await self.client.reject_message(ctx.message)
@add_msg.command(name="Link") @add_msg.command(name="Link") # type: ignore[arg-type]
async def add_link_msg(self, ctx: commands.Context, name: str, url: str): async def add_link_msg(self, ctx: commands.Context, name: str, url: str):
"""Add a new link""" """Add a new link"""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -136,7 +136,7 @@ class Owner(commands.Cog):
await self.client.confirm_message(ctx.message) await self.client.confirm_message(ctx.message)
@add_slash.command(name="custom", description="Add a custom command") @add_slash.command(name="custom", description="Add a custom command") # type: ignore[arg-type]
async def add_custom_slash(self, interaction: discord.Interaction): async def add_custom_slash(self, interaction: discord.Interaction):
"""Slash command to add a custom command""" """Slash command to add a custom command"""
if not await self.client.is_owner(interaction.user): if not await self.client.is_owner(interaction.user):
@ -145,7 +145,7 @@ class Owner(commands.Cog):
modal = CreateCustomCommand(self.client) modal = CreateCustomCommand(self.client)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@add_slash.command(name="dadjoke", description="Add a dad joke") @add_slash.command(name="dadjoke", description="Add a dad joke") # type: ignore[arg-type]
async def add_dad_joke_slash(self, interaction: discord.Interaction): async def add_dad_joke_slash(self, interaction: discord.Interaction):
"""Slash command to add a dad joke""" """Slash command to add a dad joke"""
if not await self.client.is_owner(interaction.user): if not await self.client.is_owner(interaction.user):
@ -154,7 +154,7 @@ class Owner(commands.Cog):
modal = AddDadJoke(self.client) modal = AddDadJoke(self.client)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@add_slash.command(name="deadline", description="Add a deadline") @add_slash.command(name="deadline", description="Add a deadline") # type: ignore[arg-type]
@app_commands.describe(course="The name of the course to add a deadline for (aliases work too)") @app_commands.describe(course="The name of the course to add a deadline for (aliases work too)")
async def add_deadline_slash(self, interaction: discord.Interaction, course: str): async def add_deadline_slash(self, interaction: discord.Interaction, course: str):
"""Slash command to add a deadline""" """Slash command to add a deadline"""
@ -174,7 +174,7 @@ class Owner(commands.Cog):
"""Autocompletion for the 'course'-parameter""" """Autocompletion for the 'course'-parameter"""
return self.client.database_caches.ufora_courses.get_autocomplete_suggestions(current) return self.client.database_caches.ufora_courses.get_autocomplete_suggestions(current)
@add_slash.command(name="event", description="Add a new event") @add_slash.command(name="event", description="Add a new event") # type: ignore[arg-type]
async def add_event_slash(self, interaction: discord.Interaction): async def add_event_slash(self, interaction: discord.Interaction):
"""Slash command to add new events""" """Slash command to add new events"""
if not await self.client.is_owner(interaction.user): if not await self.client.is_owner(interaction.user):
@ -183,7 +183,7 @@ class Owner(commands.Cog):
modal = AddEvent(self.client) modal = AddEvent(self.client)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@add_slash.command(name="link", description="Add a new link") @add_slash.command(name="link", description="Add a new link") # type: ignore[arg-type]
async def add_link_slash(self, interaction: discord.Interaction): async def add_link_slash(self, interaction: discord.Interaction):
"""Slash command to add new links""" """Slash command to add new links"""
if not await self.client.is_owner(interaction.user): if not await self.client.is_owner(interaction.user):
@ -192,7 +192,7 @@ class Owner(commands.Cog):
modal = AddLink(self.client) modal = AddLink(self.client)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@add_slash.command(name="meme", description="Add a new meme") @add_slash.command(name="meme", description="Add a new meme") # type: ignore[arg-type]
async def add_meme_slash(self, interaction: discord.Interaction, name: str, imgflip_id: int, field_count: int): async def add_meme_slash(self, interaction: discord.Interaction, name: str, imgflip_id: int, field_count: int):
"""Slash command to add new memes""" """Slash command to add new memes"""
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
@ -205,11 +205,11 @@ class Owner(commands.Cog):
await interaction.followup.send(f"Added meme `{meme.meme_id}`.") await interaction.followup.send(f"Added meme `{meme.meme_id}`.")
await self.client.database_caches.memes.invalidate(session) await self.client.database_caches.memes.invalidate(session)
@commands.group(name="Edit", case_insensitive=True, invoke_without_command=False) @commands.group(name="Edit", case_insensitive=True, invoke_without_command=False) # type: ignore[arg-type]
async def edit_msg(self, ctx: commands.Context): async def edit_msg(self, ctx: commands.Context):
"""Command group for [edit X] commands""" """Command group for [edit X] commands"""
@edit_msg.command(name="Custom") @edit_msg.command(name="Custom") # type: ignore[arg-type]
async def edit_custom_msg(self, ctx: commands.Context, command: str, *, flags: EditCustomFlags): async def edit_custom_msg(self, ctx: commands.Context, command: str, *, flags: EditCustomFlags):
"""Edit an existing custom command""" """Edit an existing custom command"""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
@ -220,7 +220,7 @@ class Owner(commands.Cog):
await ctx.reply(f"No command found matching `{command}`.") await ctx.reply(f"No command found matching `{command}`.")
return await self.client.reject_message(ctx.message) return await self.client.reject_message(ctx.message)
@edit_slash.command(name="custom", description="Edit a custom command") @edit_slash.command(name="custom", description="Edit a custom command") # type: ignore[arg-type]
@app_commands.describe(command="The name of the command to edit") @app_commands.describe(command="The name of the command to edit")
async def edit_custom_slash(self, interaction: discord.Interaction, command: str): async def edit_custom_slash(self, interaction: discord.Interaction, command: str):
"""Slash command to edit a custom command""" """Slash command to edit a custom command"""

View File

@ -27,7 +27,7 @@ class School(commands.Cog):
def __init__(self, client: Didier): def __init__(self, client: Didier):
self.client = client self.client = client
@commands.hybrid_command(name="deadlines") @commands.hybrid_command(name="deadlines") # type: ignore[arg-type]
async def deadlines(self, ctx: commands.Context): async def deadlines(self, ctx: commands.Context):
"""Show upcoming deadlines.""" """Show upcoming deadlines."""
async with ctx.typing(): async with ctx.typing():
@ -40,7 +40,7 @@ class School(commands.Cog):
embed = Deadlines(deadlines).to_embed() embed = Deadlines(deadlines).to_embed()
await ctx.reply(embed=embed, mention_author=False, ephemeral=False) await ctx.reply(embed=embed, mention_author=False, ephemeral=False)
@commands.hybrid_command(name="les", aliases=["sched", "schedule"]) @commands.hybrid_command(name="les", aliases=["sched", "schedule"]) # type: ignore[arg-type]
@app_commands.rename(day_dt="date") @app_commands.rename(day_dt="date")
async def les( async def les(
self, ctx: commands.Context, *, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None self, ctx: commands.Context, *, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None
@ -72,10 +72,7 @@ class School(commands.Cog):
except NotInMainGuildException: except NotInMainGuildException:
return await ctx.reply(f"You are not a member of {self.client.main_guild.name}.", mention_author=False) return await ctx.reply(f"You are not a member of {self.client.main_guild.name}.", mention_author=False)
@commands.hybrid_command( @commands.hybrid_command(name="menu", aliases=["eten", "food"]) # type: ignore[arg-type]
name="menu",
aliases=["eten", "food"],
)
@app_commands.rename(day_dt="date") @app_commands.rename(day_dt="date")
async def menu( async def menu(
self, ctx: commands.Context, *, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None self, ctx: commands.Context, *, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None
@ -96,7 +93,7 @@ class School(commands.Cog):
embed = no_menu_found(day_dt) embed = no_menu_found(day_dt)
await ctx.reply(embed=embed, mention_author=False) await ctx.reply(embed=embed, mention_author=False)
@commands.hybrid_command( @commands.hybrid_command( # type: ignore[arg-type]
name="fiche", description="Sends the link to study guides", aliases=["guide", "studiefiche"] name="fiche", description="Sends the link to study guides", aliases=["guide", "studiefiche"]
) )
@app_commands.describe(course="The name of the course to fetch the study guide for (aliases work too)") @app_commands.describe(course="The name of the course to fetch the study guide for (aliases work too)")
@ -124,7 +121,7 @@ class School(commands.Cog):
mention_author=False, mention_author=False,
) )
@commands.hybrid_command(name="ufora") @commands.hybrid_command(name="ufora") # type: ignore[arg-type]
async def ufora(self, ctx: commands.Context, course: str): async def ufora(self, ctx: commands.Context, course: str):
"""Link the Ufora page for a course.""" """Link the Ufora page for a course."""
async with self.client.postgres_session as session: async with self.client.postgres_session as session:

View File

@ -1,4 +1,6 @@
import asyncio
import datetime import datetime
import logging
import random import random
import discord import discord
@ -20,9 +22,12 @@ from didier.data.embeds.schedules import (
from didier.data.rss_feeds.free_games import fetch_free_games from didier.data.rss_feeds.free_games import fetch_free_games
from didier.data.rss_feeds.ufora import fetch_ufora_announcements from didier.data.rss_feeds.ufora import fetch_ufora_announcements
from didier.decorators.tasks import timed_task from didier.decorators.tasks import timed_task
from didier.utils.discord.channels import NON_MESSAGEABLE_CHANNEL_TYPES
from didier.utils.discord.checks import is_owner from didier.utils.discord.checks import is_owner
from didier.utils.types.datetime import LOCAL_TIMEZONE, tz_aware_now from didier.utils.types.datetime import LOCAL_TIMEZONE, tz_aware_now
logger = logging.getLogger(__name__)
# datetime.time()-instances for when every task should run # datetime.time()-instances for when every task should run
DAILY_RESET_TIME = datetime.time(hour=0, minute=0, tzinfo=LOCAL_TIMEZONE) DAILY_RESET_TIME = datetime.time(hour=0, minute=0, tzinfo=LOCAL_TIMEZONE)
SOCIALLY_ACCEPTABLE_TIME = datetime.time(hour=7, minute=0, tzinfo=LOCAL_TIMEZONE) SOCIALLY_ACCEPTABLE_TIME = datetime.time(hour=7, minute=0, tzinfo=LOCAL_TIMEZONE)
@ -56,7 +61,7 @@ class Tasks(commands.Cog):
} }
@overrides @overrides
def cog_load(self) -> None: async def cog_load(self) -> None:
# Only check birthdays if there's a channel to send it to # Only check birthdays if there's a channel to send it to
if settings.BIRTHDAY_ANNOUNCEMENT_CHANNEL is not None: if settings.BIRTHDAY_ANNOUNCEMENT_CHANNEL is not None:
self.check_birthdays.start() self.check_birthdays.start()
@ -72,9 +77,10 @@ class Tasks(commands.Cog):
# Start other tasks # Start other tasks
self.reminders.start() self.reminders.start()
asyncio.create_task(self.get_error_channel())
@overrides @overrides
def cog_unload(self) -> None: async def cog_unload(self) -> None:
# Cancel all pending tasks # Cancel all pending tasks
for task in self._tasks.values(): for task in self._tasks.values():
if task.is_running(): if task.is_running():
@ -96,7 +102,7 @@ class Tasks(commands.Cog):
await ctx.reply(embed=embed, mention_author=False) await ctx.reply(embed=embed, mention_author=False)
@tasks_group.command(name="Force", case_insensitive=True, usage="[Task]") @tasks_group.command(name="Force", case_insensitive=True, usage="[Task]") # type: ignore[arg-type]
async def force_task(self, ctx: commands.Context, name: str): async def force_task(self, ctx: commands.Context, name: str):
"""Command to force-run a task without waiting for the specified run time""" """Command to force-run a task without waiting for the specified run time"""
name = name.lower() name = name.lower()
@ -107,23 +113,53 @@ class Tasks(commands.Cog):
await task(forced=True) await task(forced=True)
await self.client.confirm_message(ctx.message) await self.client.confirm_message(ctx.message)
async def get_error_channel(self):
"""Get the configured channel from the cache"""
await self.client.wait_until_ready()
# Configure channel to send errors to
if settings.ERRORS_CHANNEL is not None:
channel = self.client.get_channel(settings.ERRORS_CHANNEL)
if isinstance(channel, NON_MESSAGEABLE_CHANNEL_TYPES):
logger.error(f"Configured error channel (id `{settings.ERRORS_CHANNEL}`) is not messageable.")
else:
self.client.error_channel = channel
elif self.client.owner_id is not None:
self.client.error_channel = self.client.get_user(self.client.owner_id)
@tasks.loop(time=SOCIALLY_ACCEPTABLE_TIME) @tasks.loop(time=SOCIALLY_ACCEPTABLE_TIME)
@timed_task(enums.TaskType.BIRTHDAYS) @timed_task(enums.TaskType.BIRTHDAYS)
async def check_birthdays(self, **kwargs): async def check_birthdays(self, **kwargs):
"""Check if it's currently anyone's birthday""" """Check if it's currently anyone's birthday"""
_ = kwargs _ = kwargs
# Can't happen (task isn't started if this is None), but Mypy doesn't know
if settings.BIRTHDAY_ANNOUNCEMENT_CHANNEL is None:
return
now = tz_aware_now().date() now = tz_aware_now().date()
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
birthdays = await get_birthdays_on_day(session, now) birthdays = await get_birthdays_on_day(session, now)
channel = self.client.get_channel(settings.BIRTHDAY_ANNOUNCEMENT_CHANNEL) channel = self.client.get_channel(settings.BIRTHDAY_ANNOUNCEMENT_CHANNEL)
if channel is None: if channel is None:
return await self.client.log_error("Unable to find channel for birthday announcements") return await self.client.log_error("Unable to fetch channel for birthday announcements.")
if isinstance(channel, NON_MESSAGEABLE_CHANNEL_TYPES):
return await self.client.log_error(
f"Birthday announcement channel (id `{settings.BIRTHDAY_ANNOUNCEMENT_CHANNEL}`) is not messageable."
)
for birthday in birthdays: for birthday in birthdays:
user = self.client.get_user(birthday.user_id) user = self.client.get_user(birthday.user_id)
if user is None:
await self.client.log_error(
f"Unable to fetch user with id `{birthday.user_id}` for birthday announcement"
)
continue
await channel.send(random.choice(BIRTHDAY_MESSAGES).format(mention=user.mention)) await channel.send(random.choice(BIRTHDAY_MESSAGES).format(mention=user.mention))
@check_birthdays.before_loop @check_birthdays.before_loop
@ -143,6 +179,14 @@ class Tasks(commands.Cog):
games = await fetch_free_games(self.client.http_session, session) games = await fetch_free_games(self.client.http_session, session)
channel = self.client.get_channel(settings.FREE_GAMES_CHANNEL) channel = self.client.get_channel(settings.FREE_GAMES_CHANNEL)
if channel is None:
return await self.client.log_error("Unable to fetch channel for free games announcements.")
if isinstance(channel, NON_MESSAGEABLE_CHANNEL_TYPES):
return await self.client.log_error(
f"Free games channel (id `{settings.FREE_GAMES_CHANNEL}`) is not messageable."
)
for game in games: for game in games:
await channel.send(embed=game.to_embed()) await channel.send(embed=game.to_embed())
@ -204,6 +248,17 @@ class Tasks(commands.Cog):
async with self.client.postgres_session as db_session: async with self.client.postgres_session as db_session:
announcements_channel = self.client.get_channel(settings.UFORA_ANNOUNCEMENTS_CHANNEL) announcements_channel = self.client.get_channel(settings.UFORA_ANNOUNCEMENTS_CHANNEL)
if announcements_channel is None:
return await self.client.log_error(
f"Unable to fetch channel for ufora announcements (id `{settings.UFORA_ANNOUNCEMENTS_CHANNEL}`)."
)
if isinstance(announcements_channel, NON_MESSAGEABLE_CHANNEL_TYPES):
return await self.client.log_error(
f"Ufora announcements channel (id `{settings.UFORA_ANNOUNCEMENTS_CHANNEL}`) is not messageable."
)
announcements = await fetch_ufora_announcements(self.client.http_session, db_session) announcements = await fetch_ufora_announcements(self.client.http_session, db_session)
for announcement in announcements: for announcement in announcements:

View File

@ -38,10 +38,10 @@ def create_error_embed(ctx: Optional[commands.Context], exception: Exception) ->
embed = discord.Embed(title="Error", colour=discord.Colour.red()) embed = discord.Embed(title="Error", colour=discord.Colour.red())
if ctx is not None: if ctx is not None:
if ctx.guild is None: if ctx.guild is None or isinstance(ctx.channel, discord.DMChannel):
origin = "DM" origin = "DM"
else: else:
origin = f"{ctx.channel.mention} ({ctx.guild.name})" origin = f"<#{ctx.channel.id}> ({ctx.guild.name})"
invocation = f"{ctx.author.display_name} in {origin}" invocation = f"{ctx.author.display_name} in {origin}"

View File

@ -11,7 +11,7 @@ __all__ = ["create_logging_embed"]
def create_logging_embed(level: int, message: str) -> discord.Embed: def create_logging_embed(level: int, message: str) -> discord.Embed:
"""Create an embed to send to the logging channel""" """Create an embed to send to the logging channel"""
colours = { colours = {
logging.DEBUG: discord.Colour.light_gray, logging.DEBUG: discord.Colour.light_grey(),
logging.ERROR: discord.Colour.red(), logging.ERROR: discord.Colour.red(),
logging.INFO: discord.Colour.blue(), logging.INFO: discord.Colour.blue(),
logging.WARNING: discord.Colour.yellow(), logging.WARNING: discord.Colour.yellow(),

View File

@ -72,12 +72,12 @@ def get_search_results(bs: BeautifulSoup) -> list[str]:
return list(dict.fromkeys(results)) return list(dict.fromkeys(results))
async def google_search(http_client: ClientSession, query: str): async def google_search(http_session: ClientSession, query: str):
"""Get the first 10 Google search results""" """Get the first 10 Google search results"""
query = urlencode({"q": query}) query = urlencode({"q": query})
# Request 20 results in case of duplicates, bad matches, ... # Request 20 results in case of duplicates, bad matches, ...
async with http_client.get(f"https://www.google.com/search?{query}&num=20&hl=en") as response: async with http_session.get(f"https://www.google.com/search?{query}&num=20&hl=en") as response:
# Something went wrong # Something went wrong
if response.status != http.HTTPStatus.OK: if response.status != http.HTTPStatus.OK:
return SearchData(query, response.status) return SearchData(query, response.status)

View File

@ -17,7 +17,7 @@ from database.utils.caches import CacheManager
from didier.data.embeds.error_embed import create_error_embed from didier.data.embeds.error_embed import create_error_embed
from didier.data.embeds.logging_embed import create_logging_embed from didier.data.embeds.logging_embed import create_logging_embed
from didier.data.embeds.schedules import Schedule, parse_schedule from didier.data.embeds.schedules import Schedule, parse_schedule
from didier.exceptions import HTTPException, NoMatch from didier.exceptions import GetNoneException, HTTPException, NoMatch
from didier.utils.discord.prefix import get_prefix from didier.utils.discord.prefix import get_prefix
from didier.utils.discord.snipe import should_snipe from didier.utils.discord.snipe import should_snipe
from didier.utils.easter_eggs import detect_easter_egg from didier.utils.easter_eggs import detect_easter_egg
@ -33,7 +33,7 @@ class Didier(commands.Bot):
"""DIDIER <3""" """DIDIER <3"""
database_caches: CacheManager database_caches: CacheManager
error_channel: discord.abc.Messageable error_channel: Optional[discord.abc.Messageable] = None
initial_extensions: tuple[str, ...] = () initial_extensions: tuple[str, ...] = ()
http_session: ClientSession http_session: ClientSession
schedules: dict[settings.ScheduleType, Schedule] = {} schedules: dict[settings.ScheduleType, Schedule] = {}
@ -56,12 +56,17 @@ class Didier(commands.Bot):
command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status command_prefix=get_prefix, case_insensitive=True, intents=intents, activity=activity, status=status
) )
self.tree.on_error = self.on_app_command_error # I'm not creating a custom tree, this is the way to do it
self.tree.on_error = self.on_app_command_error # type: ignore[method-assign]
@cached_property @cached_property
def main_guild(self) -> discord.Guild: def main_guild(self) -> discord.Guild:
"""Obtain a reference to the main guild""" """Obtain a reference to the main guild"""
return self.get_guild(settings.DISCORD_MAIN_GUILD) guild = self.get_guild(settings.DISCORD_MAIN_GUILD)
if guild is None:
raise GetNoneException("Main guild could not be found in the bot's cache")
return guild
@property @property
def postgres_session(self) -> AsyncSession: def postgres_session(self) -> AsyncSession:
@ -93,12 +98,6 @@ class Didier(commands.Bot):
await self._load_initial_extensions() await self._load_initial_extensions()
await self._load_directory_extensions("didier/cogs") await self._load_directory_extensions("didier/cogs")
# Configure channel to send errors to
if settings.ERRORS_CHANNEL is not None:
self.error_channel = self.get_channel(settings.ERRORS_CHANNEL)
else:
self.error_channel = self.get_user(self.owner_id)
def _create_ignored_directories(self): def _create_ignored_directories(self):
"""Create directories that store ignored data""" """Create directories that store ignored data"""
ignored = ["files/schedules"] ignored = ["files/schedules"]
@ -152,18 +151,27 @@ class Didier(commands.Bot):
original message instead original message instead
""" """
if ctx.message.reference is not None: if ctx.message.reference is not None:
return await self.resolve_message(ctx.message.reference) return await self.resolve_message(ctx.message.reference) or ctx.message
return ctx.message return ctx.message
async def resolve_message(self, reference: discord.MessageReference) -> discord.Message: async def resolve_message(self, reference: discord.MessageReference) -> Optional[discord.Message]:
"""Fetch a message from a reference""" """Fetch a message from a reference"""
# Message is in the cache, return it # Message is in the cache, return it
if reference.cached_message is not None: if reference.cached_message is not None:
return reference.cached_message return reference.cached_message
if reference.message_id is None:
return None
# For older messages: fetch them from the API # For older messages: fetch them from the API
channel = self.get_channel(reference.channel_id) channel = self.get_channel(reference.channel_id)
if channel is None or isinstance(
channel,
(discord.CategoryChannel, discord.ForumChannel, discord.abc.PrivateChannel),
): # Logically this can't happen, but we have to please Mypy
return None
return await channel.fetch_message(reference.message_id) return await channel.fetch_message(reference.message_id)
async def confirm_message(self, message: discord.Message): async def confirm_message(self, message: discord.Message):
@ -184,7 +192,7 @@ class Didier(commands.Bot):
} }
methods.get(level, logger.error)(message) methods.get(level, logger.error)(message)
if log_to_discord: if log_to_discord and self.error_channel is not None:
embed = create_logging_embed(level, message) embed = create_logging_embed(level, message)
await self.error_channel.send(embed=embed) await self.error_channel.send(embed=embed)
@ -253,10 +261,9 @@ class Didier(commands.Bot):
await interaction.response.send_message("Something went wrong processing this command.", ephemeral=True) await interaction.response.send_message("Something went wrong processing this command.", ephemeral=True)
if settings.ERRORS_CHANNEL is not None: if self.error_channel is not None:
embed = create_error_embed(await commands.Context.from_interaction(interaction), exception) embed = create_error_embed(await commands.Context.from_interaction(interaction), exception)
channel = self.get_channel(settings.ERRORS_CHANNEL) await self.error_channel.send(embed=embed)
await channel.send(embed=embed)
async def on_command_completion(self, ctx: commands.Context): async def on_command_completion(self, ctx: commands.Context):
"""Event triggered when a message command completes successfully""" """Event triggered when a message command completes successfully"""
@ -281,7 +288,7 @@ class Didier(commands.Bot):
# Hybrid command errors are wrapped in an additional error, so wrap it back out # Hybrid command errors are wrapped in an additional error, so wrap it back out
if isinstance(exception, commands.HybridCommandError): if isinstance(exception, commands.HybridCommandError):
exception = exception.original exception = exception.original # type: ignore[assignment]
# Ignore exceptions that aren't important # Ignore exceptions that aren't important
if isinstance( if isinstance(
@ -332,10 +339,9 @@ class Didier(commands.Bot):
# Print everything that we care about to the logs/stderr # Print everything that we care about to the logs/stderr
await super().on_command_error(ctx, exception) await super().on_command_error(ctx, exception)
if settings.ERRORS_CHANNEL is not None: if self.error_channel is not None:
embed = create_error_embed(ctx, exception) embed = create_error_embed(ctx, exception)
channel = self.get_channel(settings.ERRORS_CHANNEL) await self.error_channel.send(embed=embed)
await channel.send(embed=embed)
async def on_message(self, message: discord.Message, /) -> None: async def on_message(self, message: discord.Message, /) -> None:
"""Event triggered when a message is sent""" """Event triggered when a message is sent"""
@ -344,7 +350,7 @@ class Didier(commands.Bot):
return return
# Boos react to people that say Dider # Boos react to people that say Dider
if "dider" in message.content.lower() and message.author.id != self.user.id: if "dider" in message.content.lower() and self.user is not None and message.author.id != self.user.id:
await message.add_reaction(settings.DISCORD_BOOS_REACT) await message.add_reaction(settings.DISCORD_BOOS_REACT)
# Potential custom command # Potential custom command
@ -389,10 +395,9 @@ class Didier(commands.Bot):
async def on_task_error(self, exception: Exception): async def on_task_error(self, exception: Exception):
"""Event triggered when a task raises an exception""" """Event triggered when a task raises an exception"""
if settings.ERRORS_CHANNEL is not None: if self.error_channel:
embed = create_error_embed(None, exception) embed = create_error_embed(None, exception)
channel = self.get_channel(settings.ERRORS_CHANNEL) await self.error_channel.send(embed=embed)
await channel.send(embed=embed)
async def on_thread_create(self, thread: discord.Thread): async def on_thread_create(self, thread: discord.Thread):
"""Event triggered when a new thread is created""" """Event triggered when a new thread is created"""

View File

@ -1,6 +1,14 @@
from .get_none_exception import GetNoneException
from .http_exception import HTTPException from .http_exception import HTTPException
from .missing_env import MissingEnvironmentVariable from .missing_env import MissingEnvironmentVariable
from .no_match import NoMatch, expect from .no_match import NoMatch, expect
from .not_in_main_guild_exception import NotInMainGuildException from .not_in_main_guild_exception import NotInMainGuildException
__all__ = ["HTTPException", "MissingEnvironmentVariable", "NoMatch", "expect", "NotInMainGuildException"] __all__ = [
"GetNoneException",
"HTTPException",
"MissingEnvironmentVariable",
"NoMatch",
"expect",
"NotInMainGuildException",
]

View File

@ -0,0 +1,5 @@
__all__ = ["GetNoneException"]
class GetNoneException(RuntimeError):
"""Exception raised when a Bot.get()-method returned None"""

View File

@ -12,6 +12,6 @@ class NotInMainGuildException(ValueError):
def __init__(self, user: Union[discord.User, discord.Member]): def __init__(self, user: Union[discord.User, discord.Member]):
super().__init__( super().__init__(
f"User {user.display_name} (id {user.id}) " f"User {user.display_name} (id `{user.id}`) "
f"is not a member of the configured main guild (id {settings.DISCORD_MAIN_GUILD})." f"is not a member of the configured main guild (id `{settings.DISCORD_MAIN_GUILD}`)."
) )

View File

@ -0,0 +1,5 @@
import discord
__all__ = ["NON_MESSAGEABLE_CHANNEL_TYPES"]
NON_MESSAGEABLE_CHANNEL_TYPES = (discord.ForumChannel, discord.CategoryChannel, discord.abc.PrivateChannel)

View File

@ -15,11 +15,14 @@ def match_prefix(client: commands.Bot, message: Message) -> Optional[str]:
This is done dynamically through regexes to allow case-insensitivity This is done dynamically through regexes to allow case-insensitivity
and variable amounts of whitespace among other things. and variable amounts of whitespace among other things.
""" """
mention = f"<@!?{client.user.id}>" mention = f"<@!?{client.user.id}>" if client.user else None
regex = r"^({})\s*" regex = r"^({})\s*"
# Check which prefix was used # Check which prefix was used
for prefix in [*constants.PREFIXES, mention]: for prefix in [*constants.PREFIXES, mention]:
if prefix is None:
continue
match = re.match(regex.format(prefix), message.content, flags=re.I) match = re.match(regex.format(prefix), message.content, flags=re.I)
if match is not None: if match is not None:

View File

@ -25,24 +25,24 @@ class CreateBookmark(discord.ui.Modal, title="Create Bookmark"):
@overrides @overrides
async def on_submit(self, interaction: discord.Interaction): async def on_submit(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
label = self.name.value.strip() label = self.name.value.strip()
try: try:
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
bm = await create_bookmark(session, interaction.user.id, label, self.jump_url) bm = await create_bookmark(session, interaction.user.id, label, self.jump_url)
return await interaction.response.send_message( return await interaction.followup.send(
f"Bookmark `{label}` successfully created (`#{bm.bookmark_id}`).", ephemeral=True f"Bookmark `{label}` successfully created (`#{bm.bookmark_id}`)."
) )
except DuplicateInsertException: except DuplicateInsertException:
# Label is already in use # Label is already in use
return await interaction.response.send_message( return await interaction.followup.send(f"You already have a bookmark named `{label}`.")
f"You already have a bookmark named `{label}`.", ephemeral=True
)
except ForbiddenNameException: except ForbiddenNameException:
# Label isn't allowed # Label isn't allowed
return await interaction.response.send_message(f"Bookmarks cannot be named `{label}`.", ephemeral=True) return await interaction.followup.send(f"Bookmarks cannot be named `{label}`.")
@overrides @overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True) await interaction.followup.send("Something went wrong.", ephemeral=True)
traceback.print_tb(error.__traceback__) traceback.print_tb(error.__traceback__)

View File

@ -26,12 +26,14 @@ class AddDadJoke(discord.ui.Modal, title="Add Dad Joke"):
@overrides @overrides
async def on_submit(self, interaction: discord.Interaction): async def on_submit(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
joke = await add_dad_joke(session, str(self.joke.value)) joke = await add_dad_joke(session, str(self.joke.value))
await interaction.response.send_message(f"Successfully added joke #{joke.dad_joke_id}", ephemeral=True) await interaction.followup.send(f"Successfully added joke #{joke.dad_joke_id}")
@overrides @overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True) await interaction.followup.send("Something went wrong.", ephemeral=True)
traceback.print_tb(error.__traceback__) traceback.print_tb(error.__traceback__)

View File

@ -10,6 +10,8 @@ from didier import Didier
__all__ = ["AddEvent"] __all__ = ["AddEvent"]
from didier.utils.discord.channels import NON_MESSAGEABLE_CHANNEL_TYPES
class AddEvent(discord.ui.Modal, title="Add Event"): class AddEvent(discord.ui.Modal, title="Add Event"):
"""Modal to add a new event""" """Modal to add a new event"""
@ -33,15 +35,20 @@ class AddEvent(discord.ui.Modal, title="Add Event"):
@overrides @overrides
async def on_submit(self, interaction: discord.Interaction) -> None: async def on_submit(self, interaction: discord.Interaction) -> None:
await interaction.response.defer(ephemeral=True)
try: try:
parse(self.timestamp.value, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels")) parse(self.timestamp.value, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels"))
except ParserError: except ParserError:
return await interaction.response.send_message("Unable to parse date argument.", ephemeral=True) return await interaction.followup.send("Unable to parse date argument.")
if self.client.get_channel(int(self.channel.value)) is None: channel = self.client.get_channel(int(self.channel.value))
return await interaction.response.send_message(
f"Unable to find channel `{self.channel.value}`", ephemeral=True if channel is None:
) return await interaction.followup.send(f"Unable to find channel with id `{self.channel.value}`")
if isinstance(channel, NON_MESSAGEABLE_CHANNEL_TYPES):
return await interaction.followup.send(f"Channel with id `{self.channel.value}` is not messageable.")
async with self.client.postgres_session as session: async with self.client.postgres_session as session:
event = await add_event( event = await add_event(
@ -52,10 +59,10 @@ class AddEvent(discord.ui.Modal, title="Add Event"):
channel_id=int(self.channel.value), channel_id=int(self.channel.value),
) )
await interaction.response.send_message(f"Successfully added event `{event.event_id}`.", ephemeral=True) await interaction.followup.send(f"Successfully added event `{event.event_id}`.")
self.client.dispatch("event_create", event) self.client.dispatch("event_create", event)
@overrides @overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True) await interaction.followup.send("Something went wrong.", ephemeral=True)
traceback.print_tb(error.__traceback__) traceback.print_tb(error.__traceback__)

View File

@ -111,7 +111,7 @@ class ScheduleInfo:
role_id: Optional[int] role_id: Optional[int]
schedule_url: Optional[str] schedule_url: Optional[str]
name: Optional[str] = None name: ScheduleType
SCHEDULE_DATA = [ SCHEDULE_DATA = [