@@ -0,0 +1,218 @@
import discord
from redbot . core import commands , Config
from redbot . core . bot import Red
from typing import Optional
class ModMail ( commands . Cog ) :
"""
A configurable, forum-based ModMail system.
"""
def __init__ ( self , bot : Red ) :
self . bot = bot
# Initialize Red's Config system for storing settings per-server.
self . config = Config . get_conf ( self , identifier = 9876543210 , force_registration = True )
# Define the default settings for each server.
default_guild = {
" modmail_forum " : None , # The ID of the forum channel for tickets
" enabled " : False , # Whether the system is on or off
" active_threads " : { } # A dictionary to track {user_id: thread_id}
}
# Register the default settings.
self . config . register_guild ( * * default_guild )
@commands.Cog.listener ( )
async def on_message ( self , message : discord . Message ) :
"""
This is the core event listener. It handles both incoming DMs from users
and outgoing replies from staff.
"""
# Ignore messages from bots to prevent loops.
if message . author . bot :
return
# --- Part 1: Handling DMs from Users ---
if isinstance ( message . channel , discord . DMChannel ) :
await self . handle_dm ( message )
# --- Part 2: Handling Replies from Staff ---
elif isinstance ( message . channel , discord . Thread ) :
await self . handle_staff_reply ( message )
async def handle_dm ( self , message : discord . Message ) :
""" Handles messages sent directly to the bot. """
# Find a mutual server with the user.
guild = next ( ( g for g in self . bot . guilds if g . get_member ( message . author . id ) ) , None )
if not guild :
return
settings = await self . config . guild ( guild ) . all ( )
if not settings [ " enabled " ] or not settings [ " modmail_forum " ] :
return
forum_channel = guild . get_channel ( settings [ " modmail_forum " ] )
if not isinstance ( forum_channel , discord . ForumChannel ) :
return
active_threads = settings [ " active_threads " ]
user_id_str = str ( message . author . id )
# Check if the user already has an active thread.
if user_id_str in active_threads :
thread_id = active_threads [ user_id_str ]
thread = guild . get_thread ( thread_id )
if thread :
# Relay the message to the existing thread.
await thread . send ( f " ** { message . author . display_name } :** { message . content } " )
await message . add_reaction ( " ✅ " )
return
else :
# The thread was deleted, so we clean up our records.
async with self . config . guild ( guild ) . active_threads ( ) as threads :
del threads [ user_id_str ]
# Create a new thread for the user.
try :
thread_name = f " ModMail | { message . author . name } "
embed = discord . Embed (
title = f " New ModMail Thread " ,
description = f " **User:** { message . author . mention } (` { message . author . id } `) " ,
color = 0xadd8e6 # Light grey pastel blue
)
embed . add_field ( name = " Initial Message " , value = message . content , inline = False )
embed . set_footer ( text = " Staff can reply in this thread to send a message. " )
thread_with_message = await forum_channel . create_thread ( name = thread_name , embed = embed )
thread = thread_with_message . thread
async with self . config . guild ( guild ) . active_threads ( ) as threads :
threads [ user_id_str ] = thread . id
await message . channel . send ( " Your message has been received, and a ModMail ticket has been opened. Staff will be with you shortly. " )
await message . add_reaction ( " ✅ " )
except discord . Forbidden :
print ( f " ModMail: I don ' t have permission to create threads in { forum_channel . name } . " )
except Exception as e :
print ( f " ModMail: An unexpected error occurred: { e } " )
async def handle_staff_reply ( self , message : discord . Message ) :
""" Handles messages sent by staff inside a ModMail thread. """
guild = message . guild
if not guild :
return
active_threads = await self . config . guild ( guild ) . active_threads ( )
# Find which user this thread belongs to by checking our records.
thread_id_str = str ( message . channel . id )
user_id = None
for uid , tid in active_threads . items ( ) :
if str ( tid ) == thread_id_str :
user_id = int ( uid )
break
if not user_id :
# This is a regular thread, not a ModMail thread we're tracking.
return
user = guild . get_member ( user_id )
if not user :
# User might have left the server.
await message . channel . send ( " ⚠️ **Error:** Could not find the user. They may have left the server. " )
return
# Send the staff's message to the user's DMs.
try :
embed = discord . Embed (
description = message . content ,
color = 0xadd8e6 # Light grey pastel blue
)
embed . set_author ( name = " Staff Response " ) # Anonymize the staff member
await user . send ( embed = embed )
await message . add_reaction ( " 📨 " ) # Add a mail icon to show it was sent
except discord . Forbidden :
await message . channel . send ( " ⚠️ **Error:** I could not send a DM to this user. They may have DMs disabled. " )
except Exception as e :
await message . channel . send ( f " ⚠️ **Error:** An unexpected error occurred: { e } " )
# --- Settings and Management Commands ---
@commands.group ( aliases = [ " mmset " ] ) # type: ignore
@commands.guild_only ( )
@commands.admin_or_permissions ( manage_guild = True )
async def modmailset ( self , ctx : commands . Context ) :
"""
Configure the ModMail settings for this server.
"""
pass
@modmailset.command ( name = " forum " )
async def modmailset_forum ( self , ctx : commands . Context , channel : discord . ForumChannel ) :
""" Set the forum channel where ModMail tickets will be created. """
if not ctx . guild :
return
await self . config . guild ( ctx . guild ) . modmail_forum . set ( channel . id )
await ctx . send ( f " The ModMail forum has been set to { channel . mention } . " )
@modmailset.command ( name = " toggle " )
async def modmailset_toggle ( self , ctx : commands . Context ) :
""" Enable or disable the ModMail system on this server. """
if not ctx . guild :
return
current_status = await self . config . guild ( ctx . guild ) . enabled ( )
new_status = not current_status
await self . config . guild ( ctx . guild ) . enabled . set ( new_status )
status_text = " enabled " if new_status else " disabled "
await ctx . send ( f " The ModMail system has been { status_text } . " )
@modmailset.command ( name = " close " )
async def modmailset_close ( self , ctx : commands . Context , * , reason : Optional [ str ] = " No reason provided. " ) :
"""
Close the current ModMail thread.
You must run this command inside the thread you wish to close.
"""
if not ctx . guild or not isinstance ( ctx . channel , discord . Thread ) :
await ctx . send ( " This command can only be run inside a ModMail thread. " )
return
active_threads = await self . config . guild ( ctx . guild ) . active_threads ( )
thread_id_str = str ( ctx . channel . id )
user_id = None
for uid , tid in active_threads . items ( ) :
if str ( tid ) == thread_id_str :
user_id = int ( uid )
break
if not user_id :
await ctx . send ( " This does not appear to be an active ModMail thread. " )
return
# Clean up our records.
async with self . config . guild ( ctx . guild ) . active_threads ( ) as threads :
del threads [ str ( user_id ) ]
# Notify the user.
user = self . bot . get_user ( user_id )
if user :
try :
embed = discord . Embed (
title = " ModMail Ticket Closed " ,
description = f " Your ticket has been closed by staff. \n \n **Reason:** { reason } " ,
color = 0xadd8e6
)
await user . send ( embed = embed )
except discord . Forbidden :
pass # Can't notify user if DMs are closed
# Archive the thread.
await ctx . send ( f " Ticket closed by { ctx . author . mention } . Archiving thread... " )
await ctx . channel . edit ( archived = True , locked = True )
# This required function allows Red to load the cog.
async def setup ( bot : Red ) :
await bot . add_cog ( ModMail ( bot ) )