Remove Red's built-in contact and dm commands in setup() before the Support cog loads to prevent command registration conflicts. Commands are stored and restored in teardown() when the cog unloads. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
600 lines
27 KiB
Python
600 lines
27 KiB
Python
"""Support — Modernized DM/contact system with categories, channels, and customization.
|
||
|
||
Overrides Red's built-in `contact` and `dm` commands with a more feature-rich system.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import datetime
|
||
from typing import Optional, Literal
|
||
|
||
import discord
|
||
from redbot.core import Config, commands
|
||
from redbot.core.bot import Red
|
||
from redbot.core.utils.chat_formatting import box, pagify
|
||
|
||
|
||
# ── Config Defaults ──────────────────────────────────────────────────────────
|
||
DEFAULT_GUILD = {
|
||
"enabled": False,
|
||
"categories": {}, # category_name -> {channel_id, description, emoji, roles, greeting}
|
||
"default_category": None,
|
||
"log_channel": None,
|
||
"mod_roles": [],
|
||
"reply_with_embed": True,
|
||
"show_author_info": True,
|
||
"anonymous_mode": False,
|
||
"thread_mode": False,
|
||
"dm_on_reply": True,
|
||
"active_threads": {}, # channel_id -> {user_id, category, started_at, message_count}
|
||
"thread_timeout": 3600, # Close thread after 1hr inactivity
|
||
"custom_greeting": "",
|
||
"require_category": False,
|
||
}
|
||
|
||
DEFAULT_USER = {
|
||
"blocked": False,
|
||
"preference_category": None,
|
||
"last_contact": 0,
|
||
}
|
||
|
||
|
||
# ── Utilities ────────────────────────────────────────────────────────────────
|
||
def success_embed(msg: str) -> discord.Embed:
|
||
return discord.Embed(description=f"✅ {msg}", color=0x43B581)
|
||
|
||
def error_embed(msg: str) -> discord.Embed:
|
||
return discord.Embed(description=f"❌ {msg}", color=0xF04747)
|
||
|
||
def info_embed(msg: str) -> discord.Embed:
|
||
return discord.Embed(description=f"ℹ️ {msg}", color=0x5865F2)
|
||
|
||
def timestamp_now() -> int:
|
||
return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||
|
||
|
||
# ── Views ────────────────────────────────────────────────────────────────────
|
||
class CategorySelectView(discord.ui.View):
|
||
"""Category selection dropdown for support messages."""
|
||
|
||
def __init__(self, categories: dict, timeout: float = 180):
|
||
super().__init__(timeout=timeout)
|
||
self.selected_category: Optional[str] = None
|
||
|
||
options = []
|
||
for name, data in list(categories.items())[:25]:
|
||
emoji = data.get("emoji", "📂")
|
||
description = data.get("description", "")[:100] or "Support category"
|
||
options.append(discord.SelectOption(
|
||
label=name.title(),
|
||
value=name,
|
||
description=description,
|
||
emoji=emoji,
|
||
))
|
||
|
||
select = discord.ui.Select(
|
||
placeholder="Choose a category...",
|
||
options=options,
|
||
custom_id="support_category_select"
|
||
)
|
||
select.callback = self.on_select
|
||
self.add_item(select)
|
||
|
||
async def on_select(self, interaction: discord.Interaction):
|
||
self.selected_category = interaction.data["values"][0]
|
||
await interaction.response.send_message(
|
||
embed=success_embed(f"Category selected: **{self.selected_category.title()}**"),
|
||
ephemeral=True
|
||
)
|
||
self.stop()
|
||
|
||
|
||
class SupportControlView(discord.ui.View):
|
||
"""Control buttons for staff managing support threads."""
|
||
|
||
def __init__(self, cog: "Support"):
|
||
super().__init__(timeout=None)
|
||
self.cog = cog
|
||
|
||
@discord.ui.button(label="Close Thread", style=discord.ButtonStyle.danger, emoji="🔒", custom_id="support_close")
|
||
async def close_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||
await self.cog._close_thread(interaction)
|
||
|
||
@discord.ui.button(label="Block User", style=discord.ButtonStyle.secondary, emoji="🚫", custom_id="support_block")
|
||
async def block_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||
await self.cog._block_user_from_thread(interaction)
|
||
|
||
|
||
class ReplyModal(discord.ui.Modal):
|
||
"""Modal for staff to reply to support messages."""
|
||
|
||
def __init__(self, cog: "Support", user: discord.User, category: str):
|
||
super().__init__(title=f"Reply to {user.name}")
|
||
self.cog = cog
|
||
self.user = user
|
||
self.category = category
|
||
|
||
self.message_input = discord.ui.TextInput(
|
||
label="Your Reply",
|
||
style=discord.TextStyle.paragraph,
|
||
placeholder="Type your response here...",
|
||
required=True,
|
||
max_length=1900,
|
||
)
|
||
self.add_item(self.message_input)
|
||
|
||
async def on_submit(self, interaction: discord.Interaction):
|
||
await interaction.response.defer(ephemeral=True)
|
||
await self.cog._send_reply_to_user(
|
||
interaction.guild,
|
||
interaction.user,
|
||
self.user,
|
||
self.message_input.value,
|
||
self.category
|
||
)
|
||
await interaction.followup.send(
|
||
embed=success_embed(f"Reply sent to {self.user.mention}"),
|
||
ephemeral=True
|
||
)
|
||
|
||
|
||
# ── Main Cog ─────────────────────────────────────────────────────────────────
|
||
class Support(commands.Cog):
|
||
"""Modernized support/contact system with categories and customization."""
|
||
|
||
def __init__(self, bot: Red):
|
||
self.bot = bot
|
||
self.config = Config.get_conf(self, identifier=900010, force_registration=True)
|
||
self.config.register_guild(**DEFAULT_GUILD)
|
||
self.config.register_user(**DEFAULT_USER)
|
||
|
||
self._control_view = SupportControlView(self)
|
||
self.bot.add_view(self._control_view)
|
||
|
||
self._cleanup_task: Optional[asyncio.Task] = None
|
||
|
||
async def cog_load(self):
|
||
"""Start background tasks on cog load."""
|
||
self._cleanup_task = asyncio.create_task(self._thread_cleanup_loop())
|
||
|
||
async def cog_unload(self):
|
||
"""Clean up on unload."""
|
||
if self._cleanup_task:
|
||
self._cleanup_task.cancel()
|
||
|
||
# ── Data Management ──────────────────────────────────────────────────────
|
||
async def red_delete_data_for_user(self, *, requester: Literal["discord", "owner", "user", "user_strict"], user_id: int):
|
||
"""Handle data deletion requests."""
|
||
await self.config.user_from_id(user_id).clear()
|
||
|
||
# ── User Commands ────────────────────────────────────────────────────────
|
||
@commands.command(name="contact")
|
||
@commands.dm_only()
|
||
async def contact(self, ctx: commands.Context, *, message: str):
|
||
"""Contact server staff with a message.
|
||
|
||
Usage: `[p]contact <message>`
|
||
This replaces Red's built-in contact command with enhanced features.
|
||
"""
|
||
user_conf = await self.config.user(ctx.author).all()
|
||
|
||
if user_conf["blocked"]:
|
||
return await ctx.send(embed=error_embed("You are blocked from contacting staff."))
|
||
|
||
guilds_data = await self.config.all_guilds()
|
||
available_guilds = []
|
||
|
||
for guild_id, gdata in guilds_data.items():
|
||
if not gdata["enabled"]:
|
||
continue
|
||
guild = self.bot.get_guild(guild_id)
|
||
if guild and guild.get_member(ctx.author.id):
|
||
available_guilds.append((guild, gdata))
|
||
|
||
if not available_guilds:
|
||
return await ctx.send(embed=error_embed("No servers have support enabled where you are a member."))
|
||
|
||
if len(available_guilds) == 1:
|
||
guild, gdata = available_guilds[0]
|
||
await self._process_contact(ctx, guild, gdata, message, user_conf)
|
||
else:
|
||
embed = discord.Embed(
|
||
title="Multiple Servers Available",
|
||
description="Please use the server's contact command directly or specify which server.",
|
||
color=0x5865F2
|
||
)
|
||
for guild, _ in available_guilds:
|
||
embed.add_field(name=guild.name, value=f"ID: `{guild.id}`", inline=False)
|
||
await ctx.send(embed=embed)
|
||
|
||
async def _process_contact(self, ctx: commands.Context, guild: discord.Guild, gdata: dict, message: str, user_conf: dict):
|
||
"""Process a contact message."""
|
||
categories = gdata["categories"]
|
||
selected_category = user_conf.get("preference_category") or gdata.get("default_category")
|
||
|
||
if gdata["require_category"] and not selected_category and categories:
|
||
view = CategorySelectView(categories)
|
||
prompt = await ctx.send(
|
||
embed=info_embed("Please select a category for your message:"),
|
||
view=view
|
||
)
|
||
await view.wait()
|
||
if view.selected_category:
|
||
selected_category = view.selected_category
|
||
try:
|
||
await prompt.delete()
|
||
except discord.HTTPException:
|
||
pass
|
||
|
||
category_data = categories.get(selected_category, {}) if selected_category else {}
|
||
channel_id = category_data.get("channel_id") or (list(categories.values())[0]["channel_id"] if categories else None)
|
||
|
||
if not channel_id:
|
||
return await ctx.send(embed=error_embed("No support channel configured. Contact an administrator."))
|
||
|
||
channel = guild.get_channel(channel_id)
|
||
if not channel:
|
||
return await ctx.send(embed=error_embed("Support channel not found. Contact an administrator."))
|
||
|
||
await self._send_to_staff(ctx.author, guild, channel, message, selected_category, gdata)
|
||
|
||
greeting = category_data.get("greeting") or gdata.get("custom_greeting") or \
|
||
"Your message has been sent to staff. They will respond shortly."
|
||
await ctx.send(embed=success_embed(greeting))
|
||
|
||
async with self.config.user(ctx.author).all() as user_data:
|
||
user_data["last_contact"] = timestamp_now()
|
||
|
||
@commands.command(name="dm")
|
||
@commands.guild_only()
|
||
@commands.admin_or_permissions(manage_guild=True)
|
||
async def dm_command(self, ctx: commands.Context, user: discord.User, *, message: str):
|
||
"""Send a DM to a user from the server.
|
||
|
||
Usage: `[p]dm <user> <message>`
|
||
This replaces Red's built-in dm command with enhanced features.
|
||
"""
|
||
gdata = await self.config.guild(ctx.guild).all()
|
||
|
||
if not gdata["enabled"]:
|
||
return await ctx.send(embed=error_embed("Support system is not enabled."))
|
||
|
||
try:
|
||
if gdata["reply_with_embed"]:
|
||
embed = discord.Embed(
|
||
title=f"Message from {ctx.guild.name}",
|
||
description=message,
|
||
color=0x5865F2,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc)
|
||
)
|
||
if gdata["show_author_info"] and not gdata["anonymous_mode"]:
|
||
embed.set_footer(text=f"From: {ctx.author}", icon_url=ctx.author.display_avatar.url)
|
||
else:
|
||
embed.set_footer(text=f"From: {ctx.guild.name} Staff")
|
||
await user.send(embed=embed)
|
||
else:
|
||
prefix = f"**Message from {ctx.guild.name}**\n" if gdata["anonymous_mode"] else \
|
||
f"**{ctx.author}** from **{ctx.guild.name}**:\n"
|
||
await user.send(f"{prefix}{message}")
|
||
|
||
await ctx.send(embed=success_embed(f"Message sent to {user.mention}"))
|
||
|
||
log_channel_id = gdata["log_channel"]
|
||
if log_channel_id:
|
||
log_channel = ctx.guild.get_channel(log_channel_id)
|
||
if log_channel:
|
||
log_embed = discord.Embed(
|
||
title="📤 Staff DM Sent",
|
||
color=0x43B581,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc)
|
||
)
|
||
log_embed.add_field(name="Staff", value=ctx.author.mention, inline=True)
|
||
log_embed.add_field(name="User", value=user.mention, inline=True)
|
||
log_embed.add_field(name="Message", value=message[:1024], inline=False)
|
||
await log_channel.send(embed=log_embed)
|
||
|
||
except discord.Forbidden:
|
||
await ctx.send(embed=error_embed(f"Cannot send DM to {user.mention}. They may have DMs disabled."))
|
||
except discord.HTTPException as e:
|
||
await ctx.send(embed=error_embed(f"Failed to send DM: {e}"))
|
||
|
||
# ── Admin Commands ───────────────────────────────────────────────────────
|
||
@commands.group(name="supportset", aliases=["sset"])
|
||
@commands.guild_only()
|
||
@commands.admin_or_permissions(manage_guild=True)
|
||
async def supportset(self, ctx: commands.Context):
|
||
"""Configure the support system."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@supportset.command(name="enable")
|
||
async def supportset_enable(self, ctx: commands.Context):
|
||
"""Enable the support system for this server."""
|
||
await self.config.guild(ctx.guild).enabled.set(True)
|
||
await ctx.send(embed=success_embed("Support system enabled!"))
|
||
|
||
@supportset.command(name="disable")
|
||
async def supportset_disable(self, ctx: commands.Context):
|
||
"""Disable the support system for this server."""
|
||
await self.config.guild(ctx.guild).enabled.set(False)
|
||
await ctx.send(embed=success_embed("Support system disabled."))
|
||
|
||
@supportset.command(name="addcategory", aliases=["addcat"])
|
||
async def supportset_addcategory(self, ctx: commands.Context, name: str, channel: discord.TextChannel, *, description: str = ""):
|
||
"""Add a support category.
|
||
|
||
Usage: `[p]supportset addcategory <name> <channel> [description]`
|
||
"""
|
||
async with self.config.guild(ctx.guild).categories() as categories:
|
||
categories[name.lower()] = {
|
||
"channel_id": channel.id,
|
||
"description": description,
|
||
"emoji": "📂",
|
||
"roles": [],
|
||
"greeting": "",
|
||
}
|
||
await ctx.send(embed=success_embed(f"Category **{name}** created with channel {channel.mention}"))
|
||
|
||
@supportset.command(name="removecategory", aliases=["removecat", "delcat"])
|
||
async def supportset_removecategory(self, ctx: commands.Context, name: str):
|
||
"""Remove a support category."""
|
||
async with self.config.guild(ctx.guild).categories() as categories:
|
||
if name.lower() in categories:
|
||
del categories[name.lower()]
|
||
await ctx.send(embed=success_embed(f"Category **{name}** removed."))
|
||
else:
|
||
await ctx.send(embed=error_embed(f"Category **{name}** not found."))
|
||
|
||
@supportset.command(name="setemoji")
|
||
async def supportset_setemoji(self, ctx: commands.Context, category: str, emoji: str):
|
||
"""Set an emoji for a category."""
|
||
async with self.config.guild(ctx.guild).categories() as categories:
|
||
if category.lower() not in categories:
|
||
return await ctx.send(embed=error_embed(f"Category **{category}** not found."))
|
||
categories[category.lower()]["emoji"] = emoji
|
||
await ctx.send(embed=success_embed(f"Emoji for **{category}** set to {emoji}"))
|
||
|
||
@supportset.command(name="setgreeting")
|
||
async def supportset_setgreeting(self, ctx: commands.Context, category: str, *, greeting: str):
|
||
"""Set a custom greeting for a category."""
|
||
async with self.config.guild(ctx.guild).categories() as categories:
|
||
if category.lower() not in categories:
|
||
return await ctx.send(embed=error_embed(f"Category **{category}** not found."))
|
||
categories[category.lower()]["greeting"] = greeting
|
||
await ctx.send(embed=success_embed(f"Greeting set for **{category}**"))
|
||
|
||
@supportset.command(name="defaultcategory", aliases=["defaultcat"])
|
||
async def supportset_defaultcategory(self, ctx: commands.Context, name: str):
|
||
"""Set the default category for support messages."""
|
||
categories = await self.config.guild(ctx.guild).categories()
|
||
if name.lower() not in categories:
|
||
return await ctx.send(embed=error_embed(f"Category **{name}** not found."))
|
||
await self.config.guild(ctx.guild).default_category.set(name.lower())
|
||
await ctx.send(embed=success_embed(f"Default category set to **{name}**"))
|
||
|
||
@supportset.command(name="logchannel")
|
||
async def supportset_logchannel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
|
||
"""Set or clear the log channel for support messages."""
|
||
if channel:
|
||
await self.config.guild(ctx.guild).log_channel.set(channel.id)
|
||
await ctx.send(embed=success_embed(f"Log channel set to {channel.mention}"))
|
||
else:
|
||
await self.config.guild(ctx.guild).log_channel.set(None)
|
||
await ctx.send(embed=success_embed("Log channel cleared."))
|
||
|
||
@supportset.command(name="anonymous")
|
||
async def supportset_anonymous(self, ctx: commands.Context, enabled: bool):
|
||
"""Toggle anonymous mode for staff replies."""
|
||
await self.config.guild(ctx.guild).anonymous_mode.set(enabled)
|
||
status = "enabled" if enabled else "disabled"
|
||
await ctx.send(embed=success_embed(f"Anonymous mode {status}."))
|
||
|
||
@supportset.command(name="requirecategory")
|
||
async def supportset_requirecategory(self, ctx: commands.Context, enabled: bool):
|
||
"""Toggle whether users must select a category."""
|
||
await self.config.guild(ctx.guild).require_category.set(enabled)
|
||
status = "enabled" if enabled else "disabled"
|
||
await ctx.send(embed=success_embed(f"Category requirement {status}."))
|
||
|
||
@supportset.command(name="embed")
|
||
async def supportset_embed(self, ctx: commands.Context, enabled: bool):
|
||
"""Toggle using embeds for replies."""
|
||
await self.config.guild(ctx.guild).reply_with_embed.set(enabled)
|
||
status = "enabled" if enabled else "disabled"
|
||
await ctx.send(embed=success_embed(f"Embed replies {status}."))
|
||
|
||
@supportset.command(name="showinfo")
|
||
async def supportset_showinfo(self, ctx: commands.Context, enabled: bool):
|
||
"""Toggle showing author info in replies."""
|
||
await self.config.guild(ctx.guild).show_author_info.set(enabled)
|
||
status = "enabled" if enabled else "disabled"
|
||
await ctx.send(embed=success_embed(f"Author info {status}."))
|
||
|
||
@supportset.command(name="list")
|
||
async def supportset_list(self, ctx: commands.Context):
|
||
"""List all support categories and settings."""
|
||
gdata = await self.config.guild(ctx.guild).all()
|
||
|
||
embed = discord.Embed(
|
||
title=f"Support System — {ctx.guild.name}",
|
||
color=0x5865F2,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc)
|
||
)
|
||
|
||
status = "✅ Enabled" if gdata["enabled"] else "❌ Disabled"
|
||
embed.add_field(name="Status", value=status, inline=True)
|
||
|
||
if gdata["default_category"]:
|
||
embed.add_field(name="Default Category", value=gdata["default_category"].title(), inline=True)
|
||
|
||
if gdata["log_channel"]:
|
||
log_ch = ctx.guild.get_channel(gdata["log_channel"])
|
||
embed.add_field(name="Log Channel", value=log_ch.mention if log_ch else "Not found", inline=True)
|
||
|
||
settings = []
|
||
if gdata["anonymous_mode"]:
|
||
settings.append("🔒 Anonymous Mode")
|
||
if gdata["require_category"]:
|
||
settings.append("📋 Require Category")
|
||
if gdata["reply_with_embed"]:
|
||
settings.append("📝 Embed Replies")
|
||
if gdata["show_author_info"]:
|
||
settings.append("👤 Show Author Info")
|
||
|
||
if settings:
|
||
embed.add_field(name="Settings", value="\n".join(settings), inline=False)
|
||
|
||
categories = gdata["categories"]
|
||
if categories:
|
||
cat_list = []
|
||
for name, data in categories.items():
|
||
channel = ctx.guild.get_channel(data["channel_id"])
|
||
emoji = data.get("emoji", "📂")
|
||
cat_list.append(f"{emoji} **{name.title()}** → {channel.mention if channel else 'Missing'}")
|
||
embed.add_field(name="Categories", value="\n".join(cat_list[:10]), inline=False)
|
||
else:
|
||
embed.add_field(name="Categories", value="None configured", inline=False)
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
@supportset.command(name="block")
|
||
async def supportset_block(self, ctx: commands.Context, user: discord.User):
|
||
"""Block a user from using the support system."""
|
||
await self.config.user(user).blocked.set(True)
|
||
await ctx.send(embed=success_embed(f"{user.mention} has been blocked from support."))
|
||
|
||
@supportset.command(name="unblock")
|
||
async def supportset_unblock(self, ctx: commands.Context, user: discord.User):
|
||
"""Unblock a user from using the support system."""
|
||
await self.config.user(user).blocked.set(False)
|
||
await ctx.send(embed=success_embed(f"{user.mention} has been unblocked from support."))
|
||
|
||
# ── Internal Methods ─────────────────────────────────────────────────────
|
||
async def _send_to_staff(self, user: discord.User, guild: discord.Guild, channel: discord.TextChannel,
|
||
message: str, category: Optional[str], gdata: dict):
|
||
"""Send a user's message to the staff channel."""
|
||
embed = discord.Embed(
|
||
title=f"📨 Support Message",
|
||
description=message,
|
||
color=0x5865F2,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc)
|
||
)
|
||
|
||
embed.set_author(name=f"{user} ({user.id})", icon_url=user.display_avatar.url)
|
||
|
||
if category:
|
||
embed.add_field(name="Category", value=category.title(), inline=True)
|
||
|
||
member = guild.get_member(user.id)
|
||
if member:
|
||
embed.add_field(name="Joined", value=f"<t:{int(member.joined_at.timestamp())}:R>", inline=True)
|
||
roles = [r.mention for r in member.roles if r != guild.default_role]
|
||
if roles:
|
||
embed.add_field(name="Roles", value=", ".join(roles[:5]), inline=False)
|
||
|
||
embed.set_footer(text=f"User ID: {user.id}")
|
||
|
||
await channel.send(embed=embed)
|
||
|
||
log_channel_id = gdata["log_channel"]
|
||
if log_channel_id and log_channel_id != channel.id:
|
||
log_channel = guild.get_channel(log_channel_id)
|
||
if log_channel:
|
||
log_embed = discord.Embed(
|
||
title="📥 Support Message Received",
|
||
color=0x5865F2,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc)
|
||
)
|
||
log_embed.add_field(name="User", value=user.mention, inline=True)
|
||
log_embed.add_field(name="Channel", value=channel.mention, inline=True)
|
||
if category:
|
||
log_embed.add_field(name="Category", value=category.title(), inline=True)
|
||
log_embed.add_field(name="Message", value=message[:1024], inline=False)
|
||
await log_channel.send(embed=log_embed)
|
||
|
||
async def _send_reply_to_user(self, guild: discord.Guild, staff_member: discord.Member,
|
||
user: discord.User, message: str, category: Optional[str]):
|
||
"""Send a staff reply to a user."""
|
||
gdata = await self.config.guild(guild).all()
|
||
|
||
try:
|
||
if gdata["reply_with_embed"]:
|
||
embed = discord.Embed(
|
||
title=f"Reply from {guild.name}",
|
||
description=message,
|
||
color=0x43B581,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc)
|
||
)
|
||
if gdata["show_author_info"] and not gdata["anonymous_mode"]:
|
||
embed.set_footer(text=f"From: {staff_member}", icon_url=staff_member.display_avatar.url)
|
||
else:
|
||
embed.set_footer(text=f"From: {guild.name} Staff")
|
||
await user.send(embed=embed)
|
||
else:
|
||
prefix = f"**Reply from {guild.name}**\n" if gdata["anonymous_mode"] else \
|
||
f"**{staff_member}** from **{guild.name}**:\n"
|
||
await user.send(f"{prefix}{message}")
|
||
except discord.Forbidden:
|
||
pass
|
||
|
||
async def _close_thread(self, interaction: discord.Interaction):
|
||
"""Close an active support thread."""
|
||
ch_id = str(interaction.channel.id)
|
||
async with self.config.guild(interaction.guild).active_threads() as threads:
|
||
if ch_id in threads:
|
||
del threads[ch_id]
|
||
await interaction.response.send_message(
|
||
embed=success_embed("Support thread closed."),
|
||
ephemeral=True
|
||
)
|
||
|
||
async def _block_user_from_thread(self, interaction: discord.Interaction):
|
||
"""Block a user from an active thread."""
|
||
ch_id = str(interaction.channel.id)
|
||
threads = await self.config.guild(interaction.guild).active_threads()
|
||
thread = threads.get(ch_id)
|
||
|
||
if not thread:
|
||
return await interaction.response.send_message(
|
||
embed=error_embed("No active thread in this channel."),
|
||
ephemeral=True
|
||
)
|
||
|
||
user_id = thread.get("user_id")
|
||
if user_id:
|
||
await self.config.user_from_id(user_id).blocked.set(True)
|
||
await interaction.response.send_message(
|
||
embed=success_embed(f"User <@{user_id}> blocked from support."),
|
||
ephemeral=True
|
||
)
|
||
|
||
async def _thread_cleanup_loop(self):
|
||
"""Background task to clean up inactive threads."""
|
||
await self.bot.wait_until_ready()
|
||
while True:
|
||
try:
|
||
await asyncio.sleep(300) # Check every 5 minutes
|
||
|
||
for guild in self.bot.guilds:
|
||
gdata = await self.config.guild(guild).all()
|
||
if not gdata["enabled"]:
|
||
continue
|
||
|
||
timeout = gdata["thread_timeout"]
|
||
now = timestamp_now()
|
||
|
||
async with self.config.guild(guild).active_threads() as threads:
|
||
to_remove = []
|
||
for ch_id, thread in threads.items():
|
||
started = thread.get("started_at", now)
|
||
if now - started > timeout:
|
||
to_remove.append(ch_id)
|
||
|
||
for ch_id in to_remove:
|
||
del threads[ch_id]
|
||
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception:
|
||
pass
|