[ModLog API] Add default casetypes, remove need for a specific auditlog action (#2901)

* I know this needs a changelog entry and docs still

* update tests for new behavior

* update docs, filter; add changelog

* Ready for review

* stop fetching the same Audit logs when the bot is the mod

* I forgot to press save

* fix a comprehension

* Fix AttributeError

* And the other place that happens

* timing fixes
This commit is contained in:
DiscordLiz 2019-07-27 15:37:29 -04:00 committed by Michael H
parent 6280fd9c28
commit 20091cc10a
16 changed files with 251 additions and 214 deletions

View File

@ -0,0 +1 @@
Modlog casetypes no longer have an attribute for auditlog action type.

View File

@ -0,0 +1 @@
Modlog entries now show up properly without the mod cog loaded

View File

@ -0,0 +1 @@
Modlog no longer generates cases without being told to for actions the bot did.

View File

@ -0,0 +1 @@
Some generic modlog casetypes are now pre-registered for cog creator use

View File

@ -0,0 +1 @@
Modlog case registration and modlog event handling was moved to the core bot

View File

@ -57,9 +57,6 @@ it from your setup function:
"default_setting": True, "default_setting": True,
"image": "\N{HAMMER}", "image": "\N{HAMMER}",
"case_str": "Ban", "case_str": "Ban",
# audit_type should be omitted if the action doesn't show
# up in the audit log.
"audit_type": "ban",
} }
try: try:
await modlog.register_casetype(**ban_case) await modlog.register_casetype(**ban_case)
@ -73,14 +70,12 @@ it from your setup function:
"default_setting": True, "default_setting": True,
"image": "\N{BUST IN SILHOUETTE}\N{HAMMER}", "image": "\N{BUST IN SILHOUETTE}\N{HAMMER}",
"case_str": "Hackban", "case_str": "Hackban",
"audit_type": "ban",
}, },
{ {
"name": "kick", "name": "kick",
"default_setting": True, "default_setting": True,
"image": "\N{WOMANS BOOTS}", "image": "\N{WOMANS BOOTS}",
"case_str": "Kick", "case_str": "Kick",
"audit_type": "kick"
} }
] ]
await modlog.register_casetypes(new_types) await modlog.register_casetypes(new_types)

View File

@ -121,7 +121,7 @@ def main():
if cli_flags.dev: if cli_flags.dev:
red.add_cog(Dev()) red.add_cog(Dev())
# noinspection PyProtectedMember # noinspection PyProtectedMember
loop.run_until_complete(modlog._init()) loop.run_until_complete(modlog._init(red))
# noinspection PyProtectedMember # noinspection PyProtectedMember
bank._init() bank._init()

View File

@ -40,7 +40,7 @@ class Filter(commands.Cog):
async def register_filterban(): async def register_filterban():
try: try:
await modlog.register_casetype( await modlog.register_casetype(
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban" "filterban", False, ":filing_cabinet: :hammer:", "Filter ban"
) )
except RuntimeError: except RuntimeError:
pass pass

View File

@ -17,8 +17,6 @@ class MixinMeta(ABC):
self.settings: Config self.settings: Config
self.bot: Red self.bot: Red
self.cache: dict self.cache: dict
self.ban_queue: List[Tuple[int, int]]
self.unban_queue: List[Tuple[int, int]]
@staticmethod @staticmethod
@abstractmethod @abstractmethod
@ -26,15 +24,3 @@ class MixinMeta(ABC):
ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool
) -> bool: ) -> bool:
raise NotImplementedError() raise NotImplementedError()
@classmethod
@abstractmethod
async def get_audit_entry_info(
cls, guild: discord.Guild, action: discord.AuditLogAction, target
):
raise NotImplementedError()
@staticmethod
@abstractmethod
async def get_audit_log_entry(guild: discord.Guild, action: discord.AuditLogAction, target):
raise NotImplementedError()

View File

