mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-21 10:17:59 -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:
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
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user