1
Fork 0
mirror of https://github.com/wlinator/luminara.git synced 2024-10-02 20:23:12 +00:00

chore: Add timeout and untimeout commands for user moderation

This commit is contained in:
wlinator 2024-08-03 03:51:34 -04:00
parent 9b43865310
commit 8d4c536846
6 changed files with 218 additions and 22 deletions

View file

@ -97,6 +97,15 @@
"mod_warned_author": "User Warned",
"mod_warn_dm": "**{0}** you have been warned in `{1}`.\n\n**Reason:** `{2}`",
"mod_warned_user": "user `{0}` has been warned.",
"mod_timed_out_author": "User Timed Out",
"mod_timeout_dm": "**{0}** you have been timed out in `{1}` for `{2}`.\n\n**Reason:** `{3}`",
"mod_timed_out_user": "user `{0}` has been timed out.",
"mod_untimed_out_author": "User Timeout Removed",
"mod_untimed_out": "timeout has been removed for user `{0}`.",
"mod_not_timed_out_author": "User Not Timed Out",
"mod_not_timed_out": "user `{0}` is not timed out.",
"error_invalid_duration_author": "Invalid Duration",
"error_invalid_duration_description": "Please provide a valid duration between 1 minute and 30 days.",
"ping_author": "I'm online!",
"ping_footer": "Latency: {0}ms",
"ping_pong": "Pong!",
@ -118,5 +127,6 @@
"xp_lb_field_value": "level: **{0}**\nxp: `{1}/{2}`",
"xp_level": "Level {0}",
"xp_progress": "Progress to next level",
"xp_server_rank": "Server Rank: #{0}"
"xp_server_rank": "Server Rank: #{0}",
"error_invalid_duration": "Invalid duration: {0}"
}

View file

@ -2,6 +2,9 @@ import textwrap
import discord
from discord.ext import commands
from pytimeparse import parse
from lib.exceptions.LumiExceptions import LumiException
from lib.constants import CONST
from services.config_service import GuildConfig
@ -101,3 +104,35 @@ def get_invoked_name(ctx: commands.Context) -> str | None:
return ctx.invoked_with
except (discord.ApplicationCommandInvokeError, AttributeError):
return ctx.command.name if ctx.command else None
def format_duration_to_seconds(duration: str) -> int:
"""
Formats a duration in seconds to a human-readable string.
"""
parsed_duration = parse(duration)
if isinstance(parsed_duration, int):
return parsed_duration
else:
raise LumiException(CONST.STRINGS["error_invalid_duration"].format(duration))
def format_seconds_to_duration_string(seconds: int) -> str:
"""
Formats a duration in seconds to a human-readable string.
Returns seconds if shorter than a minute.
"""
if seconds < 60:
return f"{seconds}s"
days = seconds // 86400
hours = (seconds % 86400) // 3600
minutes = (seconds % 3600) // 60
if days > 0:
return f"{days}d{hours}h" if hours > 0 else f"{days}d"
elif hours > 0:
return f"{hours}h{minutes}m" if minutes > 0 else f"{hours}h"
else:
return f"{minutes}m"

View file

@ -1,7 +1,7 @@
import discord
from discord.ext import bridge, commands
from modules.moderation import ban, cases, warn
from modules.moderation import ban, cases, warn, timeout
class Moderation(commands.Cog):
@ -126,6 +126,43 @@ class Moderation(commands.Cog):
):
await warn.warn_user(ctx, target, reason)
@bridge.bridge_command(
name="timeout",
aliases=["t", "to"],
description="Timeout a user.",
help="Timeouts a user in the server for a specified duration.",
guild_only=True,
)
@bridge.has_permissions(moderate_members=True)
@commands.guild_only()
async def timeout_command(
self,
ctx,
target: discord.Member,
duration: str,
*,
reason: str | None = None,
):
await timeout.timeout_user(self, ctx, target, duration, reason)
@bridge.bridge_command(
name="untimeout",
aliases=["removetimeout", "rto", "uto"],
description="Remove timeout from a user.",
help="Removes the timeout from a user in the server.",
guild_only=True,
)
@bridge.has_permissions(moderate_members=True)
@commands.guild_only()
async def untimeout_command(
self,
ctx,
target: discord.Member,
*,
reason: str | None = None,
):
await timeout.untimeout_user(ctx, target, reason)
def setup(client):
client.add_cog(Moderation(client))

View file

