237 lines
8.9 KiB
Python
237 lines
8.9 KiB
Python
|
import contextlib
|
||
|
import datetime
|
||
|
from typing import List, Literal, Optional, Union
|
||
|
|
||
|
import discord
|
||
|
import humanize
|
||
|
from discord.http import Route
|
||
|
from redbot.core import Config, commands, modlog
|
||
|
from redbot.core.bot import Red
|
||
|
from redbot.core.commands.converter import TimedeltaConverter
|
||
|
|
||
|
RequestType = Literal["discord_deleted_user", "owner", "user", "user_strict"]
|
||
|
|
||
|
class Shortmute(commands.Cog):
|
||
|
"""Allows staff members to shortmute individuals for up to 30 minutes, using Discord's Timeouts feature.
|
||
|
Maintained by SeaswimmerTheFsh.
|
||
|
Original source code by sravan1946."""
|
||
|
|
||
|
def __init__(self, bot: Red) -> None:
|
||
|
self.bot = bot
|
||
|
self.config = Config.get_conf(self, identifier=2354731, force_registration=True)
|
||
|
self.config.register_guild(
|
||
|
dm = True
|
||
|
)
|
||
|
|
||
|
def format_help_for_context(self, ctx: commands.Context) -> str:
|
||
|
"""
|
||
|
Thanks Sinbad!
|
||
|
"""
|
||
|
pre_processed = super().format_help_for_context(ctx)
|
||
|
return f"{pre_processed}\n\nAuthors: {', '.join(self.__author__)}\nCog Version: {self.__version__}"
|
||
|
|
||
|
async def red_delete_data_for_user(
|
||
|
self, *, requester: RequestType, user_id: int
|
||
|
) -> None:
|
||
|
# TODO: Replace this with the proper end user data removal handling.
|
||
|
super().red_delete_data_for_user(requester=requester, user_id=user_id)
|
||
|
|
||
|
async def is_user_timed_out(self, member: discord.Member) -> bool:
|
||
|
r = Route(
|
||
|
"GET",
|
||
|
"/guilds/{guild_id}/members/{user_id}",
|
||
|
guild_id=member.guild.id,
|
||
|
user_id=member.id,
|
||
|
)
|
||
|
try:
|
||
|
data = await self.bot.http.request(r)
|
||
|
except discord.NotFound:
|
||
|
return False
|
||
|
return data["communication_disabled_until"] is not None
|
||
|
|
||
|
async def pre_load(self):
|
||
|
with contextlib.suppress(RuntimeError):
|
||
|
await modlog.register_casetype(
|
||
|
name="timeout",
|
||
|
default_setting=True,
|
||
|
image=":mute:",
|
||
|
case_str="Timeout",
|
||
|
)
|
||
|
await modlog.register_casetype(
|
||
|
name="untimeout",
|
||
|
default_setting=True,
|
||
|
image=":sound:",
|
||
|
case_str="Untimeout",
|
||
|
)
|
||
|
|
||
|
async def timeout_user(
|
||
|
self,
|
||
|
ctx: commands.Context,
|
||
|
member: discord.Member,
|
||
|
time: Optional[datetime.timedelta],
|
||
|
reason: Optional[str] = None,
|
||
|
) -> None:
|
||
|
r = Route(
|
||
|
"PATCH",
|
||
|
"/guilds/{guild_id}/members/{user_id}",
|
||
|
guild_id=ctx.guild.id,
|
||
|
user_id=member.id,
|
||
|
)
|
||
|
|
||
|
payload = {
|
||
|
"communication_disabled_until": str(
|
||
|
datetime.datetime.now(datetime.timezone.utc) + time
|
||
|
)
|
||
|
if time
|
||
|
else None
|
||
|
}
|
||
|
|
||
|
await ctx.bot.http.request(r, json=payload, reason=reason)
|
||
|
await modlog.create_case(
|
||
|
bot=ctx.bot,
|
||
|
guild=ctx.guild,
|
||
|
created_at=datetime.datetime.now(datetime.timezone.utc),
|
||
|
action_type="timeout" if time else "untimeout",
|
||
|
user=member,
|
||
|
moderator=ctx.author,
|
||
|
reason=reason,
|
||
|
until=(datetime.datetime.now(datetime.timezone.utc) + time)
|
||
|
if time
|
||
|
else None,
|
||
|
channel=ctx.channel,
|
||
|
)
|
||
|
if await self.config.guild(member.guild).dm():
|
||
|
with contextlib.suppress(discord.HTTPException):
|
||
|
message = "You have been"
|
||
|
message += (
|
||
|
f" timed out for {humanize.naturaldelta(time)}"
|
||
|
if time
|
||
|
else " untimedout"
|
||
|
)
|
||
|
message += f" in {ctx.guild.name}"
|
||
|
message += f" for reason: {reason}" if reason else ""
|
||
|
await member.send(message)
|
||
|
|
||
|
async def timeout_role(
|
||
|
self,
|
||
|
ctx: commands.Context,
|
||
|
role: discord.Role,
|
||
|
time: datetime.timedelta,
|
||
|
reason: Optional[str] = None,
|
||
|
) -> List[discord.Member]:
|
||
|
failed = []
|
||
|
members = list(role.members)
|
||
|
for member in members:
|
||
|
try:
|
||
|
await self.timeout_user(ctx, member, time, reason)
|
||
|
except discord.HTTPException:
|
||
|
failed.append(member)
|
||
|
return failed
|
||
|
|
||
|
@commands.command(aliases=["tt"])
|
||
|
@commands.guild_only()
|
||
|
@commands.cooldown(1, 1, commands.BucketType.user)
|
||
|
@commands.mod_or_permissions(administrator=True)
|
||
|
async def timeout(
|
||
|
self,
|
||
|
ctx: commands.Context,
|
||
|
member_or_role: Union[discord.Member, discord.Role],
|
||
|
time: TimedeltaConverter(
|
||
|
minimum=datetime.timedelta(minutes=1),
|
||
|
maximum=datetime.timedelta(days=28),
|
||
|
default_unit="minutes",
|
||
|
allowed_units=["minutes", "seconds", "hours", "days"],
|
||
|
) = None,
|
||
|
*,
|
||
|
reason: Optional[str] = None,
|
||
|
):
|
||
|
"""
|
||
|
Timeout users.
|
||
|
`<member_or_role>` is the username/rolename, ID or mention. If provided a role,
|
||
|
everyone with that role will be timedout.
|
||
|
`[time]` is the time to mute for. Time is any valid time length such as `45 minutes`
|
||
|
or `3 days`. If nothing is provided the timeout will be 60 seconds default.
|
||
|
`[reason]` is the reason for the timeout. Defaults to `None` if nothing is provided.
|
||
|
Examples:
|
||
|
`[p]timeout @member 5m talks too much`
|
||
|
`[p]timeout @member 10m`
|
||
|
"""
|
||
|
if not time:
|
||
|
time = datetime.timedelta(seconds=60)
|
||
|
timestamp = datetime.datetime.now(datetime.timezone.utc) + time
|
||
|
timestamp = int(datetime.datetime.timestamp(timestamp))
|
||
|
if isinstance(member_or_role, discord.Member):
|
||
|
check = await is_allowed_by_hierarchy(ctx.bot, ctx.author, member_or_role)
|
||
|
if not check:
|
||
|
return await ctx.send("You cannot timeout this user due to hierarchy.")
|
||
|
if member_or_role.permissions_in(ctx.channel).administrator:
|
||
|
return await ctx.send("You can't timeout an administrator.")
|
||
|
await self.timeout_user(ctx, member_or_role, time, reason)
|
||
|
return await ctx.send(
|
||
|
f"{member_or_role.mention} has been timed out till <t:{timestamp}:f>."
|
||
|
)
|
||
|
if isinstance(member_or_role, discord.Role):
|
||
|
await ctx.send(
|
||
|
f"Timeing out {len(member_or_role.members)} members till <t:{timestamp}:f>."
|
||
|
)
|
||
|
failed = await self.timeout_role(ctx, member_or_role, time, reason)
|
||
|
return await ctx.send(f"Failed to timeout {len(failed)} members.")
|
||
|
|
||
|
@commands.command(aliases=["utt"])
|
||
|
@commands.guild_only()
|
||
|
@commands.cooldown(1, 1, commands.BucketType.user)
|
||
|
@commands.mod_or_permissions(administrator=True)
|
||
|
async def untimeout(
|
||
|
self,
|
||
|
ctx: commands.Context,
|
||
|
member_or_role: Union[discord.Member, discord.Role],
|
||
|
*,
|
||
|
reason: Optional[str] = None,
|
||
|
):
|
||
|
"""
|
||
|
Untimeout users.
|
||
|
`<member_or_role>` is the username/rolename, ID or mention. If
|
||
|
provided a role, everyone with that role will be untimed.
|
||
|
`[reason]` is the reason for the untimeout. Defaults to `None`
|
||
|
if nothing is provided.
|
||
|
"""
|
||
|
if isinstance(member_or_role, discord.Member):
|
||
|
is_timedout = await self.is_user_timed_out(member_or_role)
|
||
|
if not is_timedout:
|
||
|
return await ctx.send("This user is not timed out.")
|
||
|
await self.timeout_user(ctx, member_or_role, None, reason)
|
||
|
return await ctx.send(f"Removed timeout from {member_or_role.mention}")
|
||
|
if isinstance(member_or_role, discord.Role):
|
||
|
await ctx.send(
|
||
|
f"Removing timeout from {len(member_or_role.members)} members."
|
||
|
)
|
||
|
members = list(member_or_role.members)
|
||
|
for member in members:
|
||
|
if await self.is_user_timed_out(member):
|
||
|
await self.timeout_user(ctx, member, None, reason)
|
||
|
return await ctx.send(f"Removed timeout from {len(members)} members.")
|
||
|
|
||
|
@commands.group()
|
||
|
@commands.guild_only()
|
||
|
@commands.admin_or_permissions(manage_guild=True)
|
||
|
async def timeoutset(self, ctx: commands.Context):
|
||
|
"""Manage timeout settings."""
|
||
|
|
||
|
@timeoutset.command(name="dm")
|
||
|
async def timeoutset_dm(self, ctx: commands.Context):
|
||
|
"""Change whether to DM the user when they are timed out."""
|
||
|
current = await self.config.guild(ctx.guild).dm()
|
||
|
await self.config.guild(ctx.guild).dm.set(not current)
|
||
|
w = "Will not" if current else "Will"
|
||
|
await ctx.send(f"I {w} DM the user when they are timed out.")
|
||
|
|
||
|
|
||
|
# https://github.com/phenom4n4n/phen-cogs/blob/8727d6ee74b40709c7eb9300713dc22b88a17915/roleutils/utils.py#L34
|
||
|
async def is_allowed_by_hierarchy(
|
||
|
bot: Red, user: discord.Member, member: discord.Member
|
||
|
) -> bool:
|
||
|
return (
|
||
|
user.guild.owner_id == user.id
|
||
|
or user.top_role > member.top_role
|
||
|
or await bot.is_owner(user)
|
||
|
)
|