mirror of
https://github.com/wlinator/luminara.git
synced 2024-10-02 18:03:12 +00:00
commit
aa1f446478
10 changed files with 249 additions and 26 deletions
|
@ -8,6 +8,7 @@
|
|||
"case_reason_field": "Reason:",
|
||||
"case_case_field_value": "`{0}`",
|
||||
"case_type_field_value": "`{0}`",
|
||||
"case_type_field_value_with_duration": "`{0} ({1})`",
|
||||
"case_moderator_field_value": "`{0}`",
|
||||
"case_target_field_value": "`{0}` 🎯",
|
||||
"case_reason_field_value": "`{0}`",
|
||||
|
@ -19,6 +20,8 @@
|
|||
"case_mod_cases_author": "Cases by Moderator ({0})",
|
||||
"case_reason_update_author": "Case Reason Updated",
|
||||
"case_reason_update_description": "case `{0}` reason has been updated.",
|
||||
"case_duration_field": "Duration:",
|
||||
"case_duration_field_value": "`{0}`",
|
||||
"daily_already_claimed_author": "Already Claimed",
|
||||
"daily_already_claimed_description": "you can claim your daily reward again <t:{0}:R>.",
|
||||
"daily_already_claimed_footer": "Daily reset is at 7 AM EST",
|
||||
|
@ -97,6 +100,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 +130,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}"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class Constants:
|
|||
TITLE = "Luminara"
|
||||
AUTHOR = "wlinator"
|
||||
LICENSE = "GNU General Public License v3.0"
|
||||
VERSION = "2.8.2" # "Moderation: Warns" update
|
||||
VERSION = "2.8.3" # "Moderation: Timeouts" update
|
||||
|
||||
# bot credentials
|
||||
TOKEN: Optional[str] = os.environ.get("TOKEN", None)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -27,13 +27,14 @@ async def view_case_by_number(ctx, guild_id: int, case_number: int):
|
|||
return await ctx.respond(embed=embed)
|
||||
|
||||
target = await UserConverter().convert(ctx, str(case["target_id"]))
|
||||
embed = create_case_embed(
|
||||
ctx,
|
||||
target,
|
||||
case["case_number"],
|
||||
case["action_type"],
|
||||
case["reason"],
|
||||
case["created_at"],
|
||||
embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
case_number=case["case_number"],
|
||||
action_type=case["action_type"],
|
||||
reason=case["reason"],
|
||||
timestamp=case["created_at"],
|
||||
duration=case["duration"] or None,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
|
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, return_exceptions=True)
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
|
@ -4,6 +4,7 @@ from lib.constants import CONST
|
|||
from lib.formatter import format_case_number
|
||||
from typing import Optional
|
||||
import datetime
|
||||
from lib.formatter import format_seconds_to_duration_string
|
||||
|
||||
|
||||
def create_case_embed(
|
||||
|
@ -13,6 +14,7 @@ def create_case_embed(
|
|||
action_type: str,
|
||||
reason: Optional[str],
|
||||
timestamp: Optional[datetime.datetime] = None,
|
||||
duration: Optional[int] = None,
|
||||
) -> discord.Embed:
|
||||
embed = EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
|
@ -28,13 +30,25 @@ def create_case_embed(
|
|||
),
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_type_field"],
|
||||
value=CONST.STRINGS["case_type_field_value"].format(
|
||||
action_type.lower().capitalize(),
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
|
||||
if not duration:
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_type_field"],
|
||||
value=CONST.STRINGS["case_type_field_value"].format(
|
||||
action_type.lower().capitalize(),
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_type_field"],
|
||||
value=CONST.STRINGS["case_type_field_value_with_duration"].format(
|
||||
action_type.lower().capitalize(),
|
||||
format_seconds_to_duration_string(duration),
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_moderator_field"],
|
||||
value=CONST.STRINGS["case_moderator_field_value"].format(
|
||||
|
|
|
@ -65,7 +65,15 @@ async def create_case(
|
|||
ctx,
|
||||
str(mod_log_channel_id),
|
||||
)
|
||||
embed = create_case_embed(ctx, target, case_number, action_type, reason)
|
||||
embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
case_number=case_number,
|
||||
action_type=action_type,
|
||||
reason=reason,
|
||||
timestamp=None,
|
||||
duration=duration,
|
||||
)
|
||||
message = await mod_log_channel.send(embed=embed)
|
||||
|
||||
# Update the case with the modlog_message_id
|
||||
|
@ -118,13 +126,14 @@ async def edit_case_modlog(
|
|||
message = await mod_log_channel.fetch_message(modlog_message_id)
|
||||
target = await UserConverter().convert(ctx, str(case["target_id"]))
|
||||
|
||||
updated_embed = create_case_embed(
|
||||
ctx,
|
||||
target,
|
||||
case_number,
|
||||
case["action_type"],
|
||||
new_reason,
|
||||
case["created_at"],
|
||||
updated_embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
case_number=case_number,
|
||||
action_type=case["action_type"],
|
||||
reason=new_reason,
|
||||
timestamp=case["created_at"],
|
||||
duration=case["duration"] or None,
|
||||
)
|
||||
|
||||
await message.edit(embed=updated_embed)
|
||||
|
|
13
poetry.lock
generated
13
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"
|
||||
|
@ -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