mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
[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:
parent
6280fd9c28
commit
20091cc10a
1
changelog.d/2897.breaking.1.rst
Normal file
1
changelog.d/2897.breaking.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Modlog casetypes no longer have an attribute for auditlog action type.
|
||||||
1
changelog.d/2897.bugfix.1.rst
Normal file
1
changelog.d/2897.bugfix.1.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Modlog entries now show up properly without the mod cog loaded
|
||||||
1
changelog.d/2897.enhance.2.rst
Normal file
1
changelog.d/2897.enhance.2.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Modlog no longer generates cases without being told to for actions the bot did.
|
||||||
1
changelog.d/2897.enhance.3.rst
Normal file
1
changelog.d/2897.enhance.3.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Some generic modlog casetypes are now pre-registered for cog creator use
|
||||||
1
changelog.d/mod/2897.misc.rst
Normal file
1
changelog.d/mod/2897.misc.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
Modlog case registration and modlog event handling was moved to the core bot
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
111
redbot/core/generic_casetypes.py
Normal file
111
redbot/core/generic_casetypes.py
Normal 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,
|
||||||
|
)
|
||||||
@ -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,7 +764,19 @@ 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)
|
||||||
return 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
|
||||||
|
|
||||||
|
|
||||||
async def get_casetype(name: str, guild: Optional[discord.Guild] = None) -> Optional[CaseType]:
|
async def get_casetype(name: str, guild: Optional[discord.Guild] = None) -> Optional[CaseType]:
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user