Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Issue #51 - Buttons for bans #109

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions src/helpers/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from src.helpers.duration import validate_duration
from src.helpers.responses import SimpleResponse
from src.helpers.schedule import schedule
from src.views.bandecisionview import BanDecisionView

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -86,7 +87,7 @@ async def ban_member(
delete_after=None
)

#DM member, before we ban, else we cannot dm since we do not share a guild
# DM member, before we ban, else we cannot dm since we do not share a guild
dm_banned_member = await _dm_banned_member(end_date, guild, member, reason)
# Try to actually ban the member from the guild
try:
Expand Down Expand Up @@ -141,15 +142,13 @@ async def ban_member(
title=f"Ban request #{ban_id}",
description=f"{author.name} would like to ban {member_name} until {end_date} (UTC). Reason: {reason}", )
embed.set_thumbnail(url=f"{settings.HTB_URL}/images/logo600.png")
embed.add_field(name="Approve duration:", value=f"/approve {ban_id}", inline=True)
embed.add_field(name="Change duration:", value=f"/dispute {ban_id} <duration>", inline=True)
embed.add_field(name="Deny and unban:", value=f"/deny {ban_id}", inline=True)
await guild.get_channel(settings.channels.SR_MOD).send(embed=embed)
view = BanDecisionView(ban_id, bot, guild, member, end_date, reason)
await guild.get_channel(settings.channels.SR_MOD).send(embed=embed, view=view)
return SimpleResponse(message=message)


async def _dm_banned_member(end_date, guild, member, reason) -> bool:
"""Send a message to the member about the ban"""
async def _dm_banned_member(end_date: str, guild: Guild, member: Member, reason: str) -> bool:
"""Send a message to the member about the ban."""
message = (f"You have been banned from {guild.name} until {end_date} (UTC). "
f"To appeal the ban, please reach out to an Administrator.\n"
f"Following is the reason given:\n>>> {reason}\n")
Expand Down
142 changes: 142 additions & 0 deletions src/views/bandecisionview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from datetime import datetime

import discord
from discord import Guild, Interaction, Member, User
from discord.ui import Button, InputText, Modal, View
from sqlalchemy import select

from src.bot import Bot
from src.core import settings
from src.database.models import Ban
from src.database.session import AsyncSessionLocal
from src.helpers.duration import validate_duration
from src.helpers.schedule import schedule


class BanDecisionView(View):
"""View for making it easier to make a decision on a ban duration. It has buttons for approving, denying, and disputing the ban duration."""

def __init__(self, ban_id: int, bot: Bot, guild: Guild, member: Member | User, end_date: str, reason: str):
super().__init__(timeout=None)
self.ban_id = ban_id
self.bot = bot
self.guild = guild
self.member = member
self.end_date = end_date
self.reason = reason

# Disable all buttons and update the message to reflect the decision

async def disable_buttons_and_update_message(self, interaction: Interaction, decision: str) -> None:
"""Disable all buttons and update the message to reflect the decision."""
# Disable all buttons
for item in self.children:
if isinstance(item, Button):
item.disabled = True

# Edit the original message to reflect the decision and who made it
admin_name = interaction.user.display_name
decision_message = f"{admin_name} has made a decision: **{decision}** for {self.member.display_name}."
await interaction.message.edit(content=decision_message, view=self)

@discord.ui.button(label="Approve duration", style=discord.ButtonStyle.success, custom_id="approve_button")
async def approve_button(self, button: Button, interaction: Interaction) -> None:
"""Approve the ban duration."""
await interaction.response.send_message(
f"Ban duration for {self.member.display_name} has been approved.", ephemeral=True
)
async with AsyncSessionLocal() as session:
stmt = select(Ban).filter(Ban.id == self.ban_id)
result = await session.scalars(stmt)
ban = result.first()
if ban:
ban.approved = True
await session.commit()
await self.guild.get_channel(settings.channels.SR_MOD).send(
f"Ban duration for {self.member.display_name} has been approved by {interaction.user.display_name}."
)
# Disable buttons and update message after approval
await self.disable_buttons_and_update_message(interaction, "Approved Duration")

@discord.ui.button(label="Deny and unban", style=discord.ButtonStyle.danger, custom_id="deny_button")
async def deny_button(self, button: Button, interaction: Interaction) -> None:
"""Deny the ban duration and unban the member."""
from src.helpers.ban import unban_member
await interaction.response.send_message(
f"Ban for {self.member.display_name} has been denied and the member will be unbanned.", ephemeral=True
)
await unban_member(self.guild, self.member)
await self.guild.get_channel(settings.channels.SR_MOD).send(
f"Ban for {self.member.display_name} has been denied by {interaction.user.display_name} and the member has been unbanned."
)
# Disable buttons and update message after denial
await self.disable_buttons_and_update_message(interaction, "Denied and Unbanned")

@discord.ui.button(label="Dispute", style=discord.ButtonStyle.primary, custom_id="dispute_button")
async def dispute_button(self, button: Button, interaction: Interaction) -> None:
"""Dispute the ban duration."""
# Pass self as the parent view to DisputeModal for updating after completion
modal = DisputeModal(self.ban_id, self.bot, self.guild, self.member, self.end_date, self.reason, self)
await interaction.response.send_modal(modal)


class DisputeModal(Modal):
"""Modal for disputing a ban duration."""

def __init__(self, ban_id: int, bot: Bot, guild: Guild, member: Member | User, end_date: str, reason: str, parent_view: BanDecisionView):
super().__init__(title="Dispute Ban Duration")
self.ban_id = ban_id
self.bot = bot
self.guild = guild
self.member = member
self.end_date = end_date
self.reason = reason
self.parent_view = parent_view # Store the parent view

# Add InputText for duration
self.add_item(
InputText(label="New Duration", placeholder="Enter new duration (e.g., 10s, 5m, 2h, 1d)", required=True)
)

async def callback(self, interaction: Interaction) -> None:
"""Handle the dispute duration callback."""
from src.helpers.ban import unban_member
new_duration_str = self.children[0].value

# Validate duration using `validate_duration`
dur, dur_exc = validate_duration(new_duration_str)
if dur_exc:
# Send an ephemeral message if the duration is invalid
await interaction.response.send_message(dur_exc, ephemeral=True)
return

# Proceed with updating the ban record if the duration is valid
async with AsyncSessionLocal() as session:
ban = await session.get(Ban, self.ban_id)

if not ban or not ban.timestamp:
await interaction.response.send_message(f"Cannot dispute ban {self.ban_id}: record not found.", ephemeral=True)
return

# Update the ban's unban time and approve the dispute
ban.unban_time = dur
ban.approved = True
await session.commit()

# Schedule the unban based on the new duration
new_unban_at = datetime.fromtimestamp(dur)
member = await self.bot.get_member_or_user(self.guild, ban.user_id)
if member:
self.bot.loop.create_task(schedule(unban_member(self.guild, member), run_at=new_unban_at))

# Notify the user and moderators of the updated ban duration
await interaction.response.send_message(
f"Ban duration updated to {new_duration_str}. The member will be unbanned on {new_unban_at.strftime('%B %d, %Y')} UTC.",
ephemeral=True
)
await self.guild.get_channel(settings.channels.SR_MOD).send(
f"Ban duration for {self.member.display_name} updated to {new_duration_str}. Unban scheduled for {new_unban_at.strftime('%B %d, %Y')} UTC."
)

# Disable buttons and update message on the parent view after dispute
await self.parent_view.disable_buttons_and_update_message(interaction, "Disputed Duration")