[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,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
import discord
@@ -14,6 +15,8 @@ from .utils.common_filters import (
)
from .i18n import Translator
from .generic_casetypes import all_generics
__all__ = [
"Case",
"CaseType",
@@ -32,17 +35,20 @@ __all__ = [
]
_conf: Optional[Config] = None
_bot_ref: Optional[Red] = None
_CASETYPES = "CASETYPES"
_CASES = "CASES"
_SCHEMA_VERSION = 2
_SCHEMA_VERSION = 3
_ = Translator("ModLog", __file__)
async def _init():
async def _init(bot: Red):
global _conf
global _bot_ref
_bot_ref = bot
_conf = Config.get_conf(None, 1354799444, cog_name="ModLog")
_conf.register_global(schema_version=1)
_conf.register_guild(mod_log=None, casetypes={})
@@ -51,12 +57,88 @@ async def _init():
_conf.register_custom(_CASETYPES)
_conf.register_custom(_CASES)
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):
if from_version == to_version:
return
elif from_version < to_version:
if from_version < 2 <= to_version:
# casetypes go from GLOBAL -> casetypes to CASETYPES
all_casetypes = await _conf.get_raw("casetypes", default={})
if all_casetypes:
@@ -72,13 +154,26 @@ async def _migrate_config(from_version: int, to_version: int):
await _conf.custom(_CASES).set(all_cases)
# 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
await _conf.clear_raw("casetypes")
for guild_id in all_guild_data:
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:
"""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())
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):
"""
@@ -371,9 +477,7 @@ class CaseType:
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`, optional
The action type of the action as it would appear in the
audit log
"""
def __init__(
@@ -382,14 +486,12 @@ class CaseType:
default_setting: bool,
image: str,
case_str: str,
audit_type: Optional[str] = None,
guild: Optional[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):
@@ -398,7 +500,6 @@ class CaseType:
"default_setting": self.default_setting,
"image": self.image,
"case_str": self.case_str,
"audit_type": self.audit_type,
}
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())
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]:
@@ -706,7 +819,7 @@ async def get_all_casetypes(guild: discord.Guild = None) -> List[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:
"""
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:)
case_str: str
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
-------
@@ -742,8 +852,6 @@ async def register_casetype(
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):
@@ -754,16 +862,10 @@ async def register_casetype(
raise ValueError("The 'image' is not a string!")
if not isinstance(case_str, str):
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)
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()
return casetype
else:
@@ -779,9 +881,6 @@ async def register_casetype(
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