mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-21 10:17:59 -05:00
[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:
@@ -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
707
redbot/core/modlog.py
Normal 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
124
redbot/core/utils/mod.py
Normal 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
|
||||
Reference in New Issue
Block a user