feat: added most of the functionality into the shortmute cog, time to see how broken it is!
This commit is contained in:
parent
9834c645f7
commit
63c764b6cc
2 changed files with 108 additions and 225 deletions
|
@ -2,8 +2,9 @@
|
||||||
"author" : ["SeaswimmerTheFsh"],
|
"author" : ["SeaswimmerTheFsh"],
|
||||||
"install_msg" : "Thank you for installing Shortmute!\nYou can find the source code of this cog here: https://git.seaswimmer.cc/SeaswimmerTheFsh/GalaxyCogs",
|
"install_msg" : "Thank you for installing Shortmute!\nYou can find the source code of this cog here: https://git.seaswimmer.cc/SeaswimmerTheFsh/GalaxyCogs",
|
||||||
"name" : "Shortmute",
|
"name" : "Shortmute",
|
||||||
"short" : "Allows staff members to shortmute individuals for up to 60 minutes.",
|
"short" : "Allows staff members to shortmute individuals for up to 30 minutes.",
|
||||||
"description" : "Allows staff members to shortmute individuals for up to 60 minutes, using Discord's Timeouts feature.",
|
"description" : "Allows staff members to shortmute individuals for up to 30 minutes, using Discord's Timeouts feature.",
|
||||||
"tags" : ["moderation, mutes, timeouts"],
|
"tags" : ["moderation, mutes, timeouts"],
|
||||||
"end_user_data_statement": "This cog stores no end user data."
|
"end_user_data_statement": "This cog stores no end user data.",
|
||||||
|
"requirements": ["pytimeparse2"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,237 +1,119 @@
|
||||||
import contextlib
|
from datetime import datetime
|
||||||
import datetime
|
|
||||||
from typing import List, Literal, Optional, Union
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import humanize
|
from pytimeparse2 import disable_dateutil, parse
|
||||||
from discord.http import Route
|
from discord import ui
|
||||||
from redbot.core import Config, commands, modlog
|
from redbot.core import Config, commands, app_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):
|
class Shortmute(commands.Cog):
|
||||||
"""Allows staff members to shortmute individuals for up to 30 minutes, using Discord's Timeouts feature.
|
"""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:
|
def __init__(self, bot) -> None:
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=2354731, force_registration=True)
|
self.config = Config.get_conf(self, identifier=25781647388294, force_registration=True)
|
||||||
self.config.register_guild(
|
self.config.register_guild(
|
||||||
dm = True
|
dm = True
|
||||||
)
|
)
|
||||||
|
|
||||||
def format_help_for_context(self, ctx: commands.Context) -> str:
|
@app_commands.command()
|
||||||
|
@app_commands.rename(target='member')
|
||||||
|
async def shortmute(self, interaction: discord.Interaction, target: discord.Member, duration: int, reason: str, evidence_link: str = None, evidence_image: discord.AppCommandOptionType.attachment = None):
|
||||||
|
"""Shortmute someone for up to 30m.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
-----------
|
||||||
|
target: discord.Member
|
||||||
|
The member to shortmute
|
||||||
|
duration: int
|
||||||
|
The duration of the shortmute
|
||||||
|
reason: str
|
||||||
|
The reason for the shortmute
|
||||||
|
evidence_link: str = None
|
||||||
|
An image link to evidence for the shortmute, do not use with evidence_image
|
||||||
|
evidence_image: discord.AppCommandOptionType.attachment = None
|
||||||
|
An image file used as evidence for the shortmute, do not use with evidence_link
|
||||||
"""
|
"""
|
||||||
Thanks Sinbad!
|
passed_info = {
|
||||||
"""
|
"target": target,
|
||||||
pre_processed = super().format_help_for_context(ctx)
|
"duration": duration,
|
||||||
return f"{pre_processed}\n\nAuthors: {', '.join(self.__author__)}\nCog Version: {self.__version__}"
|
"reason": reason,
|
||||||
|
"interaction": interaction
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if evidence_image and evidence_link:
|
||||||
|
await interaction.response.send_message(content="You've provided both the `evidence_image` and the `evidence_link` arguments! Please only use one or the other.")
|
||||||
|
return
|
||||||
|
elif evidence_link:
|
||||||
|
evidence = evidence_link
|
||||||
|
elif evidence_image:
|
||||||
|
evidence = str(evidence_image)
|
||||||
|
if duration == 1 or duration == -1:
|
||||||
|
readable_duration = f"{duration} minute"
|
||||||
|
passed_info.update({
|
||||||
|
"readable_duration": readable_duration
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
readable_duration = f"{duration} minutes"
|
||||||
|
passed_info.update({
|
||||||
|
"readable_duration": readable_duration
|
||||||
|
})
|
||||||
|
if duration > 30:
|
||||||
|
await interaction.response(content=f"{readable_duration} is longer than the 30 minutes you are allowed to shortmute users for.", ephemeral=True)
|
||||||
|
return
|
||||||
|
elif duration < 1:
|
||||||
|
await interaction.response(content=f"Please shortmute the user for longer than {readable_duration}! The maximum duration is 30 minutes.", ephemeral=True)
|
||||||
|
return
|
||||||
|
embed = discord.Embed(title="Are you sure?", description=f"Moderator: {interaction.user.mention}\nTarget: {target.mention}\nDuration: `{readable_duration}`\nReason: `{reason}`", color=await self.bot.get_embed_color(None))
|
||||||
|
if evidence:
|
||||||
|
embed.set_image(evidence)
|
||||||
|
passed_info.update({
|
||||||
|
"evidence": evidence
|
||||||
|
})
|
||||||
|
await interaction.response.send_message(embed=embed, view=self.ShortmuteButtons(timeout=180, passed_info=passed_info), ephemeral=True)
|
||||||
|
|
||||||
await ctx.bot.http.request(r, json=payload, reason=reason)
|
class ShortmuteButtons(ui.View):
|
||||||
await modlog.create_case(
|
def __init__(self, timeout, passed_info: dict):
|
||||||
bot=ctx.bot,
|
super().__init__()
|
||||||
guild=ctx.guild,
|
self.timeout = timeout
|
||||||
created_at=datetime.datetime.now(datetime.timezone.utc),
|
self.passed_info = passed_info
|
||||||
action_type="timeout" if time else "untimeout",
|
self.config = Config.get_conf(None, cog_name='Shortmute', identifier=25781647388294)
|
||||||
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(
|
@ui.button(label="Yes", style=discord.ButtonStyle.success)
|
||||||
self,
|
async def shortmute_button_yes(self, button: ui.Button, interaction: discord.Interaction):
|
||||||
ctx: commands.Context,
|
disable_dateutil()
|
||||||
role: discord.Role,
|
target = self.passed_info['target']
|
||||||
time: datetime.timedelta,
|
duration = self.passed_info['duration']
|
||||||
reason: Optional[str] = None,
|
readable_duration = self.passed_info['readable_duration']
|
||||||
) -> List[discord.Member]:
|
reason = self.passed_info['reason']
|
||||||
failed = []
|
old_interaction = self.passed_info['interaction']
|
||||||
members = list(role.members)
|
timedelta = parse(f'{duration} minutes')
|
||||||
for member in members:
|
edit_embed = discord.Embed(title="Shortmute confirmed!", description=f"Moderator: {old_interaction.user.mention}\nTarget: {target.mention}\nDuration: `{readable_duration}`\nReason: `{reason}`", color=await self.bot.get_embed_color(None))
|
||||||
|
if self.passed_info.get('evidence'):
|
||||||
|
evidence = self.passed_info['evidence']
|
||||||
|
edit_embed.set_image(evidence)
|
||||||
|
old_message = await old_interaction.edit_original_response(embed=edit_embed, view=None)
|
||||||
|
await target.timeout(until=timedelta, reason=f"User shortmuted for {readable_duration} by {old_interaction.user.name} ({old_interaction.user.id}) for: {reason}")
|
||||||
|
await interaction.response.send_message(content=f"{target.mention} shortmuted for {readable_duration} by {old_interaction.user.mention} for: `{reason}`")
|
||||||
|
if await self.config.guild(old_interaction.guild_id).dm() is True:
|
||||||
|
dm_embed = discord.Embed(title=f"You've been shortmuted in {old_interaction.guild.name}!", description=f"Moderator: {old_interaction.user.mention}\nTarget: {target.mention}\nDuration: `{readable_duration}`\nReason: `{reason}`", color=await self.bot.get_embed_color(None))
|
||||||
|
if evidence:
|
||||||
|
dm_embed.set_image(evidence)
|
||||||
try:
|
try:
|
||||||
await self.timeout_user(ctx, member, time, reason)
|
await target.send(embed=dm_embed)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException as error:
|
||||||
failed.append(member)
|
await old_message.edit(content="Could not message the target, user most likely has Direct Messages disabled.")
|
||||||
return failed
|
|
||||||
|
|
||||||
@commands.command(aliases=["tt"])
|
@ui.button(label="No", style=discord.ButtonStyle.danger)
|
||||||
@commands.guild_only()
|
async def shortmute_button_no(self, button: ui.Button, interaction: discord.Interaction):
|
||||||
@commands.cooldown(1, 1, commands.BucketType.user)
|
message = await self.passed_info['interaction'].edit_original_response(content="Command cancelled.", view=None, embed=None)
|
||||||
@commands.mod_or_permissions(administrator=True)
|
await message.delete(delay=3)
|
||||||
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.command()
|
||||||
@commands.guild_only()
|
async def shortmute_dm(self, ctx: commands.Context, enabled: bool = None):
|
||||||
@commands.cooldown(1, 1, commands.BucketType.user)
|
"""This command changes if the `/shortmute` slash command Direct Messages its' target."""
|
||||||
@commands.mod_or_permissions(administrator=True)
|
old_value = await self.config.guild(ctx.guild.id).dm()
|
||||||
async def untimeout(
|
if enabled:
|
||||||
self,
|
await self.config.guild(ctx.guild.id).dm.set(enabled)
|
||||||
ctx: commands.Context,
|
await ctx.send(content=f"Shortmute Direct Message setting changed!\nOld value: `{old_value}`\nNew value: `{enabled}`")
|
||||||
member_or_role: Union[discord.Member, discord.Role],
|
elif old_value is True:
|
||||||
*,
|
await ctx.send(content="Shortmute Direct Messages are currently enabled!")
|
||||||
reason: Optional[str] = None,
|
elif old_value is False:
|
||||||
):
|
await ctx.send(content="Shortmute Direct Messages are currently disabled!")
|
||||||
"""
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
Loading…
Reference in a new issue