Compare commits

...

10 Commits

Author SHA1 Message Date
Myra
3fd23d4163 Mod cog: Option to show an extra field with custom content on the ban embed (#6593)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2025-08-10 17:54:51 -04:00
Jakub Kuczys
2dbbb51208 Disable Rich tracebacks by default (#6576) 2025-08-09 17:45:03 -04:00
TrustyJAID
b177c80b4e Fix an issue with alias quote detection (#6582)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2025-08-09 16:55:37 -04:00
Ben Cos
029029e9a5 Add missing dependency in label pattern exhaustiveness check (#6589) 2025-08-01 04:59:48 +02:00
TrustyJAID
c6ff2191f3 Fix Reports in dm's not checking the selected guild for a configured channel (#6573) 2025-06-11 19:16:22 -04:00
Kreusada
6603cd1a86 Check attach_files permission inside send_interactive (#6552)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2025-05-25 17:04:35 -04:00
Kreusada
1daf56f3d8 Add Group.all method explanation to Config framework (#6550)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2025-05-25 14:48:47 -04:00
Kreusada
b3f0349ba2 [Docs] Update publishing cogs guide with [botname] substitution tip (#6539)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2025-05-25 14:39:02 -04:00
A Frozen Lake
bfc3561928 Fix formatting of [p]names. (#6538)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2025-05-25 14:33:28 -04:00
github-actions[bot]
8d8918b3c6 Version bump to 3.5.21.dev1 (#6571)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-03 16:13:01 +00:00
14 changed files with 280 additions and 33 deletions

View File

@@ -17,7 +17,7 @@ jobs:
- name: Install script's pre-requirements
run: |
python -m pip install -U pip
python -m pip install -U pathspec pyyaml rich
python -m pip install -U pathspec pyyaml rich typing_extensions
- name: Check label pattern exhaustiveness
run: |
python .github/workflows/scripts/check_label_pattern_exhaustiveness.py

View File

@@ -253,7 +253,23 @@ modset dm
.. code-block:: none
[p]modset dm [enabled]
[p]modset dm
**Description**
Settings for messaging the user when being kicked or banned.
.. _mod-command-modset-dm-sendmessage:
"""""""""""""""""""""
modset dm sendmessage
"""""""""""""""""""""
**Syntax**
.. code-block:: none
[p]modset dm sendmessage [enabled]
**Description**
@@ -266,6 +282,72 @@ and reason as to why they were kicked/banned.
* ``[enabled]``: Whether a message should be sent to a user when they are kicked/banned. |bool-input|
.. _mod-command-modset-banshowextrafield:
"""""""""""""""""""""""""""
modset dm banshowextrafield
"""""""""""""""""""""""""""
**Syntax**
.. code-block:: none
[p]modset dm banshowextrafield [enabled]
**Description**
Toggle whether to show an extra customizable field when banning.
This can be used to add additional information for the banned user, such as a ban appeal link.
**Arguments**
* ``[enabled]``: If an extra customizable embed field should appear when banning. |bool-input|
.. _mod-command-modset-banextrafieldtitle:
""""""""""""""""""""""""""""
modset dm banextrafieldtitle
""""""""""""""""""""""""""""
**Syntax**
.. code-block:: none
[p]modset dm banextrafieldtitle [title]
**Description**
Set the title for the optional extra embed on ban.
Cannot be over 252 characters long.
**Arguments**
* ``[title]``: The title of the embed field. Can by any string of text under 252 charcters long.
.. _mod-command-modset-banextrafieldcontents:
"""""""""""""""""""""""""""""""
modset dm banextrafieldcontents
"""""""""""""""""""""""""""""""
**Syntax**
.. code-block:: none
[p]modset dm banextrafieldcontents [contents]
**Description**
Set the contents for the optional extra embed on ban
Cannot be over 1024 characters long.
**Arguments**
* ``[contents]``: The contents of the embed field. Can by any string of text under 1024 charcters long.
.. _mod-command-modset-requirereason:
""""""""""""""""""""

View File

@@ -155,6 +155,22 @@ Here is an example of the :code:`async with` syntax:
blah.append(new_blah)
await ctx.send("The new blah value has been added!")
There is also a :py:meth:`Group.all` method. This will return all the stored data associated
with a specific config group as a :py:class:`dict`. By negating the need to excessively call config,
this method can be particularly useful when multiple values are to be retrieved from the same group.
Here is an example of :py:meth:`Group.all` usage:
.. code-block:: python
@commands.command()
async def getall(self, ctx):
all_global_data = await self.config.all()
await ctx.send("Foobar is {foobar}, foo baz is {foo_baz}".format(
foobar=str(all_global_data["foobar"]),
foo_baz=str(all_global_data["foo"]["baz"])
))
.. important::
@@ -398,7 +414,7 @@ We're responsible pet owners here, so we've also got to have a way to feed our p
# We could accomplish the same thing a slightly different way
await self.config.user(ctx.author).pets.get_attr(pet_name).hunger.set(new_hunger)
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger)
await ctx.send("Your pet is now at {}/100 hunger!".format(new_hunger))
Of course, if we're less than responsible pet owners, there are consequences::
@@ -481,7 +497,7 @@ Config prioritizes being a safe data store without developers needing to
know how end users have configured their bot.
This does come with some performance costs, so keep the following in mind when choosing to
develop using config
develop using config.
* Config use in events should be kept minimal and should only occur
after confirming the event needs to interact with config

View File

@@ -47,7 +47,7 @@ Keys common to both repo and cog info.json (case sensitive)
is installed or a repo is added
.. tip:: You can use the ``[p]`` key in your string to use the prefix
used for installing.
used for installing, and ``[botname]`` to show the bot's username.
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
is displayed when a user executes ``[p]cog list``

View File

@@ -339,7 +339,7 @@ def _early_init():
# This is bumped automatically by release workflow (`.github/workflows/scripts/bump_version.py`)
_VERSION = "3.5.20"
_VERSION = "3.5.21.dev1"
__version__, version_info = VersionInfo._get_version()

View File

@@ -59,7 +59,11 @@ class AliasEntry:
extra = []
while not view.eof:
prev = view.index
word = view.get_quoted_word()
try:
word = view.get_quoted_word()
except discord.ext.commands.errors.UnexpectedQuoteError:
view.skip_ws()
continue
if len(word) < view.index - prev:
word = "".join((view.buffer[prev], word, view.buffer[view.index - 1]))
extra.append(word.strip(" "))

View File

@@ -143,6 +143,8 @@ class KickBanMixin(MixinMeta):
toggle = await self.config.guild(guild).dm_on_kickban()
if toggle:
extra_embed = await self.config.guild(guild).ban_show_extra()
with contextlib.suppress(discord.HTTPException):
em = discord.Embed(
title=bold(_("You have been banned from {guild}.").format(guild=guild)),
@@ -153,6 +155,17 @@ class KickBanMixin(MixinMeta):
value=reason if reason is not None else _("No reason was given."),
inline=False,
)
if extra_embed:
extra_embed_title = await self.config.guild(guild).ban_extra_embed_title()
extra_embed_contents = await self.config.guild(
guild
).ban_extra_embed_contents()
em.add_field(
name=bold(extra_embed_title, escape_formatting=False),
value=extra_embed_contents,
inline=False,
)
await user.send(embed=em)
ban_type = "ban"
@@ -658,16 +671,38 @@ class KickBanMixin(MixinMeta):
with contextlib.suppress(discord.HTTPException):
# We don't want blocked DMs preventing us from banning
msg = _("You have been temporarily banned from {server_name} until {date}.").format(
server_name=guild.name, date=discord.utils.format_dt(unban_time)
extra_embed = await self.config.guild(guild).ban_show_extra()
em = discord.Embed(
title=bold(
_("You have been temporarily banned from {guild} until {date}.").format(
guild=guild, date=discord.utils.format_dt(unban_time)
)
),
color=await self.bot.get_embed_color(member),
)
em.add_field(
name=_("**Reason**"),
value=reason if reason is not None else _("No reason was given."),
inline=False,
)
if guild_data["dm_on_kickban"] and reason:
msg += _("\n\n**Reason:** {reason}").format(reason=reason)
if invite:
msg += _("\n\nHere is an invite for when your ban expires: {invite_link}").format(
invite_link=invite
em.add_field(
name=bold(_("Here is an invite for when your ban expires")),
value=invite,
inline=False,
)
await member.send(msg)
if extra_embed:
extra_embed_title = await self.config.guild(guild).ban_extra_embed_title()
extra_embed_contents = await self.config.guild(guild).ban_extra_embed_contents()
em.add_field(
name=bold(extra_embed_title, escape_formatting=False),
value=extra_embed_contents,
inline=False,
)
await member.send(embed=em)
audit_reason = get_audit_reason(author, reason, shorten=True)

View File

@@ -61,6 +61,9 @@ class Mod(
"default_days": 0,
"default_tempban_duration": 60 * 60 * 24,
"track_nicknames": True,
"ban_show_extra": False,
"ban_extra_embed_title": "Message from staff",
"ban_extra_embed_contents": "Please set me",
}
default_channel_settings = {"ignored": False}

View File

@@ -309,9 +309,9 @@ class ModInfo(MixinMeta):
usernames, display_names, nicks = await self.get_names(member)
parts = []
for header, names in (
(_("Past 20 usernames:"), usernames),
(_("Past 20 global display names:"), display_names),
(_("Past 20 server nicknames:"), nicks),
(_("Past 20 usernames: "), usernames),
(_("Past 20 global display names: "), display_names),
(_("Past 20 server nicknames: "), nicks),
):
if names:
parts.append(bold(header) + ", ".join(names))

View File

@@ -48,6 +48,9 @@ class ModSettings(MixinMeta):
dm_on_kickban = data["dm_on_kickban"]
default_days = data["default_days"]
default_tempban_duration = data["default_tempban_duration"]
ban_show_extra = data["ban_show_extra"]
ban_extra_embed_title = data["ban_extra_embed_title"]
ban_extra_embed_contents = data["ban_extra_embed_contents"]
if not track_all_names and track_nicknames:
yes_or_no = _("Overridden by another setting")
else:
@@ -98,9 +101,18 @@ class ModSettings(MixinMeta):
)
else:
msg += _("Default message history delete on ban: Don't delete any\n")
msg += _("Default tempban duration: {duration}").format(
msg += _("Default tempban duration: {duration}\n").format(
duration=humanize_timedelta(seconds=default_tempban_duration)
)
msg += _("Show optional information field in embed: {yes_or_no}\n").format(
yes_or_no=_("Yes") if ban_show_extra else _("No")
)
msg += _("Title of the optional extra field: {ban_embed_title}\n").format(
ban_embed_title=ban_extra_embed_title if ban_extra_embed_title else _("None")
)
msg += _("Contents of the optional extra field: {ban_embed_contents}").format(
ban_embed_contents=ban_extra_embed_contents if ban_extra_embed_contents else _("None")
)
await ctx.send(box(msg))
@modset.command()
@@ -347,9 +359,15 @@ class ModSettings(MixinMeta):
)
)
@modset.command()
@modset.group()
@commands.guild_only()
async def dm(self, ctx: commands.Context, enabled: bool = None):
async def dm(self, ctx: commands.Context):
"""
Settings for messaging the user when being kicked or banned.
"""
@dm.command(name="sendmessage")
async def dm_sendmessage(self, ctx: commands.Context, enabled: bool = None):
"""Toggle whether a message should be sent to a user when they are kicked/banned.
If this option is enabled, the bot will attempt to DM the user with the guild name
@@ -370,6 +388,63 @@ class ModSettings(MixinMeta):
_("Bot will no longer attempt to send a DM to user before kick and ban.")
)
@dm.command(name="banshowextrafield")
async def dm_banshowextrafield(self, ctx: commands.Context, enabled: bool = None):
"""
Toggle whether to show an extra customizable field when banning.
This can be used to add additional information for the banned user, such as a ban appeal link.
"""
guild = ctx.guild
if enabled is None:
setting = await self.config.guild(guild).ban_show_extra()
await ctx.send(
_("The extra embed field is currently set to: {setting}").format(setting=setting)
)
return
await self.config.guild(guild).ban_show_extra.set(enabled)
if enabled:
await ctx.send(
_(
"An extra field will be shown when banning. Configure it with `{prefix}modset dm banextrafieldtitle` and `{prefix}modset dm banextrafieldcontents`"
).format(prefix=ctx.prefix)
)
else:
await ctx.send(_("An extra field will be no longer be shown when banning."))
@dm.command(name="banextrafieldtitle")
async def dm_banextrafieldtitle(self, ctx: commands.Context, *, title: str) -> None:
"""
Set the title for the optional extra embed on ban.
Cannot be over 252 characters long.
"""
guild = ctx.guild
# Bolding the text is 4 characters (**bolded**)
# All the bold function used in the embeds does is add those star characters and some other convenience stuffs.
# Such as escaping formatting.
if len(title) > 252:
await ctx.send(_("Embed title cannot be over 252 characters long."))
else:
await self.config.guild(guild).ban_extra_embed_title.set(title)
await ctx.send(_("Embed Title has been set to `{title}`").format(title=title))
@dm.command(name="banextrafieldcontents")
async def dm_banextrafieldcontents(self, ctx: commands.Context, *, contents: str) -> None:
"""
Set the contents for the optional extra embed on ban
Cannot be over 1024 characters long.
"""
guild = ctx.guild
if len(contents) > 1024:
await ctx.send(_("Embed contents cannot be over 1024 characters long."))
else:
await self.config.guild(guild).ban_extra_embed_contents.set(contents)
await ctx.send(
_("Embed Contents has been set to `{contents}`").format(contents=contents)
)
@modset.command()
@commands.guild_only()
async def requirereason(self, ctx: commands.Context, enabled: bool = None):

View File

@@ -307,7 +307,7 @@ class Reports(commands.Cog):
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
if val is None:
if await self.config.guild(ctx.guild).output_channel() is None:
if await self.config.guild(guild).output_channel() is None:
await author.send(
_(
"This server has no reports channel set up. Please contact a server admin."

View File

@@ -314,6 +314,19 @@ def parse_cli_flags(args):
default=None,
help="Forcefully disables the Rich logging handlers.",
)
# DEP-WARN: use argparse.BooleanOptionalAction when we drop support for Python 3.8
parser.add_argument(
"--rich-tracebacks",
action="store_true",
default=False,
help="Format the Python exception tracebacks using Rich (with syntax highlighting)."
" *May* be useful to increase traceback readability during development.",
)
parser.add_argument(
"--no-rich-tracebacks",
action="store_false",
dest="rich_tracebacks",
)
parser.add_argument(
"--rich-traceback-extra-lines",
type=non_negative_int,

View File

@@ -2477,23 +2477,34 @@ class Red(
msg = await channel.send(box(page, lang=box_lang))
ret.append(msg)
n_remaining = len(messages) - idx
files_perm = (
not channel.guild or channel.permissions_for(channel.guild.me).attach_files
)
options = ("more", "file") if files_perm else ("more",)
if n_remaining > 0:
if n_remaining == 1:
prompt_text = _(
"There is still one message remaining. Type {command_1} to continue"
" or {command_2} to upload all contents as a file."
)
if files_perm:
prompt_text = _(
"There is still one message remaining. Type {command_1} to continue or {command_2} to upload all contents as a file."
)
else:
prompt_text = _(
"There is still one message remaining. Type {command_1} to continue."
)
else:
prompt_text = _(
"There are still {count} messages remaining. Type {command_1} to continue"
" or {command_2} to upload all contents as a file."
)
if files_perm:
prompt_text = _(
"There are still {count} messages remaining. Type {command_1} to continue or {command_2} to upload all contents as a file."
)
else:
prompt_text = _(
"There are still {count} messages remaining. Type {command_1} to continue."
)
query = await channel.send(
prompt_text.format(count=n_remaining, command_1="`more`", command_2="`file`")
)
pred = MessagePredicate.lower_contained_in(
("more", "file"), channel=channel, user=user
)
pred = MessagePredicate.lower_contained_in(options, channel=channel, user=user)
try:
resp = await self.wait_for(
"message",

View File

@@ -1,4 +1,5 @@
import argparse
import logging
import logging.handlers
import pathlib
import re
@@ -33,6 +34,7 @@ from rich.traceback import PathHighlighter, Traceback # DEP-WARN
MAX_OLD_LOGS = 8
log = logging.getLogger("red.logging")
class RotatingFileHandler(logging.handlers.RotatingFileHandler):
@@ -322,7 +324,7 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{")
stdout_handler = RedRichHandler(
rich_tracebacks=True,
rich_tracebacks=cli_flags.rich_tracebacks,
show_path=False,
highlighter=NullHighlighter(),
tracebacks_extra_lines=cli_flags.rich_traceback_extra_lines,
@@ -379,3 +381,9 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
for fhandler in (latest_fhandler, all_fhandler):
fhandler.setFormatter(file_formatter)
root_logger.addHandler(fhandler)
if not enable_rich_logging and cli_flags.rich_tracebacks:
log.warning(
"Rich tracebacks were requested but they will not be enabled"
" as Rich logging is not active."
)