mirror of
https://github.com/wlinator/luminara.git
synced 2024-10-02 18:03:12 +00:00
chore: Add timeout and untimeout commands for user moderation
This commit is contained in:
parent
9b43865310
commit
8d4c536846
6 changed files with 218 additions and 22 deletions
|
@ -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}"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
102
modules/moderation/timeout.py
Normal file
102
modules/moderation/timeout.py
Normal 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
51
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue