mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[V3] Mod initial rewrite (#1034)
* Readd work due to redoing branch * [modlog] Move to core and start work on separating it from cogs * More work on modlog separation * [Core] Finish logic for modlog, do docstrings, async getters * [Core] Add stuff to dunder all * [Docs] Add mod log docs * [Core] Move away from dunder str for Case class * [Docs] don't need to doc special members in modlog docs * More on mod log to implement commands * More work on Mod * [Mod] compatibility with async getters * [Tests] start tests for mod * [Tests] attempted fix * [Tests] mod tests passing now! * [ModLog] update for i18n * modlog.pot -> messages.pot * [Mod] i18n * fix getting admin/mod roles * Fix doc building * [Mod/Modlog] redo imports * [Tests] fix imports in mod tests * [Mod] fix logger problem * [Mod] cleanup errors * A couple of bug fixes Async getters, some old `config.set` syntax * Filter ignores private channels * Fix softban Was still relying on default channels * Actually ignore private channels * Add check for ignored channels * Fix logic for ignore check * Send confirm messages before making case * Pass in guild when setting modlog * Thanks autocomplete * Maintain all data for case * Properly ignore softbans in events * [Mod] bugfixes * [Mod] more changes * [ModLog] timestamp change * [Mod] split filter and cleanup to their own cogs + regen messages.pot * [Cleanup] change logic * [Cleanup] increase limit for channel.history * [Mod] await getter in modset banmentionspam * [Mod] attempt duplicate modlog message fix * [Mod] get_user -> get_user_info * [Modlog] change reason command so the case author can edit their cases (#806) * [Modlog] make reason command guild only * [Modlog] clarify the reason command's help * [Mod] package path changes + numpy style docstrings for modlog * [Mod] change ban and unban events to need view audit log perms to find/create a case * [Modlog] refactoring * [Filter] add autoban feature * [Mod] update case types + event changes * [Mod/Modlog] fix tests, fix permissions things * [Docs] fix up modlog docs * Regenerate messages.pot
This commit is contained in:
parent
fe61ef167e
commit
fb125ef619
95
docs/framework_modlog.rst
Normal file
95
docs/framework_modlog.rst
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
.. V3 Mod log
|
||||||
|
|
||||||
|
.. role:: python(code)
|
||||||
|
:language: python
|
||||||
|
|
||||||
|
|
||||||
|
=======
|
||||||
|
Mod log
|
||||||
|
=======
|
||||||
|
|
||||||
|
Mod log has now been separated from Mod for V3.
|
||||||
|
|
||||||
|
***********
|
||||||
|
Basic Usage
|
||||||
|
***********
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from redbot.core import modlog
|
||||||
|
import discord
|
||||||
|
|
||||||
|
class MyCog:
|
||||||
|
@commands.command()
|
||||||
|
@checks.admin_or_permissions(ban_members=True)
|
||||||
|
async def ban(self, ctx, user: discord.Member, reason: str=None):
|
||||||
|
await ctx.guild.ban(user)
|
||||||
|
case = modlog.create_case(
|
||||||
|
ctx.guild, ctx.message.created_at, "ban", user,
|
||||||
|
ctx.author, reason, until=None, channel=None
|
||||||
|
)
|
||||||
|
await ctx.send("Done. It was about time.")
|
||||||
|
|
||||||
|
|
||||||
|
**********************
|
||||||
|
Registering Case types
|
||||||
|
**********************
|
||||||
|
|
||||||
|
To register a single case type:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from redbot.core import modlog
|
||||||
|
import discord
|
||||||
|
|
||||||
|
class MyCog:
|
||||||
|
def __init__(self, bot):
|
||||||
|
ban_case = {
|
||||||
|
"name": "ban",
|
||||||
|
"default_setting": True,
|
||||||
|
"image": ":hammer:",
|
||||||
|
"case_str": "Ban",
|
||||||
|
"audit_type": "ban"
|
||||||
|
}
|
||||||
|
modlog.register_casetype(**ban_case)
|
||||||
|
|
||||||
|
To register multiple case types:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from redbot.core import modlog
|
||||||
|
import discord
|
||||||
|
|
||||||
|
class MyCog:
|
||||||
|
def __init__(self, bot):
|
||||||
|
new_types = [
|
||||||
|
{
|
||||||
|
"name": "ban",
|
||||||
|
"default_setting": True,
|
||||||
|
"image": ":hammer:",
|
||||||
|
"case_str": "Ban",
|
||||||
|
"audit_type": "ban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kick",
|
||||||
|
"default_setting": True,
|
||||||
|
"image": ":boot:",
|
||||||
|
"case_str": "Kick",
|
||||||
|
"audit_type": "kick"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
modlog.register_casetypes(new_types)
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
Image should be the emoji you want to represent your case type with.
|
||||||
|
|
||||||
|
|
||||||
|
*************
|
||||||
|
API Reference
|
||||||
|
*************
|
||||||
|
|
||||||
|
Mod log
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.modlog
|
||||||
|
:members:
|
||||||
@ -31,9 +31,11 @@ Welcome to Red - Discord Bot's documentation!
|
|||||||
framework_cogmanager
|
framework_cogmanager
|
||||||
framework_config
|
framework_config
|
||||||
framework_downloader
|
framework_downloader
|
||||||
|
framework_modlog
|
||||||
framework_context
|
framework_context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|||||||
6
redbot/cogs/cleanup/__init__.py
Normal file
6
redbot/cogs/cleanup/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .cleanup import Cleanup
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: Red):
|
||||||
|
bot.add_cog(Cleanup(bot))
|
||||||
344
redbot/cogs/cleanup/cleanup.py
Normal file
344
redbot/cogs/cleanup/cleanup.py
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from redbot.core import checks
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core.i18n import CogI18n
|
||||||
|
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||||
|
from redbot.cogs.mod.log import log
|
||||||
|
|
||||||
|
_ = CogI18n("Cleanup", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
class Cleanup:
|
||||||
|
"""Commands for cleaning messages"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
|
async def cleanup(self, ctx: commands.Context):
|
||||||
|
"""Deletes messages."""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await self.bot.send_cmd_help(ctx)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def text(self, ctx: commands.Context, text: str, number: int):
|
||||||
|
"""Deletes last X messages matching the specified text.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
cleanup text \"test\" 5
|
||||||
|
|
||||||
|
Remember to use double quotes."""
|
||||||
|
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
|
def check(m):
|
||||||
|
if text in m.content:
|
||||||
|
return True
|
||||||
|
elif m == ctx.message:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
to_delete = [ctx.message]
|
||||||
|
too_old = False
|
||||||
|
tmp = ctx.message
|
||||||
|
|
||||||
|
while not too_old and len(to_delete) - 1 < number:
|
||||||
|
async for message in channel.history(limit=1000,
|
||||||
|
before=tmp):
|
||||||
|
if len(to_delete) - 1 < number and check(message) and\
|
||||||
|
(ctx.message.created_at - message.created_at).days < 14:
|
||||||
|
to_delete.append(message)
|
||||||
|
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||||
|
too_old = True
|
||||||
|
break
|
||||||
|
tmp = message
|
||||||
|
|
||||||
|
reason = "{}({}) deleted {} messages "\
|
||||||
|
" containing '{}' in channel {}".format(author.name,
|
||||||
|
author.id, len(to_delete), text, channel.id)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
if is_bot:
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
else:
|
||||||
|
await slow_deletion(to_delete)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def user(self, ctx: commands.Context, user: discord.Member or int, number: int):
|
||||||
|
"""Deletes last X messages from specified user.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
cleanup user @\u200bTwentysix 2
|
||||||
|
cleanup user Red 6"""
|
||||||
|
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
|
def check(m):
|
||||||
|
if isinstance(user, discord.Member) and m.author == user:
|
||||||
|
return True
|
||||||
|
elif m.author.id == user: # Allow finding messages based on an ID
|
||||||
|
return True
|
||||||
|
elif m == ctx.message:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
to_delete = []
|
||||||
|
too_old = False
|
||||||
|
tmp = ctx.message
|
||||||
|
|
||||||
|
while not too_old and len(to_delete) - 1 < number:
|
||||||
|
async for message in channel.history(limit=1000,
|
||||||
|
before=tmp):
|
||||||
|
if len(to_delete) - 1 < number and check(message) and\
|
||||||
|
(ctx.message.created_at - message.created_at).days < 14:
|
||||||
|
to_delete.append(message)
|
||||||
|
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||||
|
too_old = True
|
||||||
|
break
|
||||||
|
tmp = message
|
||||||
|
reason = "{}({}) deleted {} messages "\
|
||||||
|
" made by {}({}) in channel {}"\
|
||||||
|
"".format(author.name, author.id, len(to_delete),
|
||||||
|
user.name, user.id, channel.name)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
if is_bot:
|
||||||
|
# For whatever reason the purge endpoint requires manage_messages
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
else:
|
||||||
|
await slow_deletion(to_delete)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def after(self, ctx: commands.Context, message_id: int):
|
||||||
|
"""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
|
||||||
|
author = ctx.author
|
||||||
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
|
if not is_bot:
|
||||||
|
await ctx.send(_("This command can only be used on bots with "
|
||||||
|
"bot accounts."))
|
||||||
|
return
|
||||||
|
|
||||||
|
after = await channel.get_message(message_id)
|
||||||
|
|
||||||
|
if not after:
|
||||||
|
await ctx.send(_("Message not found."))
|
||||||
|
return
|
||||||
|
|
||||||
|
to_delete = []
|
||||||
|
|
||||||
|
async for message in channel.history(after=after):
|
||||||
|
if (ctx.message.created_at - message.created_at).days < 14:
|
||||||
|
# Only add messages that are less than
|
||||||
|
# 14 days old to the deletion queue
|
||||||
|
to_delete.append(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)
|
||||||
|
|
||||||
|
@cleanup.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def messages(self, ctx: commands.Context, number: int):
|
||||||
|
"""Deletes last X messages.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
cleanup messages 26"""
|
||||||
|
|
||||||
|
channel = ctx.channel
|
||||||
|
author = ctx.author
|
||||||
|
|
||||||
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
|
to_delete = []
|
||||||
|
tmp = ctx.message
|
||||||
|
|
||||||
|
done = False
|
||||||
|
|
||||||
|
while len(to_delete) - 1 < number and not done:
|
||||||
|
async for message in channel.history(limit=1000, before=tmp):
|
||||||
|
if len(to_delete) - 1 < number and \
|
||||||
|
(ctx.message.created_at - message.created_at).days < 14:
|
||||||
|
to_delete.append(message)
|
||||||
|
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||||
|
done = True
|
||||||
|
break
|
||||||
|
tmp = message
|
||||||
|
|
||||||
|
reason = "{}({}) deleted {} messages in channel {}"\
|
||||||
|
"".format(author.name, author.id,
|
||||||
|
number, channel.name)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
if is_bot:
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
else:
|
||||||
|
await slow_deletion(to_delete)
|
||||||
|
|
||||||
|
@cleanup.command(name='bot')
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(manage_messages=True)
|
||||||
|
async def cleanup_bot(self, ctx: commands.Context, number: int):
|
||||||
|
"""Cleans up command messages and messages from the bot"""
|
||||||
|
|
||||||
|
channel = ctx.message.channel
|
||||||
|
author = ctx.message.author
|
||||||
|
is_bot = self.bot.user.bot
|
||||||
|
|
||||||
|
prefixes = self.bot.command_prefix
|
||||||
|
if isinstance(prefixes, str):
|
||||||
|
prefixes = [prefixes]
|
||||||
|
elif callable(prefixes):
|
||||||
|
if asyncio.iscoroutine(prefixes):
|
||||||
|
await ctx.send(_('Coroutine prefixes not yet implemented.'))
|
||||||
|
return
|
||||||
|
prefixes = prefixes(self.bot, ctx.message)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
return m.content[len(p):].startswith(tuple(self.bot.commands))
|
||||||
|
return False
|
||||||
|
|
||||||
|
to_delete = [ctx.message]
|
||||||
|
too_old = False
|
||||||
|
tmp = ctx.message
|
||||||
|
|
||||||
|
while not too_old and len(to_delete) - 1 < number:
|
||||||
|
async for message in channel.history(limit=1000, before=tmp):
|
||||||
|
if len(to_delete) - 1 < number and check(message) and\
|
||||||
|
(ctx.message.created_at - message.created_at).days < 14:
|
||||||
|
to_delete.append(message)
|
||||||
|
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||||
|
too_old = True
|
||||||
|
break
|
||||||
|
tmp = message
|
||||||
|
|
||||||
|
reason = "{}({}) deleted {} "\
|
||||||
|
" command messages in channel {}"\
|
||||||
|
"".format(author.name, author.id, len(to_delete),
|
||||||
|
channel.name)
|
||||||
|
log.info(reason)
|
||||||
|
|
||||||
|
if is_bot:
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
else:
|
||||||
|
await slow_deletion(to_delete)
|
||||||
|
|
||||||
|
@cleanup.command(name='self')
|
||||||
|
async def cleanup_self(self, ctx: commands.Context, 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.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 = 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 = []
|
||||||
|
# Selfbot convenience, delete trigger message
|
||||||
|
if author == self.bot.user:
|
||||||
|
to_delete.append(ctx.message)
|
||||||
|
number += 1
|
||||||
|
too_old = False
|
||||||
|
tmp = ctx.message
|
||||||
|
while not too_old and len(to_delete) < number:
|
||||||
|
async for message in channel.history(limit=1000, before=tmp):
|
||||||
|
if len(to_delete) < number and check(message) and\
|
||||||
|
(ctx.message.created_at - message.created_at).days < 14:
|
||||||
|
to_delete.append(message)
|
||||||
|
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||||
|
# Found a message that is 14 or more days old, stop here
|
||||||
|
too_old = True
|
||||||
|
break
|
||||||
|
tmp = message
|
||||||
|
|
||||||
|
if channel.name:
|
||||||
|
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 is_bot and can_mass_purge:
|
||||||
|
await mass_purge(to_delete, channel)
|
||||||
|
else:
|
||||||
|
await slow_deletion(to_delete)
|
||||||
17
redbot/cogs/cleanup/locales/messages.pot
Normal file
17
redbot/cogs/cleanup/locales/messages.pot
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"POT-Creation-Date: 2017-10-22 16:34-0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=cp1252\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
|
||||||
|
|
||||||
6
redbot/cogs/filter/__init__.py
Normal file
6
redbot/cogs/filter/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .filter import Filter
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: Red):
|
||||||
|
bot.add_cog(Filter(bot))
|
||||||
236
redbot/cogs/filter/filter.py
Normal file
236
redbot/cogs/filter/filter.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from redbot.core import checks, Config, modlog
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core.i18n import CogI18n
|
||||||
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
|
from redbot.core.utils.mod import is_mod_or_superior
|
||||||
|
|
||||||
|
_ = CogI18n("Filter", __file__)
|
||||||
|
|
||||||
|
|
||||||
|
class Filter:
|
||||||
|
"""Filter-related commands"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
|
self.bot = bot
|
||||||
|
self.settings = Config.get_conf(self, 4766951341)
|
||||||
|
default_guild_settings = {
|
||||||
|
"filter": [],
|
||||||
|
"filterban_count": 0,
|
||||||
|
"filterban_time": 0
|
||||||
|
}
|
||||||
|
default_member_settings = {
|
||||||
|
"filter_count": 0,
|
||||||
|
"next_reset_time": 0
|
||||||
|
}
|
||||||
|
self.settings.register_guild(**default_guild_settings)
|
||||||
|
self.settings.register_member(**default_member_settings)
|
||||||
|
self.bot.loop.create_task(
|
||||||
|
modlog.register_casetype(
|
||||||
|
"filterban", False, ":filing_cabinet: :hammer:",
|
||||||
|
"Filter ban", "ban"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.group(name="filter")
|
||||||
|
@commands.guild_only()
|
||||||
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
|
async def _filter(self, ctx: commands.Context):
|
||||||
|
"""Adds/removes words from filter
|
||||||
|
|
||||||
|
Use double quotes to add/remove sentences
|
||||||
|
Using this command with no subcommands will send
|
||||||
|
the list of the server's filtered words."""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await self.bot.send_cmd_help(ctx)
|
||||||
|
server = ctx.guild
|
||||||
|
author = ctx.author
|
||||||
|
word_list = await self.settings.guild(server).filter()
|
||||||
|
if word_list:
|
||||||
|
words = ", ".join(word_list)
|
||||||
|
words = _("Filtered in this server:") + "\n\n" + words
|
||||||
|
try:
|
||||||
|
for page in pagify(words, delims=[" ", "\n"], shorten_by=8):
|
||||||
|
await author.send(page)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(_("I can't send direct messages to you."))
|
||||||
|
|
||||||
|
@_filter.command(name="add")
|
||||||
|
async def filter_add(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Adds words to the filter
|
||||||
|
|
||||||
|
Use double quotes to add sentences
|
||||||
|
Examples:
|
||||||
|
filter add word1 word2 word3
|
||||||
|
filter add \"This is a sentence\""""
|
||||||
|
server = ctx.guild
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith("\"")\
|
||||||
|
and not word.endswith("\"") and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith("\""):
|
||||||
|
tmp += word[1:]
|
||||||
|
elif word.endswith("\""):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word
|
||||||
|
added = await self.add_to_filter(server, word_list)
|
||||||
|
if added:
|
||||||
|
await ctx.send(_("Words added to filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Words already in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="remove")
|
||||||
|
async def filter_remove(self, ctx: commands.Context, *, words: str):
|
||||||
|
"""Remove words from the filter
|
||||||
|
|
||||||
|
Use double quotes to remove sentences
|
||||||
|
Examples:
|
||||||
|
filter remove word1 word2 word3
|
||||||
|
filter remove \"This is a sentence\""""
|
||||||
|
server = ctx.guild
|
||||||
|
split_words = words.split()
|
||||||
|
word_list = []
|
||||||
|
tmp = ""
|
||||||
|
for word in split_words:
|
||||||
|
if not word.startswith("\"")\
|
||||||
|
and not word.endswith("\"") and not tmp:
|
||||||
|
word_list.append(word)
|
||||||
|
else:
|
||||||
|
if word.startswith("\""):
|
||||||
|
tmp += word[1:]
|
||||||
|
elif word.endswith("\""):
|
||||||
|
tmp += word[:-1]
|
||||||
|
word_list.append(tmp)
|
||||||
|
tmp = ""
|
||||||
|
else:
|
||||||
|
tmp += word
|
||||||
|
removed = await self.remove_from_filter(server, word_list)
|
||||||
|
if removed:
|
||||||
|
await ctx.send(_("Words removed from filter."))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Those words weren't in the filter."))
|
||||||
|
|
||||||
|
@_filter.command(name="ban")
|
||||||
|
async def filter_ban(
|
||||||
|
self, ctx: commands.Context, count: int, timeframe: int):
|
||||||
|
"""
|
||||||
|
Sets up an autoban if the specified number of messages are
|
||||||
|
filtered in the specified amount of time (in seconds)
|
||||||
|
"""
|
||||||
|
if (count <= 0) != (timeframe <= 0):
|
||||||
|
await ctx.send(
|
||||||
|
_("Count and timeframe either both need to be 0 "
|
||||||
|
"or both need to be greater than 0!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif count == 0 and timeframe == 0:
|
||||||
|
await self.settings.guild(ctx.guild).filterban_count.set(0)
|
||||||
|
await self.settings.guild(ctx.guild).filterban_time.set(0)
|
||||||
|
await ctx.send(_("Autoban disabled."))
|
||||||
|
else:
|
||||||
|
await self.settings.guild(ctx.guild).filterban_count.set(count)
|
||||||
|
await self.settings.guild(ctx.guild).filterban_time.set(timeframe)
|
||||||
|
await ctx.send(_("Count and time have been set."))
|
||||||
|
|
||||||
|
async def add_to_filter(self, server: discord.Guild, words: list) -> bool:
|
||||||
|
added = 0
|
||||||
|
cur_list = await self.settings.guild(server).filter()
|
||||||
|
for w in words:
|
||||||
|
if w.lower() not in cur_list and w != "":
|
||||||
|
cur_list.append(w.lower())
|
||||||
|
added += 1
|
||||||
|
if added:
|
||||||
|
await self.settings.guild(server).filter.set(cur_list)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def remove_from_filter(self, server: discord.Guild, words: list) -> bool:
|
||||||
|
removed = 0
|
||||||
|
cur_list = await self.settings.guild(server).filter()
|
||||||
|
for w in words:
|
||||||
|
if w.lower() in cur_list:
|
||||||
|
cur_list.remove(w.lower())
|
||||||
|
removed += 1
|
||||||
|
if removed:
|
||||||
|
await self.settings.guild(server).filter.set(cur_list)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_filter(self, message: discord.Message):
|
||||||
|
server = message.guild
|
||||||
|
author = message.author
|
||||||
|
word_list = await self.settings.guild(server).filter()
|
||||||
|
filter_count = await self.settings.guild(server).filterban_count()
|
||||||
|
filter_time = await self.settings.guild(server).filterban_time()
|
||||||
|
user_count = await self.settings.member(author).filter_count()
|
||||||
|
next_reset_time = await self.settings.member(author).next_reset_time()
|
||||||
|
if filter_count > 0 and filter_time > 0:
|
||||||
|
if message.created_at.timestamp() >= next_reset_time:
|
||||||
|
next_reset_time = message.created_at.timestamp() + filter_time
|
||||||
|
await self.settings.member(author).next_reset_time.set(
|
||||||
|
next_reset_time
|
||||||
|
)
|
||||||
|
if user_count > 0:
|
||||||
|
user_count = 0
|
||||||
|
await self.settings.member(author).filter_count.set(user_count)
|
||||||
|
|
||||||
|
if word_list:
|
||||||
|
for w in word_list:
|
||||||
|
if w in message.content.lower():
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if filter_count > 0 and filter_time > 0:
|
||||||
|
user_count += 1
|
||||||
|
await self.settings.member(author).filter_count.set(user_count)
|
||||||
|
if user_count >= filter_count and \
|
||||||
|
message.created_at.timestamp() < next_reset_time:
|
||||||
|
reason = "Autoban (too many filtered messages)"
|
||||||
|
try:
|
||||||
|
await server.ban(author, reason=reason)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
await modlog.create_case(
|
||||||
|
server, message.created_at, "filterban",
|
||||||
|
author, server.me, reason
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_message(self, message: discord.Message):
|
||||||
|
if isinstance(message.channel, discord.abc.PrivateChannel):
|
||||||
|
return
|
||||||
|
author = message.author
|
||||||
|
valid_user = isinstance(author, discord.Member) and not author.bot
|
||||||
|
|
||||||
|
# Bots and mods or superior are ignored from the filter
|
||||||
|
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
||||||
|
if not valid_user or mod_or_superior:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.check_filter(message)
|
||||||
|
|
||||||
|
async def on_message_edit(self, _, message):
|
||||||
|
author = message.author
|
||||||
|
if message.guild is None or self.bot.user == author:
|
||||||
|
return
|
||||||
|
|
||||||
|
valid_user = isinstance(author, discord.Member) and not author.bot
|
||||||
|
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
||||||
|
if not valid_user or mod_or_superior:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.check_filter(message)
|
||||||
17
redbot/cogs/filter/locales/messages.pot
Normal file
17
redbot/cogs/filter/locales/messages.pot
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"POT-Creation-Date: 2017-10-22 16:33-0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=cp1252\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
|
||||||
|
|
||||||
6
redbot/cogs/mod/__init__.py
Normal file
6
redbot/cogs/mod/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from redbot.core.bot import Red
|
||||||
|
from .mod import Mod
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: Red):
|
||||||
|
bot.add_cog(Mod(bot))
|
||||||
58
redbot/cogs/mod/checks.py
Normal file
58
redbot/cogs/mod/checks.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from discord.ext import commands
|
||||||
|
import discord
|
||||||
|
|
||||||
|
|
||||||
|
def mod_or_voice_permissions(**perms):
|
||||||
|
async def pred(ctx: commands.Context):
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
if await ctx.bot.is_owner(author) or guild.owner == author:
|
||||||
|
# Author is bot owner or guild owner
|
||||||
|
return True
|
||||||
|
|
||||||
|
admin_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).admin_role())
|
||||||
|
mod_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).mod_role())
|
||||||
|
|
||||||
|
if admin_role in author.roles or mod_role in author.roles:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for vc in guild.voice_channels:
|
||||||
|
resolved = vc.permissions_for(author)
|
||||||
|
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||||
|
if not good:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
|
def admin_or_voice_permissions(**perms):
|
||||||
|
async def pred(ctx: commands.Context):
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
if await ctx.bot.is_owner(author) or guild.owner == author:
|
||||||
|
return True
|
||||||
|
admin_role = discord.utils.get(guild.roles, id=await ctx.bot.db.guild(guild).admin_role())
|
||||||
|
if admin_role in author.roles:
|
||||||
|
return True
|
||||||
|
for vc in guild.voice_channels:
|
||||||
|
resolved = vc.permissions_for(author)
|
||||||
|
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||||
|
if not good:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return commands.check(pred)
|
||||||
|
|
||||||
|
|
||||||
|
def bot_has_voice_permissions(**perms):
|
||||||
|
async def pred(ctx: commands.Context):
|
||||||
|
guild = ctx.guild
|
||||||
|
for vc in guild.voice_channels:
|
||||||
|
resolved = vc.permissions_for(guild.me)
|
||||||
|
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||||
|
if not good:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return commands.check(pred)
|
||||||
17
redbot/cogs/mod/locales/messages.pot
Normal file
17
redbot/cogs/mod/locales/messages.pot
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"POT-Creation-Date: 2017-10-22 16:33-0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=cp1252\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
|
||||||
|
|
||||||
4
redbot/cogs/mod/log.py
Normal file
4
redbot/cogs/mod/log.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger("red.mod")
|
||||||
1299
redbot/cogs/mod/mod.py
Normal file
1299
redbot/cogs/mod/mod.py
Normal file
File diff suppressed because it is too large
Load Diff
6
redbot/cogs/modlog/__init__.py
Normal file
6
redbot/cogs/modlog/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from redbot.core.bot import Red
|
||||||
|
from .modlog import ModLog
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: Red):
|
||||||
|
bot.add_cog(ModLog(bot))
|
||||||
17
redbot/cogs/modlog/locales/messages.pot
Normal file
17
redbot/cogs/modlog/locales/messages.pot
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"POT-Creation-Date: 2017-10-22 16:34-0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=cp1252\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
|
||||||
|
|
||||||
154
redbot/cogs/modlog/modlog.py
Normal file
154
redbot/cogs/modlog/modlog.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from redbot.core import checks, modlog
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core.i18n import CogI18n
|
||||||
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
|
||||||
|
_ = CogI18n('ModLog', __file__)
|
||||||
|
|
||||||
|
|
||||||
|
class ModLog:
|
||||||
|
"""Log for mod actions"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.group()
|
||||||
|
@checks.guildowner_or_permissions(administrator=True)
|
||||||
|
async def modlogset(self, ctx: commands.Context):
|
||||||
|
"""Settings for the mod log"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await self.bot.send_cmd_help(ctx)
|
||||||
|
|
||||||
|
@modlogset.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||||
|
"""Sets a channel as mod log
|
||||||
|
|
||||||
|
Leaving the channel parameter empty will deactivate it"""
|
||||||
|
guild = ctx.guild
|
||||||
|
if channel:
|
||||||
|
if channel.permissions_for(guild.me).send_messages:
|
||||||
|
await modlog.set_modlog_channel(guild, channel)
|
||||||
|
await ctx.send(
|
||||||
|
_("Mod events will be sent to {}").format(
|
||||||
|
channel.mention
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
_("I do not have permissions to "
|
||||||
|
"send messages in {}!").format(channel.mention)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await modlog.get_modlog_channel(guild)
|
||||||
|
except RuntimeError:
|
||||||
|
await self.bot.send_cmd_help(ctx)
|
||||||
|
else:
|
||||||
|
await modlog.set_modlog_channel(guild, None)
|
||||||
|
await ctx.send(_("Mod log deactivated."))
|
||||||
|
|
||||||
|
@modlogset.command(name='cases')
|
||||||
|
@commands.guild_only()
|
||||||
|
async def set_cases(self, ctx: commands.Context, action: str = None):
|
||||||
|
"""Enables or disables case creation for each type of mod action"""
|
||||||
|
guild = ctx.guild
|
||||||
|
|
||||||
|
if action is None: # No args given
|
||||||
|
casetypes = await modlog.get_all_casetypes()
|
||||||
|
await self.bot.send_cmd_help(ctx)
|
||||||
|
title = _("Current settings:")
|
||||||
|
msg = ""
|
||||||
|
for ct in casetypes:
|
||||||
|
enabled = await ct.is_enabled()
|
||||||
|
value = 'enabled' if enabled else 'disabled'
|
||||||
|
msg += '%s : %s\n' % (ct.name, value)
|
||||||
|
|
||||||
|
msg = title + "\n" + box(msg)
|
||||||
|
await ctx.send(msg)
|
||||||
|
return
|
||||||
|
casetype = await modlog.get_casetype(action, guild)
|
||||||
|
if not casetype:
|
||||||
|
await ctx.send(_("That action is not registered"))
|
||||||
|
else:
|
||||||
|
|
||||||
|
enabled = await casetype.is_enabled()
|
||||||
|
await casetype.set_enabled(True if not enabled else False)
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
_('Case creation for {} actions is now {}.').format(
|
||||||
|
action, 'enabled' if not enabled else 'disabled'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ctx.send(msg)
|
||||||
|
|
||||||
|
@modlogset.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def resetcases(self, ctx: commands.Context):
|
||||||
|
"""Resets modlog's cases"""
|
||||||
|
guild = ctx.guild
|
||||||
|
await modlog.reset_cases(guild)
|
||||||
|
await ctx.send(_("Cases have been reset."))
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def case(self, ctx: commands.Context, number: int):
|
||||||
|
"""Shows the specified case"""
|
||||||
|
try:
|
||||||
|
case = await modlog.get_case(number, ctx.guild, self.bot)
|
||||||
|
except RuntimeError:
|
||||||
|
await ctx.send(_("That case does not exist for that guild"))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
await ctx.send(embed=await case.get_case_msg_content())
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
async def reason(self, ctx: commands.Context, case: int, *, reason: str = ""):
|
||||||
|
"""Lets you specify a reason for mod-log's cases
|
||||||
|
Please note that you can only edit cases you are
|
||||||
|
the owner of unless you are a mod/admin or the guild owner"""
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
if not reason:
|
||||||
|
await self.bot.send_cmd_help(ctx)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
case_before = await modlog.get_case(case, guild, self.bot)
|
||||||
|
except RuntimeError:
|
||||||
|
await ctx.send(_("That case does not exist!"))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if case_before.moderator is None:
|
||||||
|
# No mod set, so attempt to find out if the author
|
||||||
|
# triggered the case creation with an action
|
||||||
|
bot_perms = guild.me.guild_permissions
|
||||||
|
if bot_perms.view_audit_log:
|
||||||
|
case_type = await modlog.get_casetype(case_before.action_type, guild)
|
||||||
|
audit_type = getattr(discord.AuditLogAction, case_type.audit_type)
|
||||||
|
if audit_type:
|
||||||
|
audit_case = None
|
||||||
|
async for entry in guild.audit_logs(action=audit_type):
|
||||||
|
if entry.target.id == case_before.user.id and \
|
||||||
|
entry.user.id == case_before.moderator.id:
|
||||||
|
audit_case = entry
|
||||||
|
break
|
||||||
|
if audit_case:
|
||||||
|
case_before.moderator = audit_case.user
|
||||||
|
is_guild_owner = author == guild.owner
|
||||||
|
is_case_author = author == case_before.moderator
|
||||||
|
author_is_mod = await ctx.bot.is_mod(author)
|
||||||
|
if not (is_guild_owner or is_case_author or author_is_mod):
|
||||||
|
await ctx.send(_("You are not authorized to modify that case!"))
|
||||||
|
return
|
||||||
|
to_modify = {
|
||||||
|
"reason": reason,
|
||||||
|
}
|
||||||
|
if case_before.moderator != author:
|
||||||
|
to_modify["amended_by"] = author
|
||||||
|
to_modify["modified_at"] = ctx.message.created_at.timestamp()
|
||||||
|
await case_before.edit(to_modify)
|
||||||
|
await ctx.send(_("Reason has been updated."))
|
||||||
@ -91,6 +91,29 @@ class RedBase(BotBase):
|
|||||||
return True
|
return True
|
||||||
return await super().is_owner(user)
|
return await super().is_owner(user)
|
||||||
|
|
||||||
|
async def is_admin(self, member: discord.Member):
|
||||||
|
"""Checks if a member is an admin of their guild."""
|
||||||
|
admin_role = await self.db.guild(member.guild).admin_role()
|
||||||
|
return (not admin_role or
|
||||||
|
any(role.id == admin_role for role in member.roles))
|
||||||
|
|
||||||
|
async def is_mod(self, member: discord.Member):
|
||||||
|
"""Checks if a member is a mod or admin of their guild."""
|
||||||
|
mod_role = await self.db.guild(member.guild).mod_role()
|
||||||
|
admin_role = await self.db.guild(member.guild).admin_role()
|
||||||
|
return (not (admin_role or mod_role) or
|
||||||
|
any(role.id in (mod_role, admin_role) for role in member.roles))
|
||||||
|
|
||||||
|
async def send_cmd_help(self, ctx):
|
||||||
|
if ctx.invoked_subcommand:
|
||||||
|
pages = await self.formatter.format_help_for(ctx, ctx.invoked_subcommand)
|
||||||
|
for page in pages:
|
||||||
|
await ctx.send(page)
|
||||||
|
else:
|
||||||
|
pages = await self.formatter.format_help_for(ctx, ctx.command)
|
||||||
|
for page in pages:
|
||||||
|
await ctx.send(page)
|
||||||
|
|
||||||
async def get_context(self, message, *, cls=RedContext):
|
async def get_context(self, message, *, cls=RedContext):
|
||||||
return await super().get_context(message, cls=cls)
|
return await super().get_context(message, cls=cls)
|
||||||
|
|
||||||
|
|||||||
707
redbot/core/modlog.py
Normal file
707
redbot/core/modlog.py
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
import discord
|
||||||
|
import os
|
||||||
|
|
||||||
|
from redbot.core import Config
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core.utils.chat_formatting import bold
|
||||||
|
from typing import List, Union
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Case", "CaseType", "get_next_case_number", "get_case", "get_all_cases",
|
||||||
|
"create_case", "get_casetype", "get_all_casetypes", "register_casetype",
|
||||||
|
"register_casetypes", "get_modlog_channel", "set_modlog_channel",
|
||||||
|
"reset_cases"
|
||||||
|
]
|
||||||
|
|
||||||
|
_DEFAULT_GLOBAL = {
|
||||||
|
"casetypes": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
_DEFAULT_GUILD = {
|
||||||
|
"mod_log": None,
|
||||||
|
"cases": {},
|
||||||
|
"casetypes": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
_modlog_type = type("ModLog", (object,), {})
|
||||||
|
|
||||||
|
|
||||||
|
def _register_defaults():
|
||||||
|
_conf.register_global(**_DEFAULT_GLOBAL)
|
||||||
|
_conf.register_guild(**_DEFAULT_GUILD)
|
||||||
|
|
||||||
|
|
||||||
|
if not os.environ.get('BUILDING_DOCS'):
|
||||||
|
_conf = Config.get_conf(_modlog_type(), 1354799444)
|
||||||
|
_register_defaults()
|
||||||
|
|
||||||
|
|
||||||
|
class Case:
|
||||||
|
"""A single mod log case"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, guild: discord.Guild, created_at: int, action_type: str,
|
||||||
|
user: discord.User, moderator: discord.Member, case_number: int,
|
||||||
|
reason: str=None, until: int=None,
|
||||||
|
channel: discord.TextChannel=None, amended_by: discord.Member=None,
|
||||||
|
modified_at: int=None, message: discord.Message=None):
|
||||||
|
self.guild = guild
|
||||||
|
self.created_at = created_at
|
||||||
|
self.action_type = action_type
|
||||||
|
self.user = user
|
||||||
|
self.moderator = moderator
|
||||||
|
self.reason = reason
|
||||||
|
self.until = until
|
||||||
|
self.channel = channel
|
||||||
|
self.amended_by = amended_by
|
||||||
|
self.modified_at = modified_at
|
||||||
|
self.case_number = case_number
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
async def edit(self, data: dict):
|
||||||
|
"""
|
||||||
|
Edits a case
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: dict
|
||||||
|
The attributes to change
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
for item in list(data.keys()):
|
||||||
|
setattr(self, item, data[item])
|
||||||
|
case_emb = await self.message_content()
|
||||||
|
await self.message.edit(embed=case_emb)
|
||||||
|
|
||||||
|
await _conf.guild(self.guild).cases.set_attr(
|
||||||
|
str(self.case_number), self.to_json()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def message_content(self):
|
||||||
|
"""
|
||||||
|
Format a case message
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
discord.Embed
|
||||||
|
A rich embed representing a case message
|
||||||
|
|
||||||
|
"""
|
||||||
|
casetype = await get_casetype(self.action_type)
|
||||||
|
title = "{}".format(bold("Case #{} | {} {}".format(
|
||||||
|
self.case_number, casetype.case_str, casetype.image)))
|
||||||
|
|
||||||
|
if self.reason:
|
||||||
|
reason = "**Reason:** {}".format(self.reason)
|
||||||
|
else:
|
||||||
|
reason = \
|
||||||
|
"**Reason:** Type [p]reason {} <reason> to add it".format(
|
||||||
|
self.case_number
|
||||||
|
)
|
||||||
|
|
||||||
|
emb = discord.Embed(title=title, description=reason)
|
||||||
|
|
||||||
|
moderator = "{}#{} ({})\n".format(
|
||||||
|
self.moderator.name,
|
||||||
|
self.moderator.discriminator,
|
||||||
|
self.moderator.id
|
||||||
|
)
|
||||||
|
emb.set_author(name=moderator, icon_url=self.moderator.avatar_url)
|
||||||
|
user = "{}#{} ({})\n".format(
|
||||||
|
self.user.name, self.user.discriminator, self.user.id)
|
||||||
|
emb.add_field(name="User", value=user)
|
||||||
|
if self.until:
|
||||||
|
start = datetime.fromtimestamp(self.created_at)
|
||||||
|
end = datetime.fromtimestamp(self.until)
|
||||||
|
end_fmt = end.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
duration = end - start
|
||||||
|
dur_fmt = _strfdelta(duration)
|
||||||
|
until = end_fmt
|
||||||
|
duration = dur_fmt
|
||||||
|
emb.add_field(name="Until", value=until)
|
||||||
|
emb.add_field(name="Duration", value=duration)
|
||||||
|
|
||||||
|
if self.channel:
|
||||||
|
emb.add_field(name="Channel", value=self.channel.name)
|
||||||
|
if self.amended_by:
|
||||||
|
amended_by = "{}#{} ({})".format(
|
||||||
|
self.amended_by.name,
|
||||||
|
self.amended_by.discriminator,
|
||||||
|
self.amended_by.id
|
||||||
|
)
|
||||||
|
emb.add_field(name="Amended by", value=amended_by)
|
||||||
|
if self.modified_at:
|
||||||
|
last_modified = "{}".format(
|
||||||
|
datetime.fromtimestamp(
|
||||||
|
self.modified_at
|
||||||
|
).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
)
|
||||||
|
emb.add_field(name="Last modified at", value=last_modified)
|
||||||
|
emb.timestamp = datetime.fromtimestamp(self.created_at)
|
||||||
|
return emb
|
||||||
|
|
||||||
|
def to_json(self) -> dict:
|
||||||
|
"""Transform the object to a dict
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The case in the form of a dict
|
||||||
|
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"case_number": self.case_number,
|
||||||
|
"action_type": self.action_type,
|
||||||
|
"guild": self.guild.id,
|
||||||
|
"created_at": self.created_at,
|
||||||
|
"user": self.user.id,
|
||||||
|
"moderator": self.moderator.id,
|
||||||
|
"reason": self.reason,
|
||||||
|
"until": self.until,
|
||||||
|
"channel": self.channel.id if hasattr(self.channel, "id") else None,
|
||||||
|
"amended_by": self.amended_by.id if hasattr(self.amended_by, "id") else None,
|
||||||
|
"modified_at": self.modified_at,
|
||||||
|
"message": self.message.id if hasattr(self.message, "id") else None
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_json(cls, mod_channel: discord.TextChannel, bot: Red, data: dict):
|
||||||
|
"""Get a Case object from the provided information
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mod_channel: discord.TextChannel
|
||||||
|
The mod log channel for the guild
|
||||||
|
bot: Red
|
||||||
|
The bot's instance. Needed to get the target user
|
||||||
|
data: dict
|
||||||
|
The JSON representation of the case to be gotten
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Case
|
||||||
|
The case object for the requested case
|
||||||
|
|
||||||
|
"""
|
||||||
|
guild = mod_channel.guild
|
||||||
|
message = await mod_channel.get_message(data["message"])
|
||||||
|
user = await bot.get_user_info(data["user"])
|
||||||
|
moderator = guild.get_member(data["moderator"])
|
||||||
|
channel = guild.get_channel(data["channel"])
|
||||||
|
amended_by = guild.get_member(data["amended_by"])
|
||||||
|
case_guild = bot.get_guild(data["guild"])
|
||||||
|
return cls(
|
||||||
|
guild=case_guild, created_at=data["created_at"],
|
||||||
|
action_type=data["action_type"], user=user, moderator=moderator,
|
||||||
|
case_number=data["case_number"], reason=data["reason"],
|
||||||
|
until=data["until"], channel=channel, amended_by=amended_by,
|
||||||
|
modified_at=data["modified_at"], message=message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CaseType:
|
||||||
|
"""
|
||||||
|
A single case type
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of the case
|
||||||
|
default_setting: bool
|
||||||
|
Whether the case type should be on (if `True`)
|
||||||
|
or off (if `False`) by default
|
||||||
|
image: str
|
||||||
|
The emoji to use for the case type (for example, :boot:)
|
||||||
|
case_str: str
|
||||||
|
The string representation of the case (example: Ban)
|
||||||
|
audit_type: str
|
||||||
|
The action type of the action as it would appear in the
|
||||||
|
audit log
|
||||||
|
"""
|
||||||
|
def __init__(
|
||||||
|
self, name: str, default_setting: bool, image: str,
|
||||||
|
case_str: str, audit_type: str, guild: discord.Guild = None):
|
||||||
|
self.name = name
|
||||||
|
self.default_setting = default_setting
|
||||||
|
self.image = image
|
||||||
|
self.case_str = case_str
|
||||||
|
self.audit_type = audit_type
|
||||||
|
self.guild = guild
|
||||||
|
|
||||||
|
async def to_json(self):
|
||||||
|
"""Transforms the case type into a dict and saves it"""
|
||||||
|
data = {
|
||||||
|
"default_setting": self.default_setting,
|
||||||
|
"image": self.image,
|
||||||
|
"case_str": self.case_str,
|
||||||
|
"audit_type": self.audit_type
|
||||||
|
}
|
||||||
|
await _conf.casetypes.set_attr(self.name, data)
|
||||||
|
|
||||||
|
async def is_enabled(self) -> bool:
|
||||||
|
"""
|
||||||
|
Determines if the case is enabled.
|
||||||
|
If the guild is not set, this will always return False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool:
|
||||||
|
True if the guild is set and the casetype is enabled for the guild
|
||||||
|
|
||||||
|
False if the guild is not set or if the guild is set and the type
|
||||||
|
is disabled
|
||||||
|
"""
|
||||||
|
if not self.guild:
|
||||||
|
return False
|
||||||
|
return await _conf.guild(self.guild).casetypes.get_attr(self.name,
|
||||||
|
self.default_setting)
|
||||||
|
|
||||||
|
async def set_enabled(self, enabled: bool):
|
||||||
|
"""
|
||||||
|
Sets the case as enabled or disabled
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
enabled: bool
|
||||||
|
True if the case should be enabled, otherwise False"""
|
||||||
|
if not self.guild:
|
||||||
|
return
|
||||||
|
await _conf.guild(self.guild).casetypes.set_attr(self.name, enabled)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, data: dict):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: dict
|
||||||
|
The data to create an instance from
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
CaseType
|
||||||
|
|
||||||
|
"""
|
||||||
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_next_case_number(guild: discord.Guild) -> str:
|
||||||
|
"""
|
||||||
|
Gets the next case number
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild: `discord.Guild`
|
||||||
|
The guild to get the next case number for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The next case number
|
||||||
|
|
||||||
|
"""
|
||||||
|
cases = sorted(
|
||||||
|
(await _conf.guild(guild).get_attr("cases")),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
return str(int(cases[0]) + 1) if cases else "1"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_case(case_number: int, guild: discord.Guild,
|
||||||
|
bot: Red) -> Case:
|
||||||
|
"""
|
||||||
|
Gets the case with the associated case number
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
case_number: int
|
||||||
|
The case number for the case to get
|
||||||
|
guild: discord.Guild
|
||||||
|
The guild to get the case from
|
||||||
|
bot: Red
|
||||||
|
The bot's instance
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Case
|
||||||
|
The case associated with the case number
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If there is no case for the specified number
|
||||||
|
|
||||||
|
"""
|
||||||
|
case = await _conf.guild(guild).cases.get_attr(str(case_number))
|
||||||
|
if case is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"That case does not exist for guild {}".format(guild.name)
|
||||||
|
)
|
||||||
|
mod_channel = await get_modlog_channel(guild)
|
||||||
|
return await Case.from_json(mod_channel, bot, case)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_cases(guild: discord.Guild, bot: Red) -> List[Case]:
|
||||||
|
"""
|
||||||
|
Gets all cases for the specified guild
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild: `discord.Guild`
|
||||||
|
The guild to get the cases from
|
||||||
|
bot: Red
|
||||||
|
The bot's instance
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list
|
||||||
|
A list of all cases for the guild
|
||||||
|
|
||||||
|
"""
|
||||||
|
cases = await _conf.guild(guild).get_attr("cases")
|
||||||
|
case_numbers = list(cases.keys())
|
||||||
|
case_list = []
|
||||||
|
for case in case_numbers:
|
||||||
|
case_list.append(await get_case(case, guild, bot))
|
||||||
|
return case_list
|
||||||
|
|
||||||
|
|
||||||
|
async def create_case(guild: discord.Guild, created_at: datetime, action_type: str,
|
||||||
|
user: Union[discord.User, discord.Member],
|
||||||
|
moderator: discord.Member, reason: str=None,
|
||||||
|
until: datetime=None, channel: discord.TextChannel=None
|
||||||
|
) -> Union[Case, None]:
|
||||||
|
"""
|
||||||
|
Creates a new case
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild: `discord.Guild`
|
||||||
|
The guild the action was taken in
|
||||||
|
created_at: datetime
|
||||||
|
The time the action occurred at
|
||||||
|
action_type: str
|
||||||
|
The type of action that was taken
|
||||||
|
user: `discord.User` or `discord.Member`
|
||||||
|
The user target by the action
|
||||||
|
moderator: `discord.Member`
|
||||||
|
The moderator who took the action
|
||||||
|
reason: str
|
||||||
|
The reason the action was taken
|
||||||
|
until: datetime
|
||||||
|
The time the action is in effect until
|
||||||
|
channel: `discord.TextChannel` or `discord.VoiceChannel`
|
||||||
|
The channel the action was taken in
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Case
|
||||||
|
The newly created case
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If the mod log channel doesn't exist
|
||||||
|
|
||||||
|
"""
|
||||||
|
mod_channel = None
|
||||||
|
if hasattr(guild, "owner"):
|
||||||
|
# Fairly arbitrary, but it doesn't really matter
|
||||||
|
# since we don't need the modlog channel in tests
|
||||||
|
try:
|
||||||
|
mod_channel = await get_modlog_channel(guild)
|
||||||
|
except RuntimeError:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No mod log channel set for guild {}".format(guild.name)
|
||||||
|
)
|
||||||
|
case_type = await get_casetype(action_type, guild)
|
||||||
|
if case_type is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not await case_type.is_enabled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
next_case_number = int(await get_next_case_number(guild))
|
||||||
|
|
||||||
|
case = Case(guild, int(created_at.timestamp()), action_type, user, moderator,
|
||||||
|
next_case_number, reason, until, channel, amended_by=None,
|
||||||
|
modified_at=None, message=None)
|
||||||
|
if hasattr(mod_channel, "send"): # Not going to be the case for tests
|
||||||
|
case_emb = await case.message_content()
|
||||||
|
msg = await mod_channel.send(embed=case_emb)
|
||||||
|
case.message = msg
|
||||||
|
await _conf.guild(guild).cases.set_attr(str(next_case_number), case.to_json())
|
||||||
|
return case
|
||||||
|
|
||||||
|
|
||||||
|
async def get_casetype(name: str, guild: discord.Guild=None) -> Union[CaseType, None]:
|
||||||
|
"""
|
||||||
|
Gets the case type
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of the case type to get
|
||||||
|
guild: discord.Guild
|
||||||
|
If provided, sets the case type's guild attribute to this guild
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
CaseType or None
|
||||||
|
"""
|
||||||
|
casetypes = await _conf.get_attr("casetypes")
|
||||||
|
if name in casetypes:
|
||||||
|
data = casetypes[name]
|
||||||
|
data["name"] = name
|
||||||
|
casetype = CaseType.from_json(data)
|
||||||
|
casetype.guild = guild
|
||||||
|
return casetype
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_casetypes(guild: discord.Guild=None) -> List[CaseType]:
|
||||||
|
"""
|
||||||
|
Get all currently registered case types
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list
|
||||||
|
A list of case types
|
||||||
|
|
||||||
|
"""
|
||||||
|
casetypes = await _conf.get_attr("casetypes")
|
||||||
|
typelist = []
|
||||||
|
for ct in casetypes.keys():
|
||||||
|
data = casetypes[ct]
|
||||||
|
data["name"] = ct
|
||||||
|
casetype = CaseType.from_json(data)
|
||||||
|
casetype.guild = guild
|
||||||
|
typelist.append(casetype)
|
||||||
|
return typelist
|
||||||
|
|
||||||
|
|
||||||
|
async def register_casetype(
|
||||||
|
name: str, default_setting: bool,
|
||||||
|
image: str, case_str: str, audit_type: str) -> CaseType:
|
||||||
|
"""
|
||||||
|
Registers a case type. If the case type exists and
|
||||||
|
there are differences between the values passed and
|
||||||
|
what is stored already, the case type will be updated
|
||||||
|
with the new values
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of the case
|
||||||
|
default_setting: bool
|
||||||
|
Whether the case type should be on (if `True`)
|
||||||
|
or off (if `False`) by default
|
||||||
|
image: str
|
||||||
|
The emoji to use for the case type (for example, :boot:)
|
||||||
|
case_str: str
|
||||||
|
The string representation of the case (example: Ban)
|
||||||
|
audit_type: str
|
||||||
|
The action type of the action as it would appear in the
|
||||||
|
audit log
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
CaseType
|
||||||
|
The case type that was registered
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If the case type is already registered
|
||||||
|
TypeError:
|
||||||
|
If a parameter is missing
|
||||||
|
ValueError
|
||||||
|
If a parameter's value is not valid
|
||||||
|
AttributeError
|
||||||
|
If the audit_type is not an attribute of `discord.AuditLogAction`
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("The 'name' is not a string! Check the value!")
|
||||||
|
if not isinstance(default_setting, bool):
|
||||||
|
raise ValueError("'default_setting' needs to be a bool!")
|
||||||
|
if not isinstance(image, str):
|
||||||
|
raise ValueError("The 'image' is not a string!")
|
||||||
|
if not isinstance(case_str, str):
|
||||||
|
raise ValueError("The 'case_str' is not a string!")
|
||||||
|
if not isinstance(audit_type, str):
|
||||||
|
raise ValueError("The 'audit_type' is not a string!")
|
||||||
|
try:
|
||||||
|
getattr(discord.AuditLogAction, audit_type)
|
||||||
|
except AttributeError:
|
||||||
|
raise
|
||||||
|
ct = await get_casetype(name)
|
||||||
|
if ct is None:
|
||||||
|
casetype = CaseType(name, default_setting, image, case_str, audit_type)
|
||||||
|
await casetype.to_json()
|
||||||
|
return casetype
|
||||||
|
else:
|
||||||
|
# Case type exists, so check for differences
|
||||||
|
# If no differences, raise RuntimeError
|
||||||
|
changed = False
|
||||||
|
if ct.default_setting != default_setting:
|
||||||
|
ct.default_setting = default_setting
|
||||||
|
changed = True
|
||||||
|
if ct.image != image:
|
||||||
|
ct.image = image
|
||||||
|
changed = True
|
||||||
|
if ct.case_str != case_str:
|
||||||
|
ct.case_str = case_str
|
||||||
|
changed = True
|
||||||
|
if ct.audit_type != audit_type:
|
||||||
|
ct.audit_type = audit_type
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
await ct.to_json()
|
||||||
|
return ct
|
||||||
|
else:
|
||||||
|
raise RuntimeError("That case type is already registered!")
|
||||||
|
|
||||||
|
|
||||||
|
async def register_casetypes(new_types: List[dict]) -> List[CaseType]:
|
||||||
|
"""
|
||||||
|
Registers multiple case types
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
new_types: list
|
||||||
|
The new types to register
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
`True` if all were registered successfully
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
KeyError
|
||||||
|
ValueError
|
||||||
|
AttributeError
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
redbot.core.modlog.register_casetype
|
||||||
|
|
||||||
|
"""
|
||||||
|
type_list = []
|
||||||
|
for new_type in new_types:
|
||||||
|
try:
|
||||||
|
ct = await register_casetype(**new_type)
|
||||||
|
except RuntimeError:
|
||||||
|
raise
|
||||||
|
except ValueError:
|
||||||
|
raise
|
||||||
|
except AttributeError:
|
||||||
|
raise
|
||||||
|
except TypeError:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
type_list.append(ct)
|
||||||
|
else:
|
||||||
|
return type_list
|
||||||
|
|
||||||
|
|
||||||
|
async def get_modlog_channel(guild: discord.Guild
|
||||||
|
) -> Union[discord.TextChannel, None]:
|
||||||
|
"""
|
||||||
|
Get the current modlog channel
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild: `discord.Guild`
|
||||||
|
The guild to get the modlog channel for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`discord.TextChannel` or `None`
|
||||||
|
The channel object representing the modlog channel
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If the modlog channel is not found
|
||||||
|
|
||||||
|
"""
|
||||||
|
if hasattr(guild, "get_channel"):
|
||||||
|
channel = guild.get_channel(await _conf.guild(guild).mod_log())
|
||||||
|
else:
|
||||||
|
channel = await _conf.guild(guild).mod_log()
|
||||||
|
if channel is None:
|
||||||
|
raise RuntimeError("Failed to get the mod log channel!")
|
||||||
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
async def set_modlog_channel(guild: discord.Guild,
|
||||||
|
channel: Union[discord.TextChannel, None]) -> bool:
|
||||||
|
"""
|
||||||
|
Changes the modlog channel
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild: `discord.Guild`
|
||||||
|
The guild to set a mod log channel for
|
||||||
|
channel: `discord.TextChannel` or `None`
|
||||||
|
The channel to be set as modlog channel
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
`True` if successful
|
||||||
|
|
||||||
|
"""
|
||||||
|
await _conf.guild(guild).mod_log.set(
|
||||||
|
channel.id if hasattr(channel, "id") else None
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def reset_cases(guild: discord.Guild) -> bool:
|
||||||
|
"""
|
||||||
|
Wipes all modlog cases for the specified guild
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
guild: `discord.Guild`
|
||||||
|
The guild to reset cases for
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
`True` if successful
|
||||||
|
|
||||||
|
"""
|
||||||
|
await _conf.guild(guild).cases.set({})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _strfdelta(delta):
|
||||||
|
s = []
|
||||||
|
if delta.days:
|
||||||
|
ds = '%i day' % delta.days
|
||||||
|
if delta.days > 1:
|
||||||
|
ds += 's'
|
||||||
|
s.append(ds)
|
||||||
|
hrs, rem = divmod(delta.seconds, 60*60)
|
||||||
|
if hrs:
|
||||||
|
hs = '%i hr' % hrs
|
||||||
|
if hrs > 1:
|
||||||
|
hs += 's'
|
||||||
|
s.append(hs)
|
||||||
|
mins, secs = divmod(rem, 60)
|
||||||
|
if mins:
|
||||||
|
s.append('%i min' % mins)
|
||||||
|
if secs:
|
||||||
|
s.append('%i sec' % secs)
|
||||||
|
return ' '.join(s)
|
||||||
124
redbot/core/utils/mod.py
Normal file
124
redbot/core/utils/mod.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from redbot.core import Config
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
|
async def mass_purge(messages: List[discord.Message],
|
||||||
|
channel: discord.TextChannel):
|
||||||
|
while messages:
|
||||||
|
if len(messages) > 1:
|
||||||
|
await channel.delete_messages(messages[:100])
|
||||||
|
messages = messages[100:]
|
||||||
|
else:
|
||||||
|
await messages[0].delete()
|
||||||
|
messages = []
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
|
||||||
|
async def slow_deletion(messages: List[discord.Message]):
|
||||||
|
for message in messages:
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_audit_reason(author: discord.Member, reason: str = None):
|
||||||
|
"""Helper function to construct a reason to be provided
|
||||||
|
as the reason to appear in the audit log."""
|
||||||
|
return \
|
||||||
|
"Action requested by {} (ID {}). Reason: {}".format(author, author.id, reason) if reason else \
|
||||||
|
"Action requested by {} (ID {}).".format(author, author.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def is_allowed_by_hierarchy(
|
||||||
|
bot: Red, settings: Config, server: discord.Guild,
|
||||||
|
mod: discord.Member, user: discord.Member):
|
||||||
|
if not await settings.guild(server).respect_hierarchy():
|
||||||
|
return True
|
||||||
|
is_special = mod == server.owner or await bot.is_owner(mod)
|
||||||
|
return mod.top_role.position > user.top_role.position or is_special
|
||||||
|
|
||||||
|
|
||||||
|
async def is_mod_or_superior(bot: Red, obj: discord.Message or discord.Member or discord.Role):
|
||||||
|
user = None
|
||||||
|
if isinstance(obj, discord.Message):
|
||||||
|
user = obj.author
|
||||||
|
elif isinstance(obj, discord.Member):
|
||||||
|
user = obj
|
||||||
|
elif isinstance(obj, discord.Role):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise TypeError('Only messages, members or roles may be passed')
|
||||||
|
|
||||||
|
server = obj.guild
|
||||||
|
admin_role_id = await bot.db.guild(server).admin_role()
|
||||||
|
mod_role_id = await bot.db.guild(server).mod_role()
|
||||||
|
|
||||||
|
if isinstance(obj, discord.Role):
|
||||||
|
return obj.id in [admin_role_id, mod_role_id]
|
||||||
|
mod_roles = [r for r in server.roles if r.id == mod_role_id]
|
||||||
|
mod_role = mod_roles[0] if len(mod_roles) > 0 else None
|
||||||
|
admin_roles = [r for r in server.roles if r.id == admin_role_id]
|
||||||
|
admin_role = admin_roles[0] if len(admin_roles) > 0 else None
|
||||||
|
|
||||||
|
if user and user == await bot.is_owner(user):
|
||||||
|
return True
|
||||||
|
elif admin_role and discord.utils.get(user.roles, name=admin_role):
|
||||||
|
return True
|
||||||
|
elif mod_role and discord.utils.get(user.roles, name=mod_role):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def strfdelta(delta):
|
||||||
|
s = []
|
||||||
|
if delta.days:
|
||||||
|
ds = '%i day' % delta.days
|
||||||
|
if delta.days > 1:
|
||||||
|
ds += 's'
|
||||||
|
s.append(ds)
|
||||||
|
hrs, rem = divmod(delta.seconds, 60*60)
|
||||||
|
if hrs:
|
||||||
|
hs = '%i hr' % hrs
|
||||||
|
if hrs > 1:
|
||||||
|
hs += 's'
|
||||||
|
s.append(hs)
|
||||||
|
mins, secs = divmod(rem, 60)
|
||||||
|
if mins:
|
||||||
|
s.append('%i min' % mins)
|
||||||
|
if secs:
|
||||||
|
s.append('%i sec' % secs)
|
||||||
|
return ' '.join(s)
|
||||||
|
|
||||||
|
|
||||||
|
async def is_admin_or_superior(bot: Red, obj: discord.Message or discord.Role or discord.Member):
|
||||||
|
user = None
|
||||||
|
if isinstance(obj, discord.Message):
|
||||||
|
user = obj.author
|
||||||
|
elif isinstance(obj, discord.Member):
|
||||||
|
user = obj
|
||||||
|
elif isinstance(obj, discord.Role):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise TypeError('Only messages, members or roles may be passed')
|
||||||
|
|
||||||
|
server = obj.guild
|
||||||
|
admin_role_id = await bot.db.guild(server).admin_role()
|
||||||
|
|
||||||
|
if isinstance(obj, discord.Role):
|
||||||
|
return obj.id == admin_role_id
|
||||||
|
admin_roles = [r for r in server.roles if r.id == admin_role_id]
|
||||||
|
admin_role = admin_roles[0] if len(admin_roles) > 0 else None
|
||||||
|
|
||||||
|
if user and await bot.is_owner(user):
|
||||||
|
return True
|
||||||
|
elif admin_roles and discord.utils.get(user.roles, name=admin_role):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
52
tests/cogs/test_mod.py
Normal file
52
tests/cogs/test_mod.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mod(config):
|
||||||
|
from redbot.core import Config
|
||||||
|
|
||||||
|
Config.get_conf = lambda *args, **kwargs: config
|
||||||
|
|
||||||
|
from redbot.core import modlog
|
||||||
|
|
||||||
|
modlog._register_defaults()
|
||||||
|
return modlog
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_modlog_register_casetype(mod, ctx):
|
||||||
|
ct = {
|
||||||
|
"name": "ban",
|
||||||
|
"default_setting": True,
|
||||||
|
"image": ":hammer:",
|
||||||
|
"case_str": "Ban",
|
||||||
|
"audit_type": "ban"
|
||||||
|
}
|
||||||
|
casetype = await mod.register_casetype(**ct)
|
||||||
|
assert casetype is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_modlog_case_create(mod, ctx, member_factory):
|
||||||
|
from datetime import datetime as dt
|
||||||
|
usr = member_factory.get()
|
||||||
|
guild = ctx.guild
|
||||||
|
case_type = "ban"
|
||||||
|
moderator = ctx.author
|
||||||
|
reason = "Test 12345"
|
||||||
|
created_at = dt.utcnow()
|
||||||
|
case = await mod.create_case(
|
||||||
|
guild, created_at, case_type, usr, moderator, reason
|
||||||
|
)
|
||||||
|
assert case is not None
|
||||||
|
assert case.user == usr
|
||||||
|
assert case.action_type == case_type
|
||||||
|
assert case.moderator == moderator
|
||||||
|
assert case.reason == reason
|
||||||
|
assert case.created_at == int(created_at.timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_modlog_set_modlog_channel(mod, ctx):
|
||||||
|
await mod.set_modlog_channel(ctx.guild, ctx.channel)
|
||||||
|
assert await mod.get_modlog_channel(ctx.guild) == ctx.channel.id
|
||||||
Loading…
x
Reference in New Issue
Block a user