@ -94,77 +94,6 @@ class Events(MixinMeta):
if not deleted: if not deleted:
await self.check_mention_spam(message) await self.check_mention_spam(message)
@commands.Cog.listener()
async def on_member_ban(self, guild: discord.Guild, member: discord.Member):
if (guild.id, member.id) in self.ban_queue:
self.ban_queue.remove((guild.id, member.id))
return
try:
await modlog.get_modlog_channel(guild)
except RuntimeError:
return # No modlog channel so no point in continuing
mod, reason, date = await self.get_audit_entry_info(
guild, discord.AuditLogAction.ban, member
)
if date is None:
date = datetime.now()
try:
await modlog.create_case(
self.bot, guild, date, "ban", member, mod, reason if reason else None
)
except RuntimeError as e:
print(e)
@commands.Cog.listener()
async def on_member_unban(self, guild: discord.Guild, user: discord.User):
if (guild.id, user.id) in self.unban_queue:
self.unban_queue.remove((guild.id, user.id))
return
try:
await modlog.get_modlog_channel(guild)
except RuntimeError:
return # No modlog channel so no point in continuing
mod, reason, date = await self.get_audit_entry_info(
guild, discord.AuditLogAction.unban, user
)
if date is None:
date = datetime.now()
try:
await modlog.create_case(self.bot, guild, date, "unban", user, mod, reason)
except RuntimeError as e:
print(e)
@commands.Cog.listener()
async def on_modlog_case_create(self, case: modlog.Case):
"""
An event for modlog case creation
"""
try:
mod_channel = await modlog.get_modlog_channel(case.guild)
except RuntimeError:
return
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
case_content = await case.message_content(use_embeds)
if use_embeds:
msg = await mod_channel.send(embed=case_content)
else:
msg = await mod_channel.send(case_content)
await case.edit({"message": msg})
@commands.Cog.listener()
async def on_modlog_case_edit(self, case: modlog.Case):
"""
Event for modlog case edits
"""
if not case.message:
return
use_embed = await case.bot.embed_requested(case.message.channel, case.guild.me)
case_content = await case.message_content(use_embed)
if use_embed:
await case.message.edit(embed=case_content)
else:
await case.message.edit(content=case_content)
@commands.Cog.listener() @commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member): async def on_member_update(self, before: discord.Member, after: discord.Member):
if before.name != after.name: if before.name != after.name:

View File

