jack1142 febca8ccbb
Migration to discord.py 2.0 (#5600)
* Temporarily set d.py to use latest git revision

* Remove `bot` param to Client.start

* Switch to aware datetimes

A lot of this is removing `.replace(...)` which while not technically
needed, simplifies the code base. There's only a few changes that are
actually necessary here.

* Update to work with new Asset design

* [threads] Update core ModLog API to support threads

- Added proper support for passing `Thread` to `channel`
  when creating/editing case
- Added `parent_channel_id` attribute to Modlog API's Case
    - Added `parent_channel` property that tries to get parent channel
- Updated case's content to show both thread and parent information

* [threads] Disallow usage of threads in some of the commands

- announceset channel
- filter channel clear
- filter channel add
- filter channel remove
- GlobalUniqueObjectFinder converter
    - permissions addglobalrule
    - permissions removeglobalrule
    - permissions removeserverrule
    - Permissions cog does not perform any validation for IDs
      when setting through YAML so that has not been touched
- streamalert twitch/youtube/picarto
- embedset channel
- set ownernotifications adddestination

* [threads] Handle threads in Red's permissions system (Requires)

- Made permissions system apply rules of (only) parent in threads

* [threads] Update embed_requested to support threads

- Threads don't have their own embed settings and inherit from parent

* [threads] Update Red.message_eligible_as_command to support threads

* [threads] Properly handle invocation of [p](un)mutechannel in threads

Usage of a (un)mutechannel will mute/unmute user in the parent channel
if it's invoked in a thread.

* [threads] Update Filter cog to properly handle threads

- `[p]filter channel list` in a threads sends list for parent channel
- Checking for filter hits for a message in a thread checks its parent
  channel's word list. There's no separate word list for threads.

* [threads] Support threads in Audio cog

- Handle threads being notify channels
- Update type hint for `is_query_allowed()`

* [threads] Update type hints and documentation to reflect thread support

- Documented that `{channel}` in CCs might be a thread
- Allowed (documented) usage of threads with `Config.channel()`
    - Separate thread scope is still in the picture though
      if it were to be done, it's going to be in separate in PR
- GuildContext.channel might be Thread

* Use less costy channel check in customcom's on_message_without_command

This isn't needed for d.py 2.0 but whatever...

* Update for in-place edits

* Embed's bool changed behavior, I'm hoping it doesn't affect us

* Address User.permissions_in() removal

* Swap VerificationLevel.extreme with VerificationLevel.highest

* Change to keyword-only parameters

* Change of `Guild.vanity_invite()` return type

* avatar -> display_avatar

* Fix metaclass shenanigans with Converter

* Update Red.add_cog() to be inline with `dpy_commands.Bot.add_cog()`

This means adding `override` keyword-only parameter and causing
small breakage by swapping RuntimeError with discord.ClientException.

* Address all DEP-WARNs

* Remove Context.clean_prefix and use upstream implementation instead

* Remove commands.Literal and use upstream implementation instead

Honestly, this was a rather bad implementation anyway...

Breaking but actually not really - it was provisional.

* Update Command.callback's setter

Support for functools.partial is now built into d.py

* Add new perms in HUMANIZED_PERM mapping (some from d.py 1.7 it seems)

BTW, that should really be in core instead of what we have now...

* Remove the part of do_conversion that has not worked for a long while

* Stop wrapping BadArgument in ConversionFailure

This is breaking but it's best to resolve it like this.

The functionality of ConversionFailure can be replicated with
Context.current_parameter and Context.current_argument.

* Add custom errors for int and float converters

* Remove Command.__call__ as it's now implemented in d.py

* Get rid of _dpy_reimplements

These were reimplemented for the purpose of typing
so it is no longer needed now that d.py is type hinted.

* Add return to Red.remove_cog

* Ensure we don't delete messages that differ only by used sticker

* discord.InvalidArgument->ValueError

* Move from raw <t:...> syntax to discord.utils.format_dt()

* Address AsyncIter removal

* Swap to pos-only for params that are pos-only in upstream

* Update for changes to Command.params

* [threads] Support threads in ignore checks and allow ignoring them

- Updated `[p](un)ignore channel` to accept threads
- Updated `[p]ignore list` to list ignored threads
- Updated logic in `Red.ignored_channel_or_guild()`

Ignores for guild channels now work as follows (only changes for threads):
- if channel is not a thread:
    - check if user has manage channels perm in channel
      and allow command usage if so
    - check if channel is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened
- if channel is a thread:
    - check if user has manage channels perm in parent channel
      and allow command usage if so
    - check if parent channel is ignored and disallow command usage
      if so
    - check if user has manage thread perm in parent channel
      and allow command usage if so
    - check if thread is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened

* [partial] Raise TypeError when channel is of PartialMessageable type

- Red.embed_requested
- Red.ignored_channel_or_guild

* [partial] Discard command messages when channel is PartialMessageable

* [threads] Add utilities for checking appropriate perms in both channels & threads

* [threads] Update code to use can_react_in() and @bot_can_react()

* [threads] Update code to use can_send_messages_in

* [threads] Add send_messages_in_threads perm to mute role and overrides

* [threads] Update code to use (bot/user)_can_manage_channel

* [threads] Update [p]diagnoseissues to work with threads

* Type hint fix

* [threads] Patch vendored discord.ext.menus to check proper perms in threads

I guess we've reached time when we have to patch the lib we vendor...

* Make docs generation work with non-final d.py releases

* Update discord.utils.oauth_url() usage

* Swap usage of discord.Embed.Empty/discord.embeds.EmptyEmbed to None

* Update usage of Guild.member_count to work with `None`

* Switch from Guild.vanity_invite() to Guild.vanity_url

* Update startup process to work with d.py's new asynchronous startup

* Use setup_hook() for pre-connect actions

* Update core's add_cog, remove_cog, and load_extension methods

* Update all setup functions to async and add awaits to bot.add_cog calls

* Modernize cogs by using async cog_load and cog_unload

* Address StoreChannel removal

* [partial] Disallow passing PartialMessageable to Case.channel

* [partial] Update cogs and utils to work better with PartialMessageable

- Ignore messages with PartialMessageable channel in CustomCommands cog
- In Filter cog, don't pass channel to modlog.create_case()
  if it's PartialMessageable
- In Trivia cog, only compare channel IDs
- Make `.utils.menus.menu()` work for messages
  with PartialMessageable channel
- Make checks in `.utils.tunnel.Tunnel.communicate()` more rigid

* Add few missing DEP-WARNs
2022-04-03 03:21:20 +02:00

755 lines
25 KiB
Python

import contextlib
import logging
from datetime import datetime, timedelta, timezone
from typing import Callable, List, Optional, Set, Union
import discord
from redbot.core import checks, commands, Config
from redbot.core.bot import Red
from redbot.core.commands import RawUserIdConverter
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import humanize_number
from redbot.core.utils.mod import slow_deletion, mass_purge
from redbot.core.utils.predicates import MessagePredicate
from .checks import check_self_permissions
from .converters import PositiveInt, RawMessageIds, positive_int
_ = Translator("Cleanup", __file__)
log = logging.getLogger("red.cleanup")
@cog_i18n(_)
class Cleanup(commands.Cog):
"""This cog contains commands used for "cleaning up" (deleting) messages.
This is designed as a moderator tool and offers many convenient use cases.
All cleanup commands only apply to the channel the command is executed in.
Messages older than two weeks cannot be mass deleted.
This is a limitation of the API.
"""
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, 8927348724, force_registration=True)
self.config.register_guild(notify=True)
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@staticmethod
async def check_100_plus(ctx: commands.Context, number: int) -> bool:
"""
Called when trying to delete more than 100 messages at once.
Prompts the user to choose whether they want to continue or not.
Tries its best to cleanup after itself if the response is positive.
"""
if ctx.assume_yes:
return True
prompt = await ctx.send(
_("Are you sure you want to delete {number} messages?").format(
number=humanize_number(number)
)
+ " (yes/no)"
)
response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
if response.content.lower().startswith("y"):
with contextlib.suppress(discord.NotFound):
await prompt.delete()
with contextlib.suppress(discord.HTTPException):
await response.delete()
return True
else:
await ctx.send(_("Cancelled."))
return False
@staticmethod
async def get_messages_for_deletion(
*,
channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread],
number: Optional[PositiveInt] = None,
check: Callable[[discord.Message], bool] = lambda x: True,
limit: Optional[PositiveInt] = None,
before: Union[discord.Message, datetime] = None,
after: Union[discord.Message, datetime] = None,
delete_pinned: bool = False,
) -> List[discord.Message]:
"""
Gets a list of messages meeting the requirements to be deleted.
Generally, the requirements are:
- We don't have the number of messages to be deleted already
- The message passes a provided check (if no check is provided,
this is automatically true)
- The message is less than 14 days old
- The message is not pinned
Warning: Due to the way the API hands messages back in chunks,
passing after and a number together is not advisable.
If you need to accomplish this, you should filter messages on
the entire applicable range, rather than use this utility.
"""
# This isn't actually two weeks ago to allow some wiggle room on API limits
two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14, minutes=-5)
def message_filter(message):
return (
check(message)
and message.created_at > two_weeks_ago
and (delete_pinned or not message.pinned)
)
if after:
if isinstance(after, discord.Message):
after = after.created_at
after = max(after, two_weeks_ago)
collected = []
async for message in channel.history(
limit=limit, before=before, after=after, oldest_first=False
):
if message.created_at < two_weeks_ago:
break
if message_filter(message):
collected.append(message)
if number is not None and number <= len(collected):
break
return collected
async def send_optional_notification(
self,
num: int,
channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread],
*,
subtract_invoking: bool = False,
) -> None:
"""
Sends a notification to the channel that a certain number of messages have been deleted.
"""
if not hasattr(channel, "guild") or await self.config.guild(channel.guild).notify():
if subtract_invoking:
num -= 1
if num == 1:
await channel.send(_("1 message was deleted."), delete_after=5)
else:
await channel.send(
_("{num} messages were deleted.").format(num=humanize_number(num)),
delete_after=5,
)
@staticmethod
async def get_message_from_reference(
channel: Union[discord.TextChannel, discord.Thread], reference: discord.MessageReference
) -> Optional[discord.Message]:
message = None
resolved = reference.resolved
if resolved and isinstance(resolved, discord.Message):
message = resolved
elif message := reference.cached_message:
pass
else:
try:
message = await channel.fetch_message(reference.message_id)
except discord.NotFound:
pass
return message
@commands.group()
async def cleanup(self, ctx: commands.Context):
"""Base command for deleting messages."""
pass
@cleanup.command()
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def text(
self, ctx: commands.Context, text: str, number: positive_int, delete_pinned: bool = False
):
"""Delete the last X messages matching the specified text in the current channel.
Example:
- `[p]cleanup text "test" 5`
Remember to use double quotes.
**Arguments:**
- `<number>` The max number of messages to cleanup. Must be a positive integer.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.author
if number > 100:
cont = await self.check_100_plus(ctx, number)
if not cont:
return
def check(m):
if text in m.content:
return True
else:
return False
to_delete = await self.get_messages_for_deletion(
channel=channel,
number=number,
check=check,
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages containing '{}' in channel #{}.".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_us"),
text,
channel.id,
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel, subtract_invoking=True)
@cleanup.command()
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def user(
self,
ctx: commands.Context,
user: Union[discord.Member, RawUserIdConverter],
number: positive_int,
delete_pinned: bool = False,
):
"""Delete the last X messages from a specified user in the current channel.
Examples:
- `[p]cleanup user @Twentysix 2`
- `[p]cleanup user Red 6`
**Arguments:**
- `<user>` The user whose messages are to be cleaned up.
- `<number>` The max number of messages to cleanup. Must be a positive integer.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
member = None
if isinstance(user, discord.Member):
member = user
_id = member.id
else:
_id = user
author = ctx.author
if number > 100:
cont = await self.check_100_plus(ctx, number)
if not cont:
return
def check(m):
if m.author.id == _id:
return True
else:
return False
to_delete = await self.get_messages_for_deletion(
channel=channel,
number=number,
check=check,
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = (
"{}({}) deleted {} messages"
" made by {}({}) in channel #{}."
"".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_US"),
member or "???",
_id,
channel.name,
)
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel, subtract_invoking=True)
@cleanup.command()
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def after(
self,
ctx: commands.Context,
message_id: Optional[RawMessageIds],
delete_pinned: bool = False,
):
"""Delete all messages after a specified message.
To get a message id, enable developer mode in Discord's
settings, 'appearance' tab. Then right click a message
and copy its id.
Replying to a message will cleanup all messages after it.
**Arguments:**
- `<message_id>` The id of the message to cleanup after. This message won't be deleted.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.author
after = None
if message_id:
try:
after = await channel.fetch_message(message_id)
except discord.NotFound:
return await ctx.send(_("Message not found."))
elif ref := ctx.message.reference:
after = await self.get_message_from_reference(channel, ref)
if after is None:
raise commands.BadArgument
to_delete = await self.get_messages_for_deletion(
channel=channel, number=None, after=after, delete_pinned=delete_pinned
)
reason = "{}({}) deleted {} messages in channel #{}.".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_US"),
channel.name,
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel)
@cleanup.command()
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def before(
self,
ctx: commands.Context,
message_id: Optional[RawMessageIds],
number: positive_int,
delete_pinned: bool = False,
):
"""Deletes X messages before the specified message.
To get a message id, enable developer mode in Discord's
settings, 'appearance' tab. Then right click a message
and copy its id.
Replying to a message will cleanup all messages before it.
**Arguments:**
- `<message_id>` The id of the message to cleanup before. This message won't be deleted.
- `<number>` The max number of messages to cleanup. Must be a positive integer.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.author
before = None
if message_id:
try:
before = await channel.fetch_message(message_id)
except discord.NotFound:
return await ctx.send(_("Message not found."))
elif ref := ctx.message.reference:
before = await self.get_message_from_reference(channel, ref)
if before is None:
raise commands.BadArgument
to_delete = await self.get_messages_for_deletion(
channel=channel, number=number, before=before, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel #{}.".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_US"),
channel.name,
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel, subtract_invoking=True)
@cleanup.command()
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def between(
self,
ctx: commands.Context,
one: RawMessageIds,
two: RawMessageIds,
delete_pinned: bool = False,
):
"""Delete the messages between Message One and Message Two, providing the messages IDs.
The first message ID should be the older message and the second one the newer.
Example:
- `[p]cleanup between 123456789123456789 987654321987654321`
**Arguments:**
- `<one>` The id of the message to cleanup after. This message won't be deleted.
- `<two>` The id of the message to cleanup before. This message won't be deleted.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.author
try:
mone = await channel.fetch_message(one)
except discord.errors.NotFound:
return await ctx.send(
_("Could not find a message with the ID of {id}.".format(id=one))
)
try:
mtwo = await channel.fetch_message(two)
except discord.errors.NotFound:
return await ctx.send(
_("Could not find a message with the ID of {id}.".format(id=two))
)
to_delete = await self.get_messages_for_deletion(
channel=channel, before=mtwo, after=mone, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel #{}.".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_US"),
channel.name,
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel, subtract_invoking=True)
@cleanup.command()
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def messages(
self, ctx: commands.Context, number: positive_int, delete_pinned: bool = False
):
"""Delete the last X messages in the current channel.
Example:
- `[p]cleanup messages 26`
**Arguments:**
- `<number>` The max number of messages to cleanup. Must be a positive integer.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.author
if number > 100:
cont = await self.check_100_plus(ctx, number)
if not cont:
return
to_delete = await self.get_messages_for_deletion(
channel=channel, number=number, before=ctx.message, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel #{}.".format(
author.name, author.id, len(to_delete), channel.name
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel, subtract_invoking=True)
@cleanup.command(name="bot")
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def cleanup_bot(
self, ctx: commands.Context, number: positive_int, delete_pinned: bool = False
):
"""Clean up command messages and messages from the bot in the current channel.
Can only cleanup custom commands and alias commands if those cogs are loaded.
**Arguments:**
- `<number>` The max number of messages to cleanup. Must be a positive integer.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.message.author
if number > 100:
cont = await self.check_100_plus(ctx, number)
if not cont:
return
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
if isinstance(prefixes, str):
prefixes = [prefixes]
# In case some idiot sets a null prefix
if "" in prefixes:
prefixes.remove("")
cc_cog = self.bot.get_cog("CustomCommands")
if cc_cog is not None:
command_names: Set[str] = await cc_cog.get_command_names(ctx.guild)
is_cc = lambda name: name in command_names
else:
is_cc = lambda name: False
alias_cog = self.bot.get_cog("Alias")
if alias_cog is not None:
alias_names: Set[str] = set(
a.name for a in await alias_cog._aliases.get_global_aliases()
) | set(a.name for a in await alias_cog._aliases.get_guild_aliases(ctx.guild))
is_alias = lambda name: name in alias_names
else:
is_alias = lambda name: False
bot_id = self.bot.user.id
def check(m):
if m.author.id == bot_id:
return True
elif m == ctx.message:
return True
p = discord.utils.find(m.content.startswith, prefixes)
if p and len(p) > 0:
cmd_name = m.content[len(p) :].split(" ")[0]
return (
bool(self.bot.get_command(cmd_name)) or is_alias(cmd_name) or is_cc(cmd_name)
)
return False
to_delete = await self.get_messages_for_deletion(
channel=channel,
number=number,
check=check,
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = (
"{}({}) deleted {}"
" command messages in channel #{}."
"".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_US"),
channel.name,
)
)
log.info(reason)
await mass_purge(to_delete, channel)
await self.send_optional_notification(len(to_delete), channel, subtract_invoking=True)
@cleanup.command(name="self")
@check_self_permissions()
async def cleanup_self(
self,
ctx: commands.Context,
number: positive_int,
match_pattern: str = None,
delete_pinned: bool = False,
):
"""Clean up messages owned by the bot in the current channel.
By default, all messages are cleaned. If a second argument is specified,
it is used for pattern matching - only messages containing the given text will be deleted.
Examples:
- `[p]cleanup self 6`
- `[p]cleanup self 10 Pong`
- `[p]cleanup self 7 "" True`
**Arguments:**
- `<number>` The max number of messages to cleanup. Must be a positive integer.
- `<match_pattern>` The text that messages must contain to be deleted. Use "" to skip this.
- `<delete_pinned>` Whether to delete pinned messages or not. Defaults to False
"""
channel = ctx.channel
author = ctx.message.author
if number > 100:
cont = await self.check_100_plus(ctx, number)
if not cont:
return
# You can always delete your own messages, this is needed to purge
can_mass_purge = False
if type(author) is discord.Member:
me = ctx.guild.me
can_mass_purge = channel.permissions_for(me).manage_messages
if match_pattern:
def content_match(c):
return match_pattern in c
else:
def content_match(_):
return True
def check(m):
if m.author.id != self.bot.user.id:
return False
elif content_match(m.content):
return True
return False
to_delete = await self.get_messages_for_deletion(
channel=channel,
number=number,
check=check,
before=ctx.message,
delete_pinned=delete_pinned,
)
if can_mass_purge:
to_delete.append(ctx.message)
if ctx.guild:
channel_name = "channel " + channel.name
else:
channel_name = str(channel)
reason = (
"{}({}) deleted {} messages "
"sent by the bot in {}."
"".format(
author.name,
author.id,
humanize_number(len(to_delete), override_locale="en_US"),
channel_name,
)
)
log.info(reason)
if can_mass_purge:
await mass_purge(to_delete, channel)
else:
await slow_deletion(to_delete)
await self.send_optional_notification(
len(to_delete), channel, subtract_invoking=can_mass_purge
)
@cleanup.command(name="duplicates", aliases=["spam"])
@commands.guild_only()
@checks.mod_or_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
async def cleanup_duplicates(
self, ctx: commands.Context, number: positive_int = PositiveInt(50)
):
"""Deletes duplicate messages in the channel from the last X messages and keeps only one copy.
Defaults to 50.
**Arguments:**
- `<number>` The number of messages to check for duplicates. Must be a positive integer.
"""
msgs = []
spam = []
def check(m):
if m.attachments:
return False
c = (
m.author.id,
m.content,
[embed.to_dict() for embed in m.embeds],
[sticker.id for sticker in m.stickers],
)
if c in msgs:
spam.append(m)
return True
else:
msgs.append(c)
return False
to_delete = await self.get_messages_for_deletion(
channel=ctx.channel, limit=number, check=check, before=ctx.message
)
if len(to_delete) > 100:
cont = await self.check_100_plus(ctx, len(to_delete))
if not cont:
return
log.info(
"%s (%s) deleted %s spam messages in channel %s (%s).",
ctx.author,
ctx.author.id,
len(to_delete),
ctx.channel,
ctx.channel.id,
)
to_delete.append(ctx.message)
await mass_purge(to_delete, ctx.channel)
await self.send_optional_notification(len(to_delete), ctx.channel, subtract_invoking=True)
@commands.group()
@commands.admin_or_permissions(manage_messages=True)
async def cleanupset(self, ctx: commands.Context):
"""Manage the settings for the cleanup command."""
pass
@commands.guild_only()
@cleanupset.command(name="notify")
async def cleanupset_notify(self, ctx: commands.Context):
"""Toggle clean up notification settings.
When enabled, a message will be sent per cleanup, showing how many messages were deleted.
This message will be deleted after 5 seconds.
"""
toggle = await self.config.guild(ctx.guild).notify()
if toggle:
await self.config.guild(ctx.guild).notify.set(False)
await ctx.send(_("I will no longer notify of message deletions."))
else:
await self.config.guild(ctx.guild).notify.set(True)
await ctx.send(_("I will now notify of message deletions."))