mirror of https://github.com/stijndcl/didier
commit
a23ee3671a
|
@ -17,7 +17,7 @@ from didier.utils.types.string import pluralize
|
|||
|
||||
|
||||
class Currency(commands.Cog):
|
||||
"""Everything Dinks-related"""
|
||||
"""Everything Dinks-related."""
|
||||
|
||||
client: Didier
|
||||
|
||||
|
@ -25,7 +25,7 @@ class Currency(commands.Cog):
|
|||
super().__init__()
|
||||
self.client = client
|
||||
|
||||
@commands.command(name="Award")
|
||||
@commands.command(name="award")
|
||||
@commands.check(is_owner)
|
||||
async def award(
|
||||
self,
|
||||
|
@ -33,18 +33,18 @@ class Currency(commands.Cog):
|
|||
user: discord.User,
|
||||
amount: typing.Annotated[int, abbreviated_number],
|
||||
):
|
||||
"""Award a user a given amount of Didier Dinks"""
|
||||
"""Award a user `amount` Didier Dinks."""
|
||||
async with self.client.postgres_session as session:
|
||||
await crud.add_dinks(session, user.id, amount)
|
||||
plural = pluralize("Didier Dink", amount)
|
||||
await ctx.reply(
|
||||
f"**{ctx.author.display_name}** heeft **{user.display_name}** **{amount}** {plural} geschonken.",
|
||||
f"**{ctx.author.display_name}** has awarded **{user.display_name}** **{amount}** {plural}.",
|
||||
mention_author=False,
|
||||
)
|
||||
|
||||
@commands.group(name="bank", aliases=["B"], case_insensitive=True, invoke_without_command=True)
|
||||
@commands.group(name="bank", aliases=["b"], case_insensitive=True, invoke_without_command=True)
|
||||
async def bank(self, ctx: commands.Context):
|
||||
"""Show your Didier Bank information"""
|
||||
"""Show your Didier Bank information."""
|
||||
async with self.client.postgres_session as session:
|
||||
bank = await crud.get_bank(session, ctx.author.id)
|
||||
|
||||
|
@ -57,9 +57,9 @@ class Currency(commands.Cog):
|
|||
|
||||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
@bank.group(name="Upgrade", aliases=["U", "Upgrades"], case_insensitive=True, invoke_without_command=True)
|
||||
@bank.group(name="upgrade", aliases=["u", "upgrades"], case_insensitive=True, invoke_without_command=True)
|
||||
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:
|
||||
bank = await crud.get_bank(session, ctx.author.id)
|
||||
|
||||
|
@ -77,9 +77,9 @@ class Currency(commands.Cog):
|
|||
|
||||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
@bank_upgrades.command(name="Capacity", aliases=["C"])
|
||||
@bank_upgrades.command(name="capacity", aliases=["c"])
|
||||
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:
|
||||
try:
|
||||
await crud.upgrade_capacity(session, ctx.author.id)
|
||||
|
@ -88,9 +88,9 @@ class Currency(commands.Cog):
|
|||
await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False)
|
||||
await self.client.reject_message(ctx.message)
|
||||
|
||||
@bank_upgrades.command(name="Interest", aliases=["I"])
|
||||
@bank_upgrades.command(name="interest", aliases=["i"])
|
||||
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:
|
||||
try:
|
||||
await crud.upgrade_interest(session, ctx.author.id)
|
||||
|
@ -99,9 +99,9 @@ class Currency(commands.Cog):
|
|||
await ctx.reply("You don't have enough Didier Dinks to do this.", mention_author=False)
|
||||
await self.client.reject_message(ctx.message)
|
||||
|
||||
@bank_upgrades.command(name="Rob", aliases=["R"])
|
||||
@bank_upgrades.command(name="rob", aliases=["r"])
|
||||
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:
|
||||
try:
|
||||
await crud.upgrade_rob(session, ctx.author.id)
|
||||
|
@ -112,15 +112,27 @@ class Currency(commands.Cog):
|
|||
|
||||
@commands.hybrid_command(name="dinks")
|
||||
async def dinks(self, ctx: commands.Context):
|
||||
"""Check your Didier Dinks"""
|
||||
"""Check your Didier Dinks."""
|
||||
async with self.client.postgres_session as session:
|
||||
bank = await crud.get_bank(session, ctx.author.id)
|
||||
plural = pluralize("Didier Dink", bank.dinks)
|
||||
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"])
|
||||
async def invest(self, ctx: commands.Context, amount: typing.Annotated[typing.Union[str, int], abbreviated_number]):
|
||||
"""Invest a given amount of Didier Dinks"""
|
||||
"""Invest `amount` Didier Dinks into your bank.
|
||||
|
||||
The `amount`-argument can take both raw numbers, and abbreviations of big numbers. Additionally, passing
|
||||
`all` as the value will invest all of your Didier Dinks.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
didier invest all
|
||||
didier invest 500
|
||||
didier invest 25k
|
||||
didier invest 5.3b
|
||||
```
|
||||
"""
|
||||
async with self.client.postgres_session as session:
|
||||
invested = await crud.invest(session, ctx.author.id, amount)
|
||||
plural = pluralize("Didier Dink", invested)
|
||||
|
@ -134,7 +146,7 @@ class Currency(commands.Cog):
|
|||
|
||||
@commands.hybrid_command(name="nightly")
|
||||
async def nightly(self, ctx: commands.Context):
|
||||
"""Claim nightly Didier Dinks"""
|
||||
"""Claim nightly Didier Dinks."""
|
||||
async with self.client.postgres_session as session:
|
||||
try:
|
||||
await crud.claim_nightly(session, ctx.author.id)
|
||||
|
|
|
@ -22,7 +22,7 @@ from didier.views.modals import CreateBookmark
|
|||
|
||||
|
||||
class Discord(commands.Cog):
|
||||
"""Cog for commands related to Discord, servers, and members"""
|
||||
"""Commands related to Discord itself, which work with resources like servers and members."""
|
||||
|
||||
client: Didier
|
||||
|
||||
|
@ -43,43 +43,57 @@ class Discord(commands.Cog):
|
|||
self.client.tree.remove_command(self._bookmark_ctx_menu.name, type=self._bookmark_ctx_menu.type)
|
||||
self.client.tree.remove_command(self._pin_ctx_menu.name, type=self._pin_ctx_menu.type)
|
||||
|
||||
@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):
|
||||
"""Command to check the birthday of a user"""
|
||||
"""Command to check the birthday of `user`.
|
||||
|
||||
Not passing an argument for `user` will show yours instead.
|
||||
"""
|
||||
user_id = (user and user.id) or ctx.author.id
|
||||
async with self.client.postgres_session as session:
|
||||
birthday = await birthdays.get_birthday_for_user(session, user_id)
|
||||
|
||||
name = "Your" if user is None else f"{user.display_name}'s"
|
||||
name: Optional[str] = f"{user.display_name}'s" if user is not None else None
|
||||
|
||||
if birthday is None:
|
||||
return await ctx.reply(f"I don't know {name} birthday.", mention_author=False)
|
||||
return await ctx.reply(f"I don't know {name or 'your'} birthday.", mention_author=False)
|
||||
|
||||
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} birthday is set to **{day}/{month}**.", mention_author=False)
|
||||
@birthday.command(name="set", aliases=["config"])
|
||||
async def birthday_set(self, ctx: commands.Context, day: str):
|
||||
"""Set your birthday to `day`.
|
||||
|
||||
@birthday.command(name="Set", aliases=["Config"])
|
||||
async def birthday_set(self, ctx: commands.Context, date_str: str):
|
||||
"""Command to set your birthday"""
|
||||
Parsing of the `day`-argument happens in the following order: `DD/MM/YYYY`, `DD/MM/YY`, `DD/MM`.
|
||||
Other formats will not be accepted.
|
||||
"""
|
||||
try:
|
||||
default_year = 2001
|
||||
date = str_to_date(date_str, formats=["%d/%m/%Y", "%d/%m/%y", "%d/%m"])
|
||||
date = str_to_date(day, formats=["%d/%m/%Y", "%d/%m/%y", "%d/%m"])
|
||||
|
||||
# If no year was passed, make it 2001 by default
|
||||
if date_str.count("/") == 1:
|
||||
if day.count("/") == 1:
|
||||
date.replace(year=default_year)
|
||||
|
||||
except ValueError:
|
||||
return await ctx.reply(f"`{date_str}` is not a valid date.", mention_author=False)
|
||||
return await ctx.reply(f"`{day}` is not a valid date.", mention_author=False)
|
||||
|
||||
async with self.client.postgres_session as session:
|
||||
await birthdays.add_birthday(session, ctx.author.id, date)
|
||||
await self.client.confirm_message(ctx.message)
|
||||
|
||||
@commands.group(name="Bookmark", aliases=["Bm", "Bookmarks"], case_insensitive=True, invoke_without_command=True)
|
||||
@commands.group(name="bookmark", aliases=["bm", "bookmarks"], case_insensitive=True, invoke_without_command=True)
|
||||
async def bookmark(self, ctx: commands.Context, *, label: Optional[str] = None):
|
||||
"""Post a bookmarked message"""
|
||||
"""Post the message bookmarked with `label`.
|
||||
|
||||
The `label`-argument can contain spaces and does not require quotes around it. For example:
|
||||
```
|
||||
didier bookmark some label with multiple words
|
||||
```
|
||||
|
||||
When no value for `label` is provided, this is a shortcut to `bookmark search`.
|
||||
"""
|
||||
# No label: shortcut to display bookmarks
|
||||
if label is None:
|
||||
return await self.bookmark_search(ctx, query=None)
|
||||
|
@ -92,9 +106,16 @@ class Discord(commands.Cog):
|
|||
)
|
||||
await ctx.reply(result.jump_url, mention_author=False)
|
||||
|
||||
@bookmark.command(name="Create", aliases=["New"])
|
||||
@bookmark.command(name="create", aliases=["new"])
|
||||
async def bookmark_create(self, ctx: commands.Context, label: str, message: Optional[discord.Message]):
|
||||
"""Create a new bookmark"""
|
||||
"""Create a new bookmark for message `message` with label `label`.
|
||||
|
||||
Instead of the link to a message, you can also reply to the message you wish to bookmark. In this case,
|
||||
the `message`-argument can be left out.
|
||||
|
||||
`label` can not be names (or aliases) of subcommands. However, names with spaces are allowed. If you wish
|
||||
to use a name with spaces, it must be wrapped in "quotes".
|
||||
"""
|
||||
# If no message was passed, allow replying to the message that should be bookmarked
|
||||
if message is None and ctx.message.reference is not None:
|
||||
message = await self.client.resolve_message(ctx.message.reference)
|
||||
|
@ -116,9 +137,12 @@ class Discord(commands.Cog):
|
|||
# Label isn't allowed
|
||||
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"])
|
||||
async def bookmark_delete(self, ctx: commands.Context, bookmark_id: str):
|
||||
"""Delete a bookmark by its id"""
|
||||
"""Delete the bookmark with id `bookmark_id`.
|
||||
|
||||
You can only delete your own bookmarks.
|
||||
"""
|
||||
# The bookmarks are displayed with a hashtag in front of the id
|
||||
# so strip it out in case people want to try and use this
|
||||
bookmark_id = bookmark_id.removeprefix("#")
|
||||
|
@ -138,9 +162,18 @@ class Discord(commands.Cog):
|
|||
|
||||
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"])
|
||||
async def bookmark_search(self, ctx: commands.Context, *, query: Optional[str] = None):
|
||||
"""Search through the list of bookmarks"""
|
||||
"""Search through the list of bookmarks.
|
||||
|
||||
If a value for `query` was provided, results will be filtered down to only labels that include `query`.
|
||||
Otherwise, all bookmarks are displayed.
|
||||
|
||||
The `query`-argument can contain spaces and does not require quotes around it. For example:
|
||||
```
|
||||
didier bookmark search some query with multiple words
|
||||
```
|
||||
"""
|
||||
async with self.client.postgres_session as session:
|
||||
results = await bookmarks.get_bookmarks(session, ctx.author.id, query=query)
|
||||
|
||||
|
@ -160,15 +193,25 @@ class Discord(commands.Cog):
|
|||
modal = CreateBookmark(self.client, message.jump_url)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
@commands.command(name="Join", usage="[Thread]")
|
||||
@commands.command(name="join")
|
||||
async def join(self, ctx: commands.Context, thread: discord.Thread):
|
||||
"""Make Didier join a thread"""
|
||||
"""Make Didier join `thread`.
|
||||
|
||||
This command should generally not be necessary, as Didier automatically joins threads. However, it's possible
|
||||
that Didier is offline at the moment of a thread being created.
|
||||
|
||||
Alternatively, you can also `@mention` Didier to pull him into the thread instead.
|
||||
"""
|
||||
if thread.me is not None:
|
||||
return await ctx.reply()
|
||||
|
||||
@commands.command(name="Pin", usage="[Message]")
|
||||
@commands.command(name="pin")
|
||||
async def pin(self, ctx: commands.Context, message: Optional[discord.Message] = None):
|
||||
"""Pin a message in the current channel"""
|
||||
"""Pin `message` in the current channel.
|
||||
|
||||
Instead of the link to a message, you can also reply to the message you wish to pin. In this case,
|
||||
the `message`-argument can be left out.
|
||||
"""
|
||||
# If no message was passed, allow replying to the message that should be pinned
|
||||
if message is None and ctx.message.reference is not None:
|
||||
message = await self.client.resolve_message(ctx.message.reference)
|
||||
|
|
|
@ -18,7 +18,7 @@ class Fun(commands.Cog):
|
|||
client: Didier
|
||||
|
||||
# Slash groups
|
||||
memes_slash = app_commands.Group(name="meme", description="Commands to generate memes", guild_only=False)
|
||||
memes_slash = app_commands.Group(name="meme", description="Commands to generate memes.", guild_only=False)
|
||||
|
||||
def __init__(self, client: Didier):
|
||||
self.client = client
|
||||
|
@ -31,57 +31,62 @@ class Fun(commands.Cog):
|
|||
|
||||
@commands.hybrid_command(
|
||||
name="dadjoke",
|
||||
aliases=["Dad", "Dj"],
|
||||
description="Why does Yoda's code always crash? Because there is no try.",
|
||||
aliases=["dad", "dj"],
|
||||
)
|
||||
async def dad_joke(self, ctx: commands.Context):
|
||||
"""Get a random dad joke"""
|
||||
"""Why does Yoda's code always crash? Because there is no try."""
|
||||
async with self.client.postgres_session as session:
|
||||
joke = await get_random_dad_joke(session)
|
||||
return await ctx.reply(joke.joke, mention_author=False)
|
||||
|
||||
@commands.group(name="Memegen", aliases=["Meme", "Memes"], invoke_without_command=True, case_insensitive=True)
|
||||
async def memegen_msg(self, ctx: commands.Context, meme_name: str, *, fields: str):
|
||||
"""Command group for meme-related commands"""
|
||||
@commands.group(name="memegen", aliases=["meme", "memes"], invoke_without_command=True, case_insensitive=True)
|
||||
async def memegen_msg(self, ctx: commands.Context, template: str, *, fields: str):
|
||||
"""Generate a meme with template `template` and fields `fields`.
|
||||
|
||||
The arguments are parsed based on spaces. Arguments that contain spaces should be wrapped in "quotes".
|
||||
|
||||
Example: `memegen a b c d` will be parsed as `template: "a"`, `fields: ["b", "c", "d"]`
|
||||
|
||||
Example: `memegen "a b" "c d"` will be parsed as `template: "a b"`, `fields: ["c d"]`
|
||||
"""
|
||||
async with ctx.typing():
|
||||
meme = await self._do_generate_meme(meme_name, shlex.split(fields))
|
||||
meme = await self._do_generate_meme(template, shlex.split(fields))
|
||||
return await ctx.reply(meme, mention_author=False)
|
||||
|
||||
@memegen_msg.command(name="Preview", aliases=["P"])
|
||||
async def memegen_preview_msg(self, ctx: commands.Context, meme_name: str):
|
||||
"""Generate a preview for a meme, to see how the fields are structured"""
|
||||
@memegen_msg.command(name="preview", aliases=["p"])
|
||||
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."""
|
||||
async with ctx.typing():
|
||||
fields = [f"Field #{i + 1}" for i in range(20)]
|
||||
meme = await self._do_generate_meme(meme_name, fields)
|
||||
meme = await self._do_generate_meme(template, fields)
|
||||
return await ctx.reply(meme, mention_author=False)
|
||||
|
||||
@memes_slash.command(name="generate", description="Generate a meme")
|
||||
async def memegen_slash(self, interaction: discord.Interaction, meme: str):
|
||||
"""Slash command to generate a meme"""
|
||||
@memes_slash.command(name="generate")
|
||||
async def memegen_slash(self, interaction: discord.Interaction, template: str):
|
||||
"""Generate a meme with template `template`."""
|
||||
async with self.client.postgres_session as session:
|
||||
result = expect(await get_meme_by_name(session, meme), entity_type="meme", argument=meme)
|
||||
result = expect(await get_meme_by_name(session, template), entity_type="meme", argument=template)
|
||||
|
||||
modal = GenerateMeme(self.client, result)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
@memes_slash.command(
|
||||
name="preview", description="Generate a preview for a meme, to see how the fields are structured"
|
||||
)
|
||||
async def memegen_preview_slash(self, interaction: discord.Interaction, meme: str):
|
||||
"""Slash command to generate a meme preview"""
|
||||
@memes_slash.command(name="preview")
|
||||
@app_commands.describe(template="The meme template to use in the preview.")
|
||||
async def memegen_preview_slash(self, interaction: discord.Interaction, template: str):
|
||||
"""Generate a preview for a meme, to see how the fields are structured."""
|
||||
await interaction.response.defer()
|
||||
|
||||
fields = [f"Field #{i + 1}" for i in range(20)]
|
||||
meme_url = await self._do_generate_meme(meme, fields)
|
||||
meme_url = await self._do_generate_meme(template, fields)
|
||||
|
||||
await interaction.followup.send(meme_url, ephemeral=True)
|
||||
|
||||
@memegen_slash.autocomplete("meme")
|
||||
@memegen_preview_slash.autocomplete("meme")
|
||||
async def _memegen_slash_autocomplete_meme(
|
||||
@memegen_slash.autocomplete("template")
|
||||
@memegen_preview_slash.autocomplete("template")
|
||||
async def _memegen_slash_autocomplete_template(
|
||||
self, _: discord.Interaction, current: str
|
||||
) -> list[app_commands.Choice[str]]:
|
||||
"""Autocompletion for the 'meme'-parameter"""
|
||||
"""Autocompletion for the 'template'-parameter"""
|
||||
return self.client.database_caches.memes.get_autocomplete_suggestions(current)
|
||||
|
||||
|
||||
|
|
|
@ -1,44 +1,267 @@
|
|||
from typing import List, Mapping, Optional
|
||||
import inspect
|
||||
import re
|
||||
from typing import Mapping, Optional, Type
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from overrides import overrides
|
||||
|
||||
from didier import Didier
|
||||
from didier.utils.discord.colours import error_red
|
||||
from didier.utils.discord.flags import PosixFlags
|
||||
from didier.utils.types.string import re_find_all, re_replace_with_list
|
||||
|
||||
|
||||
class CustomHelpCommand(commands.MinimalHelpCommand):
|
||||
"""Customised Help command to override the default implementation
|
||||
"""Customised Help command that overrides the default implementation
|
||||
|
||||
The default is ugly as hell, so we do some fiddling with it
|
||||
The default is ugly as hell, so we do some fiddling with it and put everything
|
||||
in fancy embeds
|
||||
"""
|
||||
|
||||
@overrides
|
||||
async def command_callback(self, ctx: commands.Context, /, *, command: Optional[str] = None):
|
||||
"""Slightly modify the original command_callback to better suit my needs"""
|
||||
|
||||
# No argument provided: send a list of all cogs
|
||||
if command is None:
|
||||
mapping = self.get_bot_mapping()
|
||||
return await self.send_bot_help(mapping)
|
||||
|
||||
command = command.lower()
|
||||
|
||||
# Hide cogs the user is not allowed to see
|
||||
cogs = list(ctx.bot.cogs.values())
|
||||
cogs = await self._filter_cogs(cogs)
|
||||
# Allow fetching cogs case-insensitively
|
||||
cog = self._get_cog(cogs, command)
|
||||
if cog is not None:
|
||||
return await self.send_cog_help(cog)
|
||||
|
||||
# Traverse tree of commands
|
||||
keys = command.split(" ")
|
||||
current_command = ctx.bot.all_commands.get(keys[0])
|
||||
|
||||
# No command found
|
||||
if current_command is None:
|
||||
return await self.send_error_message(self.command_not_found(keys[0]))
|
||||
|
||||
# Look for subcommands
|
||||
for key in keys[1:]:
|
||||
try:
|
||||
found = current_command.all_commands.get(key) # type: ignore
|
||||
except AttributeError:
|
||||
return await self.send_error_message(self.subcommand_not_found(current_command, key))
|
||||
else:
|
||||
if found is None:
|
||||
return await self.send_error_message(self.subcommand_not_found(current_command, key))
|
||||
|
||||
current_command = found
|
||||
|
||||
if isinstance(current_command, commands.Group):
|
||||
return await self.send_group_help(current_command)
|
||||
else:
|
||||
return await self.send_command_help(current_command)
|
||||
|
||||
@overrides
|
||||
def command_not_found(self, string: str, /) -> str:
|
||||
return f"Found no command named `{string}`."
|
||||
|
||||
@overrides
|
||||
def get_command_signature(self, command: commands.Command, /) -> str:
|
||||
signature_list = [command.name]
|
||||
|
||||
# Perform renaming for hybrid commands
|
||||
if hasattr(command.callback, "__discord_app_commands_param_rename__"):
|
||||
renames = command.callback.__discord_app_commands_param_rename__
|
||||
else:
|
||||
renames = {}
|
||||
|
||||
sig = command.params
|
||||
|
||||
for name, param in sig.items():
|
||||
name = renames.get(name, name)
|
||||
is_optional = param.default is not param.empty
|
||||
|
||||
# Wrap optional arguments in square brackets
|
||||
if is_optional:
|
||||
name = f"[{name}]"
|
||||
|
||||
# If there are options/flags, add them
|
||||
# The hardcoded name-check is done for performance reasons
|
||||
if name == "flags" and inspect.isclass(param.annotation) and issubclass(param.annotation, PosixFlags):
|
||||
signature_list.append("[--OPTIONS]")
|
||||
else:
|
||||
signature_list.append(name)
|
||||
|
||||
return " ".join(signature_list)
|
||||
|
||||
@overrides
|
||||
async def send_bot_help(self, mapping: Mapping[Optional[commands.Cog], list[commands.Command]], /):
|
||||
embed = self._help_embed_base("Categories")
|
||||
filtered_cogs = await self._filter_cogs(list(mapping.keys()))
|
||||
embed.description = "\n".join(list(map(lambda cog: cog.qualified_name, filtered_cogs)))
|
||||
await self.context.reply(embed=embed, mention_author=False)
|
||||
|
||||
@overrides
|
||||
async def send_cog_help(self, cog: commands.Cog, /):
|
||||
embed = self._help_embed_base(cog.qualified_name)
|
||||
embed.description = cog.description
|
||||
|
||||
commands_names = list(map(lambda c: c.qualified_name, cog.get_commands()))
|
||||
commands_names.sort()
|
||||
|
||||
embed.add_field(name="Commands", value=", ".join(commands_names), inline=False)
|
||||
|
||||
return await self.context.reply(embed=embed, mention_author=False)
|
||||
|
||||
@overrides
|
||||
async def send_command_help(self, command: commands.Command, /):
|
||||
embed = self._help_embed_base(command.qualified_name)
|
||||
self._add_command_help(embed, command)
|
||||
|
||||
return await self.context.reply(embed=embed, mention_author=False)
|
||||
|
||||
@overrides
|
||||
async def send_group_help(self, group: commands.Group, /):
|
||||
embed = self._help_embed_base(group.qualified_name)
|
||||
|
||||
if group.invoke_without_command:
|
||||
self._add_command_help(embed, group)
|
||||
|
||||
subcommand_names = list(map(lambda c: c.name, group.commands))
|
||||
subcommand_names.sort()
|
||||
|
||||
embed.add_field(name="Subcommands", value=", ".join(subcommand_names))
|
||||
|
||||
return await self.context.reply(embed=embed, mention_author=False)
|
||||
|
||||
@overrides
|
||||
async def send_error_message(self, error: str, /):
|
||||
embed = discord.Embed(colour=error_red(), title="Help", description=error)
|
||||
return await self.context.reply(embed=embed, mention_author=False)
|
||||
|
||||
@overrides
|
||||
def subcommand_not_found(self, command: commands.Command, string: str, /) -> str:
|
||||
return f"Found no subcommand named `{string}` for command `{command.qualified_name}`."
|
||||
|
||||
def _help_embed_base(self, title: str) -> discord.Embed:
|
||||
"""Create the base structure for the embeds that get sent with the Help commands"""
|
||||
embed = discord.Embed(title=title, colour=discord.Colour.blue())
|
||||
embed.set_footer(text="Syntax: Didier Help [Categorie] of Didier Help [Commando]")
|
||||
embed = discord.Embed(title=title.title(), colour=discord.Colour.blue())
|
||||
return embed
|
||||
|
||||
async def _filter_cogs(self, cogs: List[commands.Cog]) -> List[commands.Cog]:
|
||||
def _clean_command_doc(self, command: commands.Command) -> str:
|
||||
"""Clean up a help docstring
|
||||
|
||||
This will strip out single newlines, because these are only there for readability and line length.
|
||||
These are instead replaced with spaces.
|
||||
|
||||
Code in codeblocks is ignored, as it is used to create examples.
|
||||
"""
|
||||
description = command.help
|
||||
codeblocks = re_find_all(r"\n?```.*?```", description, flags=re.DOTALL)
|
||||
|
||||
# Regex borrowed from https://stackoverflow.com/a/59843498/13568999
|
||||
description = re.sub(
|
||||
r"([^\S\n]*\n(?:[^\S\n]*\n)+[^\S\n]*)|[^\S\n]*\n[^\S\n]*", lambda x: x.group(1) or " ", description
|
||||
)
|
||||
|
||||
# Replace codeblocks with their original form
|
||||
if codeblocks:
|
||||
description = re_replace_with_list(r"```.*?```", description, codeblocks)
|
||||
|
||||
# Add flag help in
|
||||
flags_class = self._get_flags_class(command)
|
||||
if flags_class is not None:
|
||||
description += f"\n\n{self.get_flags_help(flags_class)}"
|
||||
|
||||
return description
|
||||
|
||||
def _add_command_help(self, embed: discord.Embed, command: commands.Command):
|
||||
"""Add command-related information to an embed
|
||||
|
||||
This allows re-using this logic for Group commands that can be invoked by themselves.
|
||||
"""
|
||||
embed.description = self._clean_command_doc(command)
|
||||
|
||||
signature = self.get_command_signature(command)
|
||||
embed.add_field(name="Signature", value=signature, inline=False)
|
||||
|
||||
if command.aliases:
|
||||
embed.add_field(name="Aliases", value=", ".join(command.aliases), inline=False)
|
||||
|
||||
def _get_cog(self, cogs: list[commands.Cog], name: str) -> Optional[commands.Cog]:
|
||||
"""Try to find a cog, case-insensitively"""
|
||||
for cog in cogs:
|
||||
if cog.qualified_name.lower() == name:
|
||||
return cog
|
||||
|
||||
return None
|
||||
|
||||
async def _filter_cogs(self, cogs: list[commands.Cog]) -> list[commands.Cog]:
|
||||
"""Filter the list of cogs down to all those that the user can see"""
|
||||
|
||||
async def _predicate(cog: Optional[commands.Cog]) -> bool:
|
||||
if cog is None:
|
||||
return False
|
||||
|
||||
# Remove cogs that we never want to see in the help page because they
|
||||
# don't contain commands
|
||||
filtered_cogs = list(filter(lambda cog: cog is not None and cog.qualified_name.lower() not in ("tasks",), cogs))
|
||||
# don't contain commands, or shouldn't be visible at all
|
||||
if not cog.get_commands():
|
||||
return False
|
||||
|
||||
# Remove owner-only cogs
|
||||
if cog.qualified_name.lower() in ("tasks", "debugcog"):
|
||||
return False
|
||||
|
||||
# Hide owner-only cogs if you're not the owner
|
||||
if not await self.context.bot.is_owner(self.context.author):
|
||||
filtered_cogs = list(filter(lambda cog: cog.qualified_name.lower() not in ("owner",), filtered_cogs))
|
||||
return cog.qualified_name.lower() not in ("owner",)
|
||||
|
||||
return True
|
||||
|
||||
# Filter list of cogs down
|
||||
filtered_cogs = [cog for cog in cogs if await _predicate(cog)]
|
||||
return list(sorted(filtered_cogs, key=lambda cog: cog.qualified_name))
|
||||
|
||||
@overrides
|
||||
async def send_bot_help(self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]], /):
|
||||
embed = self._help_embed_base("Categorieën")
|
||||
filtered_cogs = await self._filter_cogs(list(mapping.keys()))
|
||||
embed.description = "\n".join(list(map(lambda cog: cog.qualified_name, filtered_cogs)))
|
||||
await self.get_destination().send(embed=embed)
|
||||
def _get_flags_class(self, command: commands.Command) -> Optional[Type[PosixFlags]]:
|
||||
"""Check if a command has flags"""
|
||||
flag_param = command.params.get("flags", None)
|
||||
if flag_param is None:
|
||||
return None
|
||||
|
||||
if issubclass(flag_param.annotation, PosixFlags):
|
||||
return flag_param.annotation
|
||||
|
||||
return None
|
||||
|
||||
def get_flags_help(self, flags_class: Type[PosixFlags]) -> str:
|
||||
"""Get the description for flag arguments"""
|
||||
help_data = []
|
||||
|
||||
# Present flags in alphabetical order, as dicts have no set ordering
|
||||
flag_mapping = flags_class.__commands_flags__
|
||||
flags = list(flag_mapping.items())
|
||||
flags.sort(key=lambda f: f[0])
|
||||
|
||||
for name, flag in flags:
|
||||
flag_names = [name, *flag.aliases]
|
||||
# Add the --prefix in front of all flags
|
||||
flag_names = list(map(lambda n: f"--{n}", flag_names))
|
||||
help_data.append(f"{', '.join(flag_names)} [default `{flag.default}`]")
|
||||
|
||||
return "Options:\n" + "\n".join(help_data)
|
||||
|
||||
|
||||
async def setup(client: Didier):
|
||||
"""Load the cog"""
|
||||
client.help_command = CustomHelpCommand()
|
||||
help_str = (
|
||||
"Shows the help page for a category or command. "
|
||||
"`/commands` are not included, as they already have built-in descriptions in the UI."
|
||||
"\n\nThe command signatures follow the POSIX-standard format for help messages:"
|
||||
"\n- `required_positional_argument`"
|
||||
"\n- `[optional_positional_argument]`"
|
||||
)
|
||||
|
||||
attributes = {"aliases": ["h", "man"], "usage": "[category or command]", "help": help_str}
|
||||
|
||||
client.help_command = CustomHelpCommand(command_attrs=attributes)
|
||||
|
|
|
@ -8,21 +8,31 @@ from didier import Didier
|
|||
|
||||
|
||||
class Meta(commands.Cog):
|
||||
"""Cog for Didier-related commands"""
|
||||
"""Commands related to Didier himself."""
|
||||
|
||||
client: Didier
|
||||
|
||||
def __init__(self, client: Didier):
|
||||
self.client = client
|
||||
|
||||
@commands.command(name="Marco")
|
||||
@commands.command(name="marco")
|
||||
async def marco(self, ctx: commands.Context):
|
||||
"""Ping command to get the delay of the bot"""
|
||||
"""Get Didier's latency."""
|
||||
return await ctx.reply(f"Polo! {round(self.client.latency * 1000)}ms", mention_author=False)
|
||||
|
||||
@commands.command(name="Source", aliases=["Src"])
|
||||
@commands.command(name="source", aliases=["src"])
|
||||
async def source(self, ctx: commands.Context, *, command_name: Optional[str] = None):
|
||||
"""Command to get links to the source code of Didier"""
|
||||
"""Get a link to the source code of Didier.
|
||||
|
||||
If a value for `command_name` is passed, the source for `command_name` is shown instead.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
didier source
|
||||
didier source dinks
|
||||
didier source source
|
||||
```
|
||||
"""
|
||||
repo_home = "https://github.com/stijndcl/didier"
|
||||
|
||||
if command_name is None:
|
||||
|
@ -38,12 +48,12 @@ class Meta(commands.Cog):
|
|||
filename = src.co_filename
|
||||
|
||||
if command is None:
|
||||
return await ctx.reply(f"Geen commando gevonden voor ``{command_name}``.", mention_author=False)
|
||||
return await ctx.reply(f"Found no command named `{command_name}`.", mention_author=False)
|
||||
|
||||
lines, first_line = inspect.getsourcelines(src)
|
||||
|
||||
if filename is None:
|
||||
return await ctx.reply(f"Geen code gevonden voor ``{command_name}``.", mention_author=False)
|
||||
return await ctx.reply(f"Found no source file for `{command_name}`.", mention_author=False)
|
||||
|
||||
file_location = os.path.relpath(filename).replace("\\", "/")
|
||||
|
||||
|
|
|
@ -13,16 +13,18 @@ from didier.data.scrapers import google
|
|||
|
||||
|
||||
class Other(commands.Cog):
|
||||
"""Cog for commands that don't really belong anywhere else"""
|
||||
"""Commands that don't really belong anywhere else."""
|
||||
|
||||
client: Didier
|
||||
|
||||
def __init__(self, client: Didier):
|
||||
self.client = client
|
||||
|
||||
@commands.hybrid_command(name="define", description="Urban Dictionary", aliases=["Ud", "Urban"], usage="[Term]")
|
||||
@commands.hybrid_command(
|
||||
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):
|
||||
"""Look up the definition of a word on the Urban Dictionary"""
|
||||
"""Look up the definition of `query` on the Urban Dictionary."""
|
||||
async with ctx.typing():
|
||||
status_code, definitions = await urban_dictionary.lookup(self.client.http_session, query)
|
||||
if not definitions:
|
||||
|
@ -30,10 +32,17 @@ class Other(commands.Cog):
|
|||
|
||||
await ctx.reply(embed=definitions[0].to_embed(), mention_author=False)
|
||||
|
||||
@commands.hybrid_command(name="google", description="Google search", usage="[Query]")
|
||||
@commands.hybrid_command(name="google", description="Google search")
|
||||
@app_commands.describe(query="Search query")
|
||||
async def google(self, ctx: commands.Context, *, query: str):
|
||||
"""Google something"""
|
||||
"""Show the Google search results for `query`.
|
||||
|
||||
The `query`-argument can contain spaces and does not require quotes around it. For example:
|
||||
```
|
||||
didier query didier source github
|
||||
didier query "didier source github"
|
||||
```
|
||||
"""
|
||||
async with ctx.typing():
|
||||
results = await google.google_search(self.client.http_session, query)
|
||||
embed = GoogleSearch(results).to_embed()
|
||||
|
@ -43,9 +52,9 @@ class Other(commands.Cog):
|
|||
async with self.client.postgres_session as session:
|
||||
return await get_link_by_name(session, name.lower())
|
||||
|
||||
@commands.command(name="Link", aliases=["Links"], usage="[Name]")
|
||||
@commands.command(name="Link", aliases=["Links"])
|
||||
async def link_msg(self, ctx: commands.Context, name: str):
|
||||
"""Message command to get the link to something"""
|
||||
"""Get the link to the resource named `name`."""
|
||||
link = await self._get_link(name)
|
||||
if link is None:
|
||||
return await ctx.reply(f"Found no links matching `{name}`.", mention_author=False)
|
||||
|
@ -53,10 +62,10 @@ class Other(commands.Cog):
|
|||
target_message = await self.client.get_reply_target(ctx)
|
||||
await target_message.reply(link.url, mention_author=False)
|
||||
|
||||
@app_commands.command(name="link", description="Get the link to something")
|
||||
@app_commands.describe(name="The name of the link")
|
||||
@app_commands.command(name="link")
|
||||
@app_commands.describe(name="The name of the resource")
|
||||
async def link_slash(self, interaction: discord.Interaction, name: str):
|
||||
"""Slash command to get the link to something"""
|
||||
"""Get the link to something."""
|
||||
link = await self._get_link(name)
|
||||
if link is None:
|
||||
return await interaction.response.send_message(f"Found no links matching `{name}`.", ephemeral=True)
|
||||
|
|
|
@ -27,21 +27,26 @@ class School(commands.Cog):
|
|||
def __init__(self, client: Didier):
|
||||
self.client = client
|
||||
|
||||
@commands.hybrid_command(name="deadlines", description="Show upcoming deadlines")
|
||||
@commands.hybrid_command(name="deadlines")
|
||||
async def deadlines(self, ctx: commands.Context):
|
||||
"""Show upcoming deadlines"""
|
||||
"""Show upcoming deadlines."""
|
||||
async with self.client.postgres_session as session:
|
||||
deadlines = await get_deadlines(session)
|
||||
|
||||
embed = Deadlines(deadlines).to_embed()
|
||||
await ctx.reply(embed=embed, mention_author=False, ephemeral=False)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="les", description="Show your personalized schedule for a given day.", aliases=["Sched", "Schedule"]
|
||||
)
|
||||
@commands.hybrid_command(name="les", aliases=["sched", "schedule"])
|
||||
@app_commands.rename(day_dt="date")
|
||||
async def les(self, ctx: commands.Context, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None):
|
||||
"""Show your personalized schedule for a given day."""
|
||||
"""Show your personalized schedule for a given day.
|
||||
|
||||
If no day is provided, this defaults to the schedule for the current day. When invoked during a weekend,
|
||||
it will skip forward to the next weekday instead.
|
||||
|
||||
Schedules are personalized based on your roles in the server. If your schedule doesn't look right, make sure
|
||||
that you've got the correct roles selected. In case you do, ping D STIJN.
|
||||
"""
|
||||
if day_dt is None:
|
||||
day_dt = date.today()
|
||||
|
||||
|
@ -62,14 +67,14 @@ class School(commands.Cog):
|
|||
|
||||
@commands.hybrid_command(
|
||||
name="menu",
|
||||
description="Show the menu in the Ghent University restaurants.",
|
||||
aliases=["Eten", "Food"],
|
||||
aliases=["eten", "food"],
|
||||
)
|
||||
@app_commands.rename(day_dt="date")
|
||||
async def menu(self, ctx: commands.Context, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None):
|
||||
"""Show the menu in the Ghent University restaurants.
|
||||
"""Show the menu in the Ghent University restaurants on `date`.
|
||||
|
||||
Menus are Dutch, as a lot of dishes have very weird translations
|
||||
If no value for `date` is provided, this defaults to the schedule for the current day.
|
||||
Menus are shown in Dutch by default, as a lot of dishes have very weird translations.
|
||||
"""
|
||||
if day_dt is None:
|
||||
day_dt = date.today()
|
||||
|
@ -83,11 +88,22 @@ class School(commands.Cog):
|
|||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="fiche", description="Sends the link to the study guide for [Course]", 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)")
|
||||
async def study_guide(self, ctx: commands.Context, course: str, *, flags: StudyGuideFlags):
|
||||
"""Create links to study guides"""
|
||||
"""Sends the link to the study guide for `course`.
|
||||
|
||||
The value for `course` can contain spaces, but must be wrapped in "quotes".
|
||||
|
||||
Aliases (nicknames) for courses are also accepted, given that they are known and in the database.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
didier fiche ad2
|
||||
didier fiche "algoritmen en datastructuren 2"
|
||||
```
|
||||
"""
|
||||
async with self.client.postgres_session as session:
|
||||
ufora_course = await ufora_courses.get_course_by_name(session, course)
|
||||
|
||||
|
|
|
@ -190,8 +190,10 @@ class Tasks(commands.Cog):
|
|||
await self.client.wait_until_ready()
|
||||
|
||||
@check_birthdays.error
|
||||
@pull_schedules.error
|
||||
@pull_ufora_announcements.error
|
||||
@remove_old_ufora_announcements.error
|
||||
@reset_wordle_word.error
|
||||
async def _on_tasks_error(self, error: BaseException):
|
||||
"""Error handler for all tasks"""
|
||||
print("".join(traceback.format_exception(type(error), error, error.__traceback__)))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import math
|
||||
from typing import Optional
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
|
||||
__all__ = ["abbreviate", "leading", "pluralize", "get_edu_year_name"]
|
||||
__all__ = ["abbreviate", "leading", "pluralize", "re_find_all", "re_replace_with_list", "get_edu_year_name"]
|
||||
|
||||
|
||||
def abbreviate(text: str, max_length: int) -> str:
|
||||
|
@ -45,6 +46,29 @@ def pluralize(word: str, amount: int, plural_form: Optional[str] = None) -> str:
|
|||
return plural_form or (word + "s")
|
||||
|
||||
|
||||
def re_find_all(pattern: str, string: str, flags: Union[int, re.RegexFlag] = 0) -> list[str]:
|
||||
"""Find all matches of a regex in a string"""
|
||||
matches = []
|
||||
|
||||
while True:
|
||||
match = re.search(pattern, string, flags=flags)
|
||||
if not match:
|
||||
break
|
||||
|
||||
matches.append(match.group(0))
|
||||
string = string[match.end() :]
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def re_replace_with_list(pattern: str, string: str, replacements: list[str]) -> str:
|
||||
"""Replace all matches of a pattern one by one using a list of replacements"""
|
||||
for replacement in replacements:
|
||||
string = re.sub(pattern, replacement, string, count=1)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def get_edu_year_name(year: int) -> str: # pragma: no cover
|
||||
"""Get the string representation of a university year"""
|
||||
years = ["1st Bachelor", "2nd Bachelor", "3rd Bachelor", "1st Master", "2nd Master"]
|
||||
|
|
Loading…
Reference in New Issue