mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
API changes: - Cogs must now inherit from `commands.Cog` (see #2151 for discussion and more details) - All functions which are not decorators in the `redbot.core.checks` module are now deprecated in favour of their counterparts in `redbot.core.utils.mod`. This is to make this module more consistent and end the confusing naming convention. - `redbot.core.checks.check_overrides` function is now gone, overrideable checks can now be created with the `@commands.permissions_check` decorator - Command, Group, Cog and Context have some new attributes and methods, but they are for internal use so shouldn't concern cog creators (unless they're making a permissions cog!). - `__permissions_check_before` and `__permissions_check_after` have been replaced: A cog method named `__permissions_hook` will be evaluated as permissions hooks in the same way `__permissions_check_before` previously was. Permissions hooks can also be added/removed/verified through the new `*_permissions_hook()` methods on the bot object, and they will be verified even when permissions is unloaded. - New utility method `redbot.core.utils.chat_formatting.humanize_list` - New dependency [`schema`](https://github.com/keleshev/schema) User-facing changes: - When a `@bot_has_permissions` check fails, the bot will respond saying what permissions were actually missing. - All YAML-related `[p]permissions` subcommands now reside under the `[p]permissions acl` sub-group (tbh I still think the whole cog has too many top-level commands) - The YAML schema for these commands has been changed - A rule cannot be set as allow and deny at the same time (previously this would just default to allow) Documentation: - New documentation for `redbot.core.commands.requires` and `redbot.core.checks` modules - Renewed documentation for the permissions cog - `sphinx.ext.doctest` is now enabled Note: standard discord.py checks will still behave exactly the same way, in fact they are checked before `Requires` is looked at, so they are not overrideable. Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
416 lines
13 KiB
Python
416 lines
13 KiB
Python
import re
|
|
from datetime import datetime, timedelta
|
|
from typing import Union, List, Callable
|
|
|
|
import discord
|
|
|
|
from redbot.core import checks, commands
|
|
from redbot.core.bot import Red
|
|
from redbot.core.i18n import Translator, cog_i18n
|
|
from redbot.core.utils.mod import slow_deletion, mass_purge
|
|
from redbot.cogs.mod.log import log
|
|
|
|
_ = Translator("Cleanup", __file__)
|
|
|
|
|
|
@cog_i18n(_)
|
|
class Cleanup(commands.Cog):
|
|
"""Commands for cleaning messages"""
|
|
|
|
def __init__(self, bot: Red):
|
|
super().__init__()
|
|
self.bot = bot
|
|
|
|
@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.
|
|
"""
|
|
|
|
def author_check(message):
|
|
return message.author == ctx.author
|
|
|
|
prompt = await ctx.send(
|
|
_("Are you sure you want to delete {} messages? (y/n)").format(number)
|
|
)
|
|
response = await ctx.bot.wait_for("message", check=author_check)
|
|
|
|
if response.content.lower().startswith("y"):
|
|
await prompt.delete()
|
|
try:
|
|
await response.delete()
|
|
except:
|
|
pass
|
|
return True
|
|
else:
|
|
await ctx.send(_("Cancelled."))
|
|
return False
|
|
|
|
@staticmethod
|
|
async def get_messages_for_deletion(
|
|
*,
|
|
channel: discord.TextChannel,
|
|
number: int = None,
|
|
check: Callable[[discord.Message], bool] = lambda x: True,
|
|
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.utcnow() - 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=None, before=before, after=after, reverse=False
|
|
):
|
|
if message.created_at < two_weeks_ago:
|
|
break
|
|
if check(message):
|
|
collected.append(message)
|
|
if number and number <= len(collected):
|
|
break
|
|
|
|
return collected
|
|
|
|
@commands.group()
|
|
@checks.mod_or_permissions(manage_messages=True)
|
|
async def cleanup(self, ctx: commands.Context):
|
|
"""Deletes messages."""
|
|
pass
|
|
|
|
@cleanup.command()
|
|
@commands.guild_only()
|
|
async def text(
|
|
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
|
):
|
|
"""Deletes last X messages matching the specified text.
|
|
|
|
Example:
|
|
cleanup text \"test\" 5
|
|
|
|
Remember to use double quotes."""
|
|
|
|
channel = ctx.channel
|
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
await ctx.send("I need the Manage Messages permission to do this.")
|
|
return
|
|
|
|
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
|
|
elif m == ctx.message:
|
|
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,
|
|
)
|
|
|
|
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
|
author.name, author.id, len(to_delete), text, channel.id
|
|
)
|
|
log.info(reason)
|
|
|
|
await mass_purge(to_delete, channel)
|
|
|
|
@cleanup.command()
|
|
@commands.guild_only()
|
|
async def user(
|
|
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
|
):
|
|
"""Deletes last X messages from specified user.
|
|
|
|
Examples:
|
|
cleanup user @\u200bTwentysix 2
|
|
cleanup user Red 6"""
|
|
channel = ctx.channel
|
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
await ctx.send("I need the Manage Messages permission to do this.")
|
|
return
|
|
|
|
member = None
|
|
try:
|
|
member = await commands.converter.MemberConverter().convert(ctx, user)
|
|
except commands.BadArgument:
|
|
try:
|
|
_id = int(user)
|
|
except ValueError:
|
|
raise commands.BadArgument()
|
|
else:
|
|
_id = member.id
|
|
|
|
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
|
|
elif m == ctx.message:
|
|
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,
|
|
)
|
|
reason = (
|
|
"{}({}) deleted {} messages "
|
|
" made by {}({}) in channel {}."
|
|
"".format(author.name, author.id, len(to_delete), member or "???", _id, channel.name)
|
|
)
|
|
log.info(reason)
|
|
|
|
await mass_purge(to_delete, channel)
|
|
|
|
@cleanup.command()
|
|
@commands.guild_only()
|
|
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
|
"""Deletes all messages after 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.
|
|
|
|
This command only works on bots running as bot accounts.
|
|
"""
|
|
|
|
channel = ctx.channel
|
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
await ctx.send("I need the Manage Messages permission to do this.")
|
|
return
|
|
author = ctx.author
|
|
|
|
try:
|
|
after = await channel.get_message(message_id)
|
|
except discord.NotFound:
|
|
return await ctx.send(_("Message not found."))
|
|
|
|
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, len(to_delete), channel.name
|
|
)
|
|
log.info(reason)
|
|
|
|
await mass_purge(to_delete, channel)
|
|
|
|
@cleanup.command()
|
|
@commands.guild_only()
|
|
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
|
"""Deletes last X messages.
|
|
|
|
Example:
|
|
cleanup messages 26"""
|
|
|
|
channel = ctx.channel
|
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
await ctx.send("I need the Manage Messages permission to do this.")
|
|
return
|
|
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, number, channel.name
|
|
)
|
|
log.info(reason)
|
|
|
|
await mass_purge(to_delete, channel)
|
|
|
|
@cleanup.command(name="bot")
|
|
@commands.guild_only()
|
|
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
|
"""Cleans up command messages and messages from the bot."""
|
|
|
|
channel = ctx.channel
|
|
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
|
await ctx.send("I need the Manage Messages permission to do this.")
|
|
return
|
|
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("")
|
|
|
|
def check(m):
|
|
if m.author.id == self.bot.user.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))
|
|
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, len(to_delete), channel.name)
|
|
)
|
|
log.info(reason)
|
|
|
|
await mass_purge(to_delete, channel)
|
|
|
|
@cleanup.command(name="self")
|
|
async def cleanup_self(
|
|
self,
|
|
ctx: commands.Context,
|
|
number: int,
|
|
match_pattern: str = None,
|
|
delete_pinned: bool = False,
|
|
):
|
|
"""Cleans up messages owned by the bot.
|
|
|
|
By default, all messages are cleaned. If a third argument is specified,
|
|
it is used for pattern matching: If it begins with r( and ends with ),
|
|
then it is interpreted as a regex, and messages that match it are
|
|
deleted. Otherwise, it is used in a simple substring test.
|
|
|
|
Some helpful regex flags to include in your pattern:
|
|
Dots match newlines: (?s); Ignore case: (?i); Both: (?si)
|
|
"""
|
|
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
|
|
|
|
use_re = match_pattern and match_pattern.startswith("r(") and match_pattern.endswith(")")
|
|
|
|
if use_re:
|
|
match_pattern = match_pattern[1:] # strip 'r'
|
|
match_re = re.compile(match_pattern)
|
|
|
|
def content_match(c):
|
|
return bool(match_re.match(c))
|
|
|
|
elif 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 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, len(to_delete), channel_name)
|
|
)
|
|
log.info(reason)
|
|
|
|
if can_mass_purge:
|
|
await mass_purge(to_delete, channel)
|
|
else:
|
|
await slow_deletion(to_delete)
|