diff --git a/cogs/mod.py b/cogs/mod.py index 8c453234f..67d60c8d4 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -6,6 +6,7 @@ from __main__ import send_cmd_help, settings from collections import deque, defaultdict from cogs.utils.chat_formatting import escape_mass_mentions, box import os +import re import logging import asyncio @@ -398,14 +399,10 @@ class Mod: dataIO.save_json("data/mod/perms_cache.json", self._perms_cache) await self.bot.say("User has been unmuted in this server.") - @commands.group(pass_context=True, no_pm=True) + @commands.group(pass_context=True) @checks.mod_or_permissions(manage_messages=True) async def cleanup(self, ctx): - """Deletes messages. - - cleanup messages [number] - cleanup user [name/mention] [number] - cleanup text \"Text here\" [number]""" + """Deletes messages.""" if ctx.invoked_subcommand is None: await send_cmd_help(ctx) @@ -492,7 +489,7 @@ class Mod: while tries_left and len(to_delete) - 1 < number: async for message in self.bot.logs_from(channel, limit=100, before=tmp): - if len(to_delete) -1 < number and check(message): + if len(to_delete) - 1 < number and check(message): to_delete.append(message) tmp = message tries_left -= 1 @@ -514,6 +511,8 @@ class Mod: 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.message.channel @@ -522,6 +521,11 @@ class Mod: is_bot = self.bot.user.bot has_permissions = channel.permissions_for(server.me).manage_messages + if not is_bot: + await self.bot.say("This command can only be used on bots with " + "bot accounts.") + return + to_delete = [] after = await self.bot.get_message(channel, message_id) @@ -541,10 +545,7 @@ class Mod: "".format(author.name, author.id, len(to_delete), channel.name)) - if is_bot: - await self.mass_purge(to_delete) - else: - await self.slow_deletion(to_delete) + await self.mass_purge(to_delete) @cleanup.command(pass_context=True, no_pm=True) async def messages(self, ctx, number: int): @@ -577,6 +578,139 @@ class Mod: else: await self.slow_deletion(to_delete) + @cleanup.command(pass_context=True, no_pm=True, name='bot') + async def cleanup_bot(self, ctx, number: int): + """Cleans up command messages and messages from the bot""" + + channel = ctx.message.channel + author = ctx.message.author + server = channel.server + is_bot = self.bot.user.bot + has_permissions = channel.permissions_for(server.me).manage_messages + + prefixes = self.bot.command_prefix + if isinstance(prefixes, str): + prefixes = [prefixes] + elif callable(prefixes): + if asyncio.iscoroutine(prefixes): + await self.bot.say('Coroutine prefixes not yet implemented.') + return + prefixes = prefixes(self.bot, ctx.message) + + # In case some idiot sets a null prefix + if '' in prefixes: + prefixes.pop('') + + 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: + return m.content[len(p):].startswith(tuple(self.bot.commands)) + return False + + to_delete = [ctx.message] + + if not has_permissions: + await self.bot.say("I'm not allowed to delete messages.") + return + + tries_left = 5 + tmp = ctx.message + + while tries_left and len(to_delete) - 1 < number: + async for message in self.bot.logs_from(channel, limit=100, + before=tmp): + if len(to_delete) - 1 < number and check(message): + to_delete.append(message) + tmp = message + tries_left -= 1 + + logger.info("{}({}) deleted {} " + " command messages in channel {}" + "".format(author.name, author.id, len(to_delete), + channel.name)) + + if is_bot: + await self.mass_purge(to_delete) + else: + await self.slow_deletion(to_delete) + + @cleanup.command(pass_context=True, name='self') + async def cleanup_self(self, ctx, number: int, match_pattern: str = None): + """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.message.channel + author = ctx.message.author + is_bot = self.bot.user.bot + + # You can always delete your own messages, this is needed to purge + can_mass_purge = False + if type(author) is discord.Member: + me = channel.server.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 = [] + + tries_left = 5 + tmp = ctx.message + + while tries_left and len(to_delete) < number: + async for message in self.bot.logs_from(channel, limit=100, + before=tmp): + if len(to_delete) < number and check(message): + to_delete.append(message) + tmp = message + tries_left -= 1 + + if channel.name: + channel_name = 'channel ' + channel.name + else: + channel_name = str(channel) + + logger.info("{}({}) deleted {} messages " + "sent by the bot in {}" + "".format(author.name, author.id, len(to_delete), + channel_name)) + + if is_bot and can_mass_purge: + await self.mass_purge(to_delete) + else: + await self.slow_deletion(to_delete) + @commands.command(pass_context=True) @checks.mod_or_permissions(manage_messages=True) async def reason(self, ctx, case, *, reason : str=""): @@ -613,7 +747,6 @@ class Mod: else: await self.bot.say("Case #{} updated.".format(case)) - @commands.group(pass_context=True) @checks.is_owner() async def blacklist(self, ctx): @@ -938,7 +1071,6 @@ class Mod: await self.bot.delete_message(message) except: pass - await asyncio.sleep(1.5) def is_mod_or_superior(self, message): user = message.author @@ -1057,7 +1189,7 @@ class Mod: self.cache[author].append(message) msgs = self.cache[author] if len(msgs) == 3 and \ - msgs[0].content == msgs[1].content == msgs[2].content: + msgs[0].content == msgs[1].content == msgs[2].content: if any([m.attachments for m in msgs]): return False try: @@ -1146,6 +1278,7 @@ class Mod: empty = [p for p in iter(discord.PermissionOverwrite())] return original == empty + def check_folders(): folders = ("data", "data/mod/") for folder in folders: @@ -1174,6 +1307,7 @@ def check_files(): print("Creating empty {}".format(filename)) dataIO.save_json("data/mod/{}".format(filename), value) + def setup(bot): global logger check_folders()