@ -85,7 +85,6 @@ class KickBanMixin(MixinMeta):
audit_reason = get_audit_reason(author, reason) audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user, reason=audit_reason, delete_message_days=days) await guild.ban(user, reason=audit_reason, delete_message_days=days)
log.info( log.info(
@ -94,10 +93,8 @@ class KickBanMixin(MixinMeta):
) )
) )
except discord.Forbidden: except discord.Forbidden:
self.ban_queue.remove(queue_entry)
return _("I'm not allowed to do that.") return _("I'm not allowed to do that.")
except Exception as e: except Exception as e:
self.ban_queue.remove(queue_entry)
return e # TODO: impproper return type? Is this intended to be re-raised? return e # TODO: impproper return type? Is this intended to be re-raised?
if create_modlog_case: if create_modlog_case:
@ -134,15 +131,13 @@ class KickBanMixin(MixinMeta):
if now > unban_time: # Time to unban the user if now > unban_time: # Time to unban the user
user = await self.bot.fetch_user(uid) user = await self.bot.fetch_user(uid)
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
self.unban_queue.append(queue_entry)
try: try:
await guild.unban(user, reason=_("Tempban finished")) await guild.unban(user, reason=_("Tempban finished"))
guild_tempbans.remove(uid) guild_tempbans.remove(uid)
except discord.Forbidden: except discord.Forbidden:
self.unban_queue.remove(queue_entry)
log.info("Failed to unban member due to permissions") log.info("Failed to unban member due to permissions")
except discord.HTTPException: except discord.HTTPException as e:
self.unban_queue.remove(queue_entry) log.info(f"Failed to unban member: error code: {e.code}")
await asyncio.sleep(60) await asyncio.sleep(60)
@commands.command() @commands.command()
@ -319,16 +314,13 @@ class KickBanMixin(MixinMeta):
user = discord.Object(id=user_id) user = discord.Object(id=user_id)
audit_reason = get_audit_reason(author, reason) audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user_id) queue_entry = (guild.id, user_id)
self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user, reason=audit_reason, delete_message_days=days) await guild.ban(user, reason=audit_reason, delete_message_days=days)
log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id)) log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
except discord.NotFound: except discord.NotFound:
self.ban_queue.remove(queue_entry)
errors[user_id] = _("User {user_id} does not exist.").format(user_id=user_id) errors[user_id] = _("User {user_id} does not exist.").format(user_id=user_id)
continue continue
except discord.Forbidden: except discord.Forbidden:
self.ban_queue.remove(queue_entry)
errors[user_id] = _("Could not ban {user_id}: missing permissions.").format( errors[user_id] = _("Could not ban {user_id}: missing permissions.").format(
user_id=user_id user_id=user_id
) )
@ -389,7 +381,6 @@ class KickBanMixin(MixinMeta):
invite_link=invite, invite_link=invite,
) )
) )
self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user) await guild.ban(user)
except discord.Forbidden: except discord.Forbidden:
@ -455,24 +446,19 @@ class KickBanMixin(MixinMeta):
) )
except discord.HTTPException: except discord.HTTPException:
msg = None msg = None
self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user, reason=audit_reason, delete_message_days=1) await guild.ban(user, reason=audit_reason, delete_message_days=1)
except discord.errors.Forbidden: except discord.errors.Forbidden:
self.ban_queue.remove(queue_entry)
await ctx.send(_("My role is not high enough to softban that user.")) await ctx.send(_("My role is not high enough to softban that user."))
if msg is not None: if msg is not None:
await msg.delete() await msg.delete()
return return
except discord.HTTPException as e: except discord.HTTPException as e:
self.ban_queue.remove(queue_entry)
print(e) print(e)
return return
self.unban_queue.append(queue_entry)
try: try:
await guild.unban(user) await guild.unban(user)
except discord.HTTPException as e: except discord.HTTPException as e:
self.unban_queue.remove(queue_entry)
print(e) print(e)
return return
else: else:
@ -571,11 +557,9 @@ class KickBanMixin(MixinMeta):
await ctx.send(_("It seems that user isn't banned!")) await ctx.send(_("It seems that user isn't banned!"))
return return
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
self.unban_queue.append(queue_entry)
try: try:
await guild.unban(user, reason=audit_reason) await guild.unban(user, reason=audit_reason)
except discord.HTTPException: except discord.HTTPException:
self.unban_queue.remove(queue_entry)
await ctx.send(_("Something went wrong while attempting to unban that user")) await ctx.send(_("Something went wrong while attempting to unban that user"))
return return
else: else:

View File

