diff --git a/shortmute/info.json b/shortmute/info.json index 690cc92..73cdea8 100644 --- a/shortmute/info.json +++ b/shortmute/info.json @@ -2,8 +2,9 @@ "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", "name" : "Shortmute", - "short" : "Allows staff members to shortmute individuals for up to 60 minutes.", - "description" : "Allows staff members to shortmute individuals for up to 60 minutes, using Discord's Timeouts feature.", + "short" : "Allows staff members to shortmute individuals for up to 30 minutes.", + "description" : "Allows staff members to shortmute individuals for up to 30 minutes, using Discord's Timeouts feature.", "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"] } diff --git a/shortmute/shortmute.py b/shortmute/shortmute.py index b097a5f..0a7922e 100644 --- a/shortmute/shortmute.py +++ b/shortmute/shortmute.py @@ -1,237 +1,119 @@ -import contextlib -import datetime -from typing import List, Literal, Optional, Union - +from datetime import datetime 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"] +from pytimeparse2 import disable_dateutil, parse +from discord import ui +from redbot.core import Config, commands, app_commands, modlog 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.""" + """Allows staff members to shortmute individuals for up to 30 minutes, using Discord's Timeouts feature.""" - def __init__(self, bot: Red) -> None: + def __init__(self, bot) -> None: 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( 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__}" + @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. - 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 + 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 + """ + passed_info = { + "target": target, + "duration": duration, + "reason": reason, + "interaction": interaction } + 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) - 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) + class ShortmuteButtons(ui.View): + def __init__(self, timeout, passed_info: dict): + super().__init__() + self.timeout = timeout + self.passed_info = passed_info + self.config = Config.get_conf(None, cog_name='Shortmute', identifier=25781647388294) - 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 + @ui.button(label="Yes", style=discord.ButtonStyle.success) + async def shortmute_button_yes(self, button: ui.Button, interaction: discord.Interaction): + disable_dateutil() + target = self.passed_info['target'] + duration = self.passed_info['duration'] + readable_duration = self.passed_info['readable_duration'] + reason = self.passed_info['reason'] + old_interaction = self.passed_info['interaction'] + timedelta = parse(f'{duration} minutes') + 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: + await target.send(embed=dm_embed) + except discord.HTTPException as error: + await old_message.edit(content="Could not message the target, user most likely has Direct Messages disabled.") - @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. - `` 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 ." - ) - if isinstance(member_or_role, discord.Role): - await ctx.send( - f"Timeing out {len(member_or_role.members)} members till ." - ) - failed = await self.timeout_role(ctx, member_or_role, time, reason) - return await ctx.send(f"Failed to timeout {len(failed)} members.") + @ui.button(label="No", style=discord.ButtonStyle.danger) + async def shortmute_button_no(self, button: ui.Button, interaction: discord.Interaction): + message = await self.passed_info['interaction'].edit_original_response(content="Command cancelled.", view=None, embed=None) + await message.delete(delay=3) - @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. - `` 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) - ) \ No newline at end of file + @commands.command() + async def shortmute_dm(self, ctx: commands.Context, enabled: bool = None): + """This command changes if the `/shortmute` slash command Direct Messages its' target.""" + old_value = await self.config.guild(ctx.guild.id).dm() + if enabled: + await self.config.guild(ctx.guild.id).dm.set(enabled) + await ctx.send(content=f"Shortmute Direct Message setting changed!\nOld value: `{old_value}`\nNew value: `{enabled}`") + elif old_value is True: + await ctx.send(content="Shortmute Direct Messages are currently enabled!") + elif old_value is False: + await ctx.send(content="Shortmute Direct Messages are currently disabled!")