[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:
palmtree5 2017-10-22 17:02:16 -08:00 committed by Will
parent fe61ef167e
commit fb125ef619
20 changed files with 3190 additions and 0 deletions

95
docs/framework_modlog.rst Normal file
View 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:

View File

@ -31,9 +31,11 @@ Welcome to Red - Discord Bot's documentation!
framework_cogmanager
framework_config
framework_downloader
framework_modlog
framework_context
Indices and tables
==================

View File

@ -0,0 +1,6 @@
from .cleanup import Cleanup
from redbot.core.bot import Red
def setup(bot: Red):
bot.add_cog(Cleanup(bot))

View 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)

View 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"

View File

@ -0,0 +1,6 @@
from .filter import Filter
from redbot.core.bot import Red
def setup(bot: Red):
bot.add_cog(Filter(bot))

View 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)

View 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"

View 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
View 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)

View 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
View File

@ -0,0 +1,4 @@
import logging
log = logging.getLogger("red.mod")

1299
redbot/cogs/mod/mod.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
from redbot.core.bot import Red
from .modlog import ModLog
def setup(bot: Red):
bot.add_cog(ModLog(bot))

View 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"

View 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."))

View File

@ -91,6 +91,29 @@ class RedBase(BotBase):
return True
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):
return await super().get_context(message, cls=cls)

707
redbot/core/modlog.py Normal file
View 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
View 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
View 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