@ -0,0 +1,102 @@
import asyncio
import discord
import datetime
from lib import formatter
from lib.constants import CONST
from lib.embed_builder import EmbedBuilder
from modules.moderation.utils.actionable import async_actionable
from modules.moderation.utils.case_handler import create_case
from typing import Optional
from discord.ext.commands import UserConverter
from lib.formatter import format_duration_to_seconds, format_seconds_to_duration_string
async def timeout_user(
cog,
ctx,
target: discord.Member,
duration: str,
reason: Optional[str] = None,
):
bot_member = await cog.client.get_or_fetch_member(ctx.guild, ctx.bot.user.id)
await async_actionable(target, ctx.author, bot_member)
output_reason = reason or CONST.STRINGS["mod_no_reason"]
# Parse duration to minutes and validate
duration_int = format_duration_to_seconds(duration)
duration_str = format_seconds_to_duration_string(duration_int)
await target.timeout_for(
duration=datetime.timedelta(seconds=duration_int),
reason=CONST.STRINGS["mod_reason"].format(
ctx.author.name,
formatter.shorten(output_reason, 200),
),
)
dm_task = target.send(
embed=EmbedBuilder.create_warning_embed(
ctx,
author_text=CONST.STRINGS["mod_timed_out_author"],
description=CONST.STRINGS["mod_timeout_dm"].format(
target.name,
ctx.guild.name,
duration_str,
output_reason,
),
show_name=False,
),
)
respond_task = ctx.respond(
embed=EmbedBuilder.create_success_embed(
ctx,
author_text=CONST.STRINGS["mod_timed_out_author"],
description=CONST.STRINGS["mod_timed_out_user"].format(target.name),
),
)
target_user = await UserConverter().convert(ctx, str(target.id))
create_case_task = create_case(ctx, target_user, "TIMEOUT", reason, duration_int)
await asyncio.gather(
dm_task,
respond_task,
create_case_task,
return_exceptions=True,
)
async def untimeout_user(ctx, target: discord.Member, reason: Optional[str] = None):
output_reason = reason or CONST.STRINGS["mod_no_reason"]
try:
await target.remove_timeout(
reason=CONST.STRINGS["mod_reason"].format(
ctx.author.name,
formatter.shorten(output_reason, 200),
),
)
respond_task = ctx.respond(
embed=EmbedBuilder.create_success_embed(
ctx,
author_text=CONST.STRINGS["mod_untimed_out_author"],
description=CONST.STRINGS["mod_untimed_out"].format(target.name),
),
)
target_user = await UserConverter().convert(ctx, str(target.id))
create_case_task = create_case(ctx, target_user, "UNTIMEOUT", reason)
await asyncio.gather(respond_task, create_case_task)
except discord.HTTPException:
return await ctx.respond(
embed=EmbedBuilder.create_warning_embed(
ctx,
author_text=CONST.STRINGS["mod_not_timed_out_author"],
description=CONST.STRINGS["mod_not_timed_out"].format(target.name),
),
)

51
poetry.lock generated
View file

@ -797,6 +797,17 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pytimeparse"
version = "1.1.8"
description = "Time expression parser"
optional = false
python-versions = "*"
files = [
{file = "pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd"},
{file = "pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a"},
]
[[package]]
name = "pytz"
version = "2024.1"
@ -891,29 +902,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.5.5"
version = "0.5.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"},
{file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"},
{file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
{file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"},
{file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"},
{file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"},
{file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"},
{file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"},
{file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"},
{file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"},
{file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
{file = "ruff-0.5.6-py3-none-linux_armv6l.whl", hash = "sha256:a0ef5930799a05522985b9cec8290b185952f3fcd86c1772c3bdbd732667fdcd"},
{file = "ruff-0.5.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b652dc14f6ef5d1552821e006f747802cc32d98d5509349e168f6bf0ee9f8f42"},
{file = "ruff-0.5.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80521b88d26a45e871f31e4b88938fd87db7011bb961d8afd2664982dfc3641a"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9bc8f328a9f1309ae80e4d392836e7dbc77303b38ed4a7112699e63d3b066ab"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d394940f61f7720ad371ddedf14722ee1d6250fd8d020f5ea5a86e7be217daf"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111a99cdb02f69ddb2571e2756e017a1496c2c3a2aeefe7b988ddab38b416d36"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e395daba77a79f6dc0d07311f94cc0560375ca20c06f354c7c99af3bf4560c5d"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c476acb43c3c51e3c614a2e878ee1589655fa02dab19fe2db0423a06d6a5b1b6"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2ff8003f5252fd68425fd53d27c1f08b201d7ed714bb31a55c9ac1d4c13e2eb"},
{file = "ruff-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c94e084ba3eaa80c2172918c2ca2eb2230c3f15925f4ed8b6297260c6ef179ad"},
{file = "ruff-0.5.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f77c1c3aa0669fb230b06fb24ffa3e879391a3ba3f15e3d633a752da5a3e670"},
{file = "ruff-0.5.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f908148c93c02873210a52cad75a6eda856b2cbb72250370ce3afef6fb99b1ed"},
{file = "ruff-0.5.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:563a7ae61ad284187d3071d9041c08019975693ff655438d8d4be26e492760bd"},
{file = "ruff-0.5.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:94fe60869bfbf0521e04fd62b74cbca21cbc5beb67cbb75ab33fe8c174f54414"},
{file = "ruff-0.5.6-py3-none-win32.whl", hash = "sha256:e6a584c1de6f8591c2570e171cc7ce482bb983d49c70ddf014393cd39e9dfaed"},
{file = "ruff-0.5.6-py3-none-win_amd64.whl", hash = "sha256:d7fe7dccb1a89dc66785d7aa0ac283b2269712d8ed19c63af908fdccca5ccc1a"},
{file = "ruff-0.5.6-py3-none-win_arm64.whl", hash = "sha256:57c6c0dd997b31b536bff49b9eee5ed3194d60605a4427f735eeb1f9c1b8d264"},
{file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"},
]
[[package]]
@ -1111,4 +1122,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "f99a624c8fbf0661b2dfe3a937f1182c527ed3d565715bb868b1cbac95221405"
content-hash = "9288a5bc8e1fdb08faf1bc44752ed3adb05e225879e142cc0bf2bbce2436fa5c"

View file

@ -20,6 +20,7 @@ python = "^3.12"
python-dotenv = "^1.0.1"
pytz = "^2024.1"
ruff = "^0.5.2"
pytimeparse = "^1.1.8"
[build-system]
build-backend = "poetry.core.masonry.api"