@ -71,10 +71,7 @@ class Mod(
self.settings.register_channel(**self.default_channel_settings) self.settings.register_channel(**self.default_channel_settings)
self.settings.register_member(**self.default_member_settings) self.settings.register_member(**self.default_member_settings)
self.settings.register_user(**self.default_user_settings) self.settings.register_user(**self.default_user_settings)
self.ban_queue: List[Tuple[int, int]] = []
self.unban_queue: List[Tuple[int, int]] = []
self.cache: dict = {} self.cache: dict = {}
self.registration_task = self.bot.loop.create_task(self._casetype_registration())
self.tban_expiry_task = self.bot.loop.create_task(self.check_tempban_expirations()) self.tban_expiry_task = self.bot.loop.create_task(self.check_tempban_expirations())
self.last_case: dict = defaultdict(dict) self.last_case: dict = defaultdict(dict)
@ -99,13 +96,6 @@ class Mod(
await self.settings.guild(discord.Object(id=guild_id)).delete_repeats.set(val) await self.settings.guild(discord.Object(id=guild_id)).delete_repeats.set(val)
await self.settings.version.set(__version__) await self.settings.version.set(__version__)
@staticmethod
async def _casetype_registration():
try:
await modlog.register_casetypes(CASETYPES)
except RuntimeError:
pass
# TODO: Move this to core. # TODO: Move this to core.
# This would be in .movetocore , but the double-under name here makes that more trouble # This would be in .movetocore , but the double-under name here makes that more trouble
async def bot_check(self, ctx): async def bot_check(self, ctx):
@ -126,59 +116,3 @@ class Mod(
guild_ignored = await self.settings.guild(ctx.guild).ignored() guild_ignored = await self.settings.guild(ctx.guild).ignored()
chann_ignored = await self.settings.channel(ctx.channel).ignored() chann_ignored = await self.settings.channel(ctx.channel).ignored()
return not (guild_ignored or chann_ignored and not perms.manage_channels) return not (guild_ignored or chann_ignored and not perms.manage_channels)
@classmethod
async def get_audit_entry_info(
cls, guild: discord.Guild, action: discord.AuditLogAction, target
):
"""Get info about an audit log entry.
Parameters
----------
guild : discord.Guild
Same as ``guild`` in `get_audit_log_entry`.
action : int
Same as ``action`` in `get_audit_log_entry`.
target : `discord.User` or `discord.Member`
Same as ``target`` in `get_audit_log_entry`.
Returns
-------
tuple
A tuple in the form``(mod: discord.Member, reason: str,
date_created: datetime.datetime)``. Returns ``(None, None, None)``
if the audit log entry could not be found.
"""
try:
entry = await cls.get_audit_log_entry(guild, action=action, target=target)
except discord.HTTPException:
entry = None
if entry is None:
return None, None, None
return entry.user, entry.reason, entry.created_at
@staticmethod
async def get_audit_log_entry(guild: discord.Guild, action: discord.AuditLogAction, target):
"""Get an audit log entry.
Any exceptions encountered when looking through the audit log will be
propogated out of this function.
Parameters
----------
guild : discord.Guild
The guild for the audit log.
action : int
The audit log action (see `discord.AuditLogAction`).
target : `discord.Member` or `discord.User`
The target of the audit log action.
Returns
-------
discord.AuditLogEntry
The audit log entry. Returns ``None`` if not found.
"""
async for entry in guild.audit_logs(action=action):
if entry.target == target:
return entry

View File

@ -0,0 +1,111 @@
"""
Contains generic mod action casetypes for use in Red and 3rd party cogs.
These do not need to be registered to the modlog, as it is done for you.
"""
ban = {"name": "ban", "default_setting": True, "image": "\N{HAMMER}", "case_str": "Ban"}
kick = {"name": "kick", "default_setting": True, "image": "\N{WOMANS BOOTS}", "case_str": "Kick"}
hackban = {
"name": "hackban",
"default_setting": True,
"image": "\N{BUST IN SILHOUETTE}\N{HAMMER}",
"case_str": "Hackban",
}
tempban = {
"name": "tempban",
"default_setting": True,
"image": "\N{ALARM CLOCK}\N{HAMMER}",
"case_str": "Tempban",
}
softban = {
"name": "softban",
"default_setting": True,
"image": "\N{DASH SYMBOL}\N{HAMMER}",
"case_str": "Softban",
}
unban = {
"name": "unban",
"default_setting": True,
"image": "\N{DOVE OF PEACE}",
"case_str": "Unban",
}
voiceban = {
"name": "voiceban",
"default_setting": True,
"image": "\N{SPEAKER WITH CANCELLATION STROKE}",
"case_str": "Voice Ban",
}
voiceunban = {
"name": "voiceunban",
"default_setting": True,
"image": "\N{SPEAKER}",
"case_str": "Voice Unban",
}
voicemute = {
"name": "vmute",
"default_setting": False,
"image": "\N{SPEAKER WITH CANCELLATION STROKE}",
"case_str": "Voice Mute",
}
channelmute = {
"name": "cmute",
"default_setting": False,
"image": "\N{SPEAKER WITH CANCELLATION STROKE}",
"case_str": "Channel Mute",
}
servermute = {
"name": "smute",
"default_setting": True,
"image": "\N{SPEAKER WITH CANCELLATION STROKE}",
"case_str": "Server Mute",
}
voiceunmute = {
"name": "vunmute",
"default_setting": False,
"image": "\N{SPEAKER}",
"case_str": "Voice Unmute",
}
channelunmute = {
"name": "cunmute",
"default_setting": False,
"image": "\N{SPEAKER}",
"case_str": "Channel Unmute",
}
serverunmute = {
"name": "sunmute",
"default_setting": True,
"image": "\N{SPEAKER}",
"case_str": "Server Unmute",
}
voicekick = {
"name": "vkick",
"default_setting": False,
"image": "\N{SPEAKER WITH CANCELLATION STROKE}",
"case_str": "Voice Kick",
}
all_generics = (
ban,
kick,
hackban,
tempban,
softban,
unban,
voiceban,
voiceunban,
voicemute,
channelmute,
servermute,
voiceunmute,
serverunmute,
channelunmute,
voicekick,
)

View File

@ -1,4 +1,5 @@
from datetime import datetime import asyncio
from datetime import datetime, timedelta
from typing import List, Union, Optional, cast from typing import List, Union, Optional, cast
import discord import discord
@ -14,6 +15,8 @@ from .utils.common_filters import (
) )
from .i18n import Translator from .i18n import Translator
from .generic_casetypes import all_generics
__all__ = [ __all__ = [
"Case", "Case",
"CaseType", "CaseType",
@ -32,17 +35,20 @@ __all__ = [
] ]
_conf: Optional[Config] = None _conf: Optional[Config] = None
_bot_ref: Optional[Red] = None
_CASETYPES = "CASETYPES" _CASETYPES = "CASETYPES"
_CASES = "CASES" _CASES = "CASES"
_SCHEMA_VERSION = 2 _SCHEMA_VERSION = 3
_ = Translator("ModLog", __file__) _ = Translator("ModLog", __file__)
async def _init(): async def _init(bot: Red):
global _conf global _conf
global _bot_ref
_bot_ref = bot
_conf = Config.get_conf(None, 1354799444, cog_name="ModLog") _conf = Config.get_conf(None, 1354799444, cog_name="ModLog")
_conf.register_global(schema_version=1) _conf.register_global(schema_version=1)
_conf.register_guild(mod_log=None, casetypes={}) _conf.register_guild(mod_log=None, casetypes={})
@ -51,12 +57,88 @@ async def _init():
_conf.register_custom(_CASETYPES) _conf.register_custom(_CASETYPES)
_conf.register_custom(_CASES) _conf.register_custom(_CASES)
await _migrate_config(from_version=await _conf.schema_version(), to_version=_SCHEMA_VERSION) await _migrate_config(from_version=await _conf.schema_version(), to_version=_SCHEMA_VERSION)
await register_casetypes(all_generics)
async def on_member_ban(guild: discord.Guild, member: discord.Member):
if not guild.me.guild_permissions.view_audit_log:
return
try:
await get_modlog_channel(guild)
except RuntimeError:
return # No modlog channel so no point in continuing
when = datetime.utcnow()
before = when + timedelta(minutes=1)
after = when - timedelta(minutes=1)
await asyncio.sleep(10) # prevent small delays from causing a 5 minute delay on entry
attempts = 0
while attempts < 12: # wait up to an hour to find a matching case
attempts += 1
try:
entry = await guild.audit_logs(
action=discord.AuditLogAction.ban, before=before, after=after
).find(lambda e: e.target.id == member.id and after < e.created_at < before)
except discord.Forbidden:
break
except discord.HTTPException:
pass
else:
if entry:
if entry.user.id != guild.me.id:
# Don't create modlog entires for the bot's own bans, cogs do this.
mod, reason, date = entry.user, entry.reason, entry.created_at
await create_case(_bot_ref, guild, date, "ban", member, mod, reason)
return
await asyncio.sleep(300)
async def on_member_unban(guild: discord.Guild, user: discord.User):
if not guild.me.guild_permissions.view_audit_log:
return
try:
await get_modlog_channel(guild)
except RuntimeError:
return # No modlog channel so no point in continuing
when = datetime.utcnow()
before = when + timedelta(minutes=1)
after = when - timedelta(minutes=1)
await asyncio.sleep(10) # prevent small delays from causing a 5 minute delay on entry
attempts = 0
while attempts < 12: # wait up to an hour to find a matching case
attempts += 1
try:
entry = await guild.audit_logs(
action=discord.AuditLogAction.unban, before=before, after=after
).find(lambda e: e.target.id == user.id and after < e.created_at < before)
except discord.Forbidden:
break
except discord.HTTPException:
pass
else:
if entry:
if entry.user.id != guild.me.id:
# Don't create modlog entires for the bot's own unbans, cogs do this.
mod, reason, date = entry.user, entry.reason, entry.created_at
await create_case(_bot_ref, guild, date, "unban", user, mod, reason)
return
await asyncio.sleep(300)
bot.add_listener(on_member_ban)
bot.add_listener(on_member_unban)
async def _migrate_config(from_version: int, to_version: int): async def _migrate_config(from_version: int, to_version: int):
if from_version == to_version: if from_version == to_version:
return return
elif from_version < to_version:
if from_version < 2 <= to_version:
# casetypes go from GLOBAL -> casetypes to CASETYPES # casetypes go from GLOBAL -> casetypes to CASETYPES
all_casetypes = await _conf.get_raw("casetypes", default={}) all_casetypes = await _conf.get_raw("casetypes", default={})
if all_casetypes: if all_casetypes:
@ -72,13 +154,26 @@ async def _migrate_config(from_version: int, to_version: int):
await _conf.custom(_CASES).set(all_cases) await _conf.custom(_CASES).set(all_cases)
# new schema is now in place # new schema is now in place
await _conf.schema_version.set(_SCHEMA_VERSION) await _conf.schema_version.set(2)
# migration done, now let's delete all the old stuff # migration done, now let's delete all the old stuff
await _conf.clear_raw("casetypes") await _conf.clear_raw("casetypes")
for guild_id in all_guild_data: for guild_id in all_guild_data:
await _conf.guild(cast(discord.Guild, discord.Object(id=guild_id))).clear_raw("cases") await _conf.guild(cast(discord.Guild, discord.Object(id=guild_id))).clear_raw("cases")
if from_version < 3 <= to_version:
all_casetypes = {
casetype_name: {
inner_key: inner_value
for inner_key, inner_value in casetype_data.items()
if inner_key != "audit_type"
}
for casetype_name, casetype_data in (await _conf.custom(_CASETYPES).all()).items()
}
await _conf.custom(_CASETYPES).set(all_casetypes)
await _conf.schema_version.set(3)
class Case: class Case:
"""A single mod log case""" """A single mod log case"""
@ -131,6 +226,17 @@ class Case:
await _conf.custom(_CASES, str(self.guild.id), str(self.case_number)).set(self.to_json()) await _conf.custom(_CASES, str(self.guild.id), str(self.case_number)).set(self.to_json())
self.bot.dispatch("modlog_case_edit", self) self.bot.dispatch("modlog_case_edit", self)
if not self.message:
return
try:
use_embed = await self.bot.embed_requested(self.message.channel, self.guild.me)
case_content = await self.message_content(use_embed)
if use_embed:
await self.message.edit(embed=case_content)
else:
await self.message.edit(content=case_content)
finally:
return None
async def message_content(self, embed: bool = True): async def message_content(self, embed: bool = True):
""" """
@ -371,9 +477,7 @@ class CaseType:
The emoji to use for the case type (for example, :boot:) The emoji to use for the case type (for example, :boot:)
case_str: str case_str: str
The string representation of the case (example: Ban) The string representation of the case (example: Ban)
audit_type: `str`, optional
The action type of the action as it would appear in the
audit log
""" """
def __init__( def __init__(
@ -382,14 +486,12 @@ class CaseType:
default_setting: bool, default_setting: bool,
image: str, image: str,
case_str: str, case_str: str,
audit_type: Optional[str] = None,
guild: Optional[discord.Guild] = None, guild: Optional[discord.Guild] = None,
): ):
self.name = name self.name = name
self.default_setting = default_setting self.default_setting = default_setting
self.image = image self.image = image
self.case_str = case_str self.case_str = case_str
self.audit_type = audit_type
self.guild = guild self.guild = guild
async def to_json(self): async def to_json(self):
@ -398,7 +500,6 @@ class CaseType:
"default_setting": self.default_setting, "default_setting": self.default_setting,
"image": self.image, "image": self.image,
"case_str": self.case_str, "case_str": self.case_str,
"audit_type": self.audit_type,
} }
await _conf.custom(_CASETYPES, self.name).set(data) await _conf.custom(_CASETYPES, self.name).set(data)
@ -663,6 +764,18 @@ async def create_case(
) )
await _conf.custom(_CASES, str(guild.id), str(next_case_number)).set(case.to_json()) await _conf.custom(_CASES, str(guild.id), str(next_case_number)).set(case.to_json())
bot.dispatch("modlog_case_create", case) bot.dispatch("modlog_case_create", case)
try:
mod_channel = await get_modlog_channel(case.guild)
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
case_content = await case.message_content(use_embeds)
if use_embeds:
msg = await mod_channel.send(embed=case_content)
else:
msg = await mod_channel.send(case_content)
await case.edit({"message": msg})
except (RuntimeError, discord.HTTPException):
pass
finally:
return case return case
@ -706,7 +819,7 @@ async def get_all_casetypes(guild: discord.Guild = None) -> List[CaseType]:
async def register_casetype( async def register_casetype(
name: str, default_setting: bool, image: str, case_str: str, audit_type: str = None name: str, default_setting: bool, image: str, case_str: str
) -> CaseType: ) -> CaseType:
""" """
Registers a case type. If the case type exists and Registers a case type. If the case type exists and
@ -725,9 +838,6 @@ async def register_casetype(
The emoji to use for the case type (for example, :boot:) The emoji to use for the case type (for example, :boot:)
case_str: str case_str: str
The string representation of the case (example: Ban) The string representation of the case (example: Ban)
audit_type: `str`, optional
The action type of the action as it would appear in the
audit log
Returns Returns
------- -------
@ -742,8 +852,6 @@ async def register_casetype(
If a parameter is missing If a parameter is missing
ValueError ValueError
If a parameter's value is not valid 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): if not isinstance(name, str):
@ -754,16 +862,10 @@ async def register_casetype(
raise ValueError("The 'image' is not a string!") raise ValueError("The 'image' is not a string!")
if not isinstance(case_str, str): if not isinstance(case_str, str):
raise ValueError("The 'case_str' is not a string!") raise ValueError("The 'case_str' is not a string!")
if audit_type is not None:
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) ct = await get_casetype(name)
if ct is None: if ct is None:
casetype = CaseType(name, default_setting, image, case_str, audit_type) casetype = CaseType(name, default_setting, image, case_str)
await casetype.to_json() await casetype.to_json()
return casetype return casetype
else: else:
@ -779,9 +881,6 @@ async def register_casetype(
if ct.case_str != case_str: if ct.case_str != case_str:
ct.case_str = case_str ct.case_str = case_str
changed = True changed = True
if ct.audit_type != audit_type:
ct.audit_type = audit_type
changed = True
if changed: if changed:
await ct.to_json() await ct.to_json()
return ct return ct

View File

@ -5,11 +5,11 @@ __all__ = ["mod"]
@pytest.fixture @pytest.fixture
async def mod(config, monkeypatch): async def mod(config, monkeypatch, red):
from redbot.core import Config from redbot.core import Config
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(Config, "get_conf", lambda *args, **kwargs: config) m.setattr(Config, "get_conf", lambda *args, **kwargs: config)
await modlog._init() await modlog._init(red)
return modlog return modlog

View File

@ -5,13 +5,7 @@ from redbot.pytest.mod import *
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_modlog_register_casetype(mod): async def test_modlog_register_casetype(mod):
ct = { ct = {"name": "ban", "default_setting": True, "image": ":hammer:", "case_str": "Ban"}
"name": "ban",
"default_setting": True,
"image": ":hammer:",
"case_str": "Ban",
"audit_type": "ban",
}
casetype = await mod.register_casetype(**ct) casetype = await mod.register_casetype(**ct)
assert casetype is not None assert casetype is not None