import discord import asyncio from redbot.core import commands, Config, app_commands from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from redbot.core.bot import Red # --- UI Components --- class AdSubmissionModal(discord.ui.Modal, title="MORS Ad Submission"): ad_text = discord.ui.TextInput( label="Your Server Advertisement", style=discord.TextStyle.paragraph, placeholder="Please enter the full text of your ad here.", required=True, max_length=2000, ) ad_link = discord.ui.TextInput( label="Your Server Invite Link", placeholder="https://discord.gg/your-invite", required=True, max_length=100, ) def __init__(self, cog: "MORS"): super().__init__() self.cog = cog async def on_submit(self, interaction: discord.Interaction): guild = interaction.guild if not guild: await interaction.response.send_message("Something went wrong.", ephemeral=True) return conf = self.cog.config.guild(guild) ad_channel_id = await conf.ad_channel() if not ad_channel_id: await interaction.response.send_message( "The MORS system has not been fully configured by an administrator.", ephemeral=True ) return ad_channel = guild.get_channel(ad_channel_id) if not isinstance(ad_channel, discord.TextChannel): await interaction.response.send_message( "The configured ad channel is invalid. Please contact an administrator.", ephemeral=True ) return try: thread = await ad_channel.create_thread( name=f"mors-ticket-{interaction.user.name}", type=discord.ChannelType.private_thread ) await thread.add_user(interaction.user) except (discord.Forbidden, discord.HTTPException): await interaction.response.send_message( "I don't have permissions to create private threads in the ad channel.", ephemeral=True ) return await thread.send(f"Welcome {interaction.user.mention}! Your MORS ticket has been created and is awaiting admin approval.") await thread.send(f"For easy access on mobile, your thread ID is: `{thread.id}`") async with conf.active_tickets() as active_tickets: active_tickets[str(interaction.user.id)] = { "thread_id": thread.id, "ad_text": self.ad_text.value, "ad_link": self.ad_link.value, "status": "pending_approval" } await interaction.response.send_message("Your submission has been received and is awaiting approval!", ephemeral=True) class TimeSelect(discord.ui.Select): def __init__(self, cog: "MORS"): self.cog = cog options = [ discord.SelectOption(label="30 Minutes", value="30m"), discord.SelectOption(label="1 Hour", value="1h"), discord.SelectOption(label="2 Hours", value="2h"), discord.SelectOption(label="3 Hours", value="3h"), discord.SelectOption(label="4 Hours (Bypass Only)", value="4h"), discord.SelectOption(label="Once a Week (Bypass Only)", value="ovn"), ] super().__init__(placeholder="Select the advertisement duration...", min_values=1, max_values=1, options=options) async def callback(self, interaction: discord.Interaction): guild = interaction.guild if not guild or not isinstance(interaction.user, discord.Member): await interaction.response.send_message("An error occurred.", ephemeral=True) return member = interaction.user selected_time = self.values[0] conf = self.cog.config.guild(guild) # Logic for bypass role and cooldowns will be added here bypass_role_id = await conf.bypass_role() has_bypass = False if bypass_role_id: bypass_role = guild.get_role(bypass_role_id) if bypass_role and bypass_role in member.roles: has_bypass = True if selected_time in ["4h", "ovn"] and not has_bypass: await interaction.response.send_message("You do not have the required role for this duration.", ephemeral=True) return async with conf.active_tickets() as active_tickets: user_id_str = str(interaction.user.id) if user_id_str not in active_tickets: await interaction.response.send_message("You don't have an active MORS ticket.", ephemeral=True) return thread_id = active_tickets[user_id_str]["thread_id"] thread = guild.get_thread(thread_id) if not thread: await interaction.response.send_message("Could not find your ticket thread.", ephemeral=True) del active_tickets[user_id_str] return try: await thread.edit(name=f"n2p-{selected_time}-{interaction.user.name}") # Logic to add the role will go here once the role ID is configured except discord.Forbidden: await interaction.response.send_message("I don't have permission to rename your ticket thread.", ephemeral=True) return # Send confirmation messages queue_channel_id = await conf.queue_channel() if queue_channel_id: queue_channel = guild.get_channel(queue_channel_id) if isinstance(queue_channel, discord.TextChannel): queue_link = f"https://discord.com/channels/{guild.id}/{queue_channel.id}" await queue_channel.send(f"{thread.mention} :mooncatblue: {interaction.user.mention} queued for __{selected_time}__ *!*") await thread.send(f":81407babybluebutterfly: you're in the [queue]({queue_link}) *!* \n-# **p.s using invites command is __required__ for sep to state invites. no reply to pings within 3 days or inv cmnd done will get you suspended from massing.**") await interaction.response.send_message(f"You have selected: {selected_time}", ephemeral=True) if self.view: self.view.stop() class TimeSelectionView(discord.ui.View): def __init__(self, cog: "MORS"): super().__init__(timeout=180) self.add_item(TimeSelect(cog)) class ReviewModal(discord.ui.Modal, title="MORS Review Submission"): service_type = discord.ui.TextInput(label="What service type did you use?") rating = discord.ui.TextInput(label="Rating (1-10)", max_length=2) review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph, max_length=1500) toxicity = discord.ui.TextInput(label="Toxicity Level (ntox/stox)") def __init__(self, cog: "MORS"): super().__init__() self.cog = cog async def on_submit(self, interaction: discord.Interaction): guild = interaction.guild if not guild: await interaction.response.send_message("An error occurred.", ephemeral=True) return review_channel_id = await self.cog.config.guild(guild).review_channel() if not review_channel_id: await interaction.response.send_message("The review channel has not been configured.", ephemeral=True) return review_channel = guild.get_channel(review_channel_id) if not isinstance(review_channel, discord.TextChannel): await interaction.response.send_message("The configured review channel is invalid.", ephemeral=True) return embed = discord.Embed( title="New MORS Review", color=discord.Color.green() ) embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url) embed.add_field(name="Service Type", value=self.service_type.value, inline=False) embed.add_field(name="Rating", value=f"{self.rating.value}/10", inline=True) embed.add_field(name="Toxicity", value=self.toxicity.value, inline=True) embed.add_field(name="Review", value=self.review_text.value, inline=False) try: await review_channel.send(embed=embed) except discord.Forbidden: # This would only happen if permissions changed mid-process pass await interaction.response.send_message("Your review has been submitted! This ticket will close shortly.", ephemeral=True) class MORS(commands.Cog): """ Mass Outreach & Review System """ def __init__(self, bot: "Red"): self.bot = bot self.config = Config.get_conf(self, identifier=1234567894, force_registration=True) default_guild = { "ad_channel": None, "review_channel": None, "done_channel": None, "queue_channel": None, # Added for queue messages "access_role": None, "bypass_role": None, "active_tickets": {} } self.config.register_guild(**default_guild) # --- User Commands --- @app_commands.command() @app_commands.guild_only() async def game(self, interaction: discord.Interaction): """Start the mass outreach process by submitting an ad.""" guild = interaction.guild if not guild: return if not isinstance(interaction.user, discord.Member): await interaction.response.send_message("This command can only be used by server members.", ephemeral=True) return member = interaction.user access_role_id = await self.config.guild(guild).access_role() if access_role_id: access_role = guild.get_role(access_role_id) if access_role and access_role not in member.roles: await interaction.response.send_message( f"You need the {access_role.name} role to use this command.", ephemeral=True ) return modal = AdSubmissionModal(cog=self) await interaction.response.send_modal(modal) @app_commands.command() @app_commands.guild_only() async def over(self, interaction: discord.Interaction): """Select your advertisement duration.""" guild = interaction.guild if not guild: return active_tickets = await self.config.guild(guild).active_tickets() if str(interaction.user.id) not in active_tickets: await interaction.response.send_message( "You need to start the process with `/game` before you can use this command.", ephemeral=True ) return view = TimeSelectionView(cog=self) await interaction.response.send_message("Please select your desired ad duration from the dropdown below:", view=view, ephemeral=True) @app_commands.command(name="pudding-head") @app_commands.guild_only() async def pudding_head(self, interaction: discord.Interaction): """Submit your final review for the MORS process.""" guild = interaction.guild if not guild: return active_tickets = await self.config.guild(guild).active_tickets() if str(interaction.user.id) not in active_tickets: await interaction.response.send_message( "You do not have an active MORS ticket to review.", ephemeral=True ) return modal = ReviewModal(cog=self) await interaction.response.send_modal(modal) # --- Admin Commands --- @app_commands.command() @app_commands.guild_only() @app_commands.describe(member="The user whose ad you want to post.") @app_commands.default_permissions(manage_guild=True) async def posted(self, interaction: discord.Interaction, member: discord.Member): """Approves and posts a user's submitted ad.""" guild = interaction.guild if not guild: return conf = self.config.guild(guild) ad_channel_id = await conf.ad_channel() if not ad_channel_id: await interaction.response.send_message("Ad channel not configured.", ephemeral=True) return ad_channel = guild.get_channel(ad_channel_id) if not isinstance(ad_channel, discord.TextChannel): await interaction.response.send_message("Configured ad channel is invalid.", ephemeral=True) return async with conf.active_tickets() as active_tickets: user_id_str = str(member.id) if user_id_str not in active_tickets: await interaction.response.send_message(f"{member.name} has no pending ad.", ephemeral=True) return ticket_data = active_tickets[user_id_str] ad_embed = discord.Embed( title="New Server Advertisement", description=ticket_data.get("ad_text", "No ad text provided."), color=discord.Color.blue() ) ad_embed.add_field(name="Invite Link", value=ticket_data.get("ad_link", "No link provided.")) ad_embed.set_author(name=member.name, icon_url=member.display_avatar.url) try: ad_message = await ad_channel.send(embed=ad_embed) await ad_message.pin() active_tickets[user_id_str]["status"] = "posted" except discord.Forbidden: await interaction.response.send_message("I lack permissions to post or pin in the ad channel.", ephemeral=True) return await interaction.response.send_message(f"Ad for {member.name} has been posted and pinned. Remember to set a reminder.", ephemeral=True) @app_commands.command() @app_commands.guild_only() @app_commands.describe(member="The user whose ticket you want to finalize.") @app_commands.default_permissions(manage_guild=True) async def done(self, interaction: discord.Interaction, member: discord.Member): """Admin command to finalize and close a MORS ticket.""" guild = interaction.guild if not guild: return conf = self.config.guild(guild) done_channel_id = await conf.done_channel() if not done_channel_id: await interaction.response.send_message("The 'done' channel is not configured.", ephemeral=True) return done_channel = guild.get_channel(done_channel_id) if not isinstance(done_channel, discord.TextChannel): await interaction.response.send_message("The configured 'done' channel is invalid.", ephemeral=True) return async with conf.active_tickets() as active_tickets: user_id_str = str(member.id) if user_id_str not in active_tickets: await interaction.response.send_message(f"{member.name} does not have an active MORS ticket.", ephemeral=True) return ticket_data = active_tickets.pop(user_id_str) # Remove ticket from active list thread_id = ticket_data.get("thread_id") ad_link = ticket_data.get("ad_link", "Not available") thread = guild.get_thread(thread_id) if thread_id else None # Send embed to done channel done_embed = discord.Embed( description=f"Advertisement period is complete for {member.mention}.", color=discord.Color.teal() ) done_embed.add_field(name="Server Ad Link", value=ad_link, inline=False) # Client mentioned "image", will need clarification on what image to include. await done_channel.send(embed=done_embed) if thread: await thread.send(f"{member.mention}, your MORS process is complete! Please submit your review with `/pudding-head`.") await thread.send("This ticket will be archived in 60 seconds.") await interaction.response.send_message(f"Finalizing ticket for {member.name}. The thread will be archived in 60 seconds.", ephemeral=True) await asyncio.sleep(60) try: await thread.edit(archived=True, locked=True) except discord.Forbidden: await thread.send("I don't have permission to archive this thread.") else: await interaction.response.send_message(f"Finalizing ticket for {member.name}. Could not find original thread to archive.", ephemeral=True) # --- Settings Commands --- @commands.group() # type: ignore @commands.guild_only() @commands.admin_or_permissions(manage_guild=True) async def morsset(self, ctx: commands.Context): """Configure MORS settings.""" pass @morsset.command(name="adchannel") async def set_ad_channel(self, ctx: commands.Context, channel: discord.TextChannel): """Set the channel where ads are posted.""" if not ctx.guild: return await self.config.guild(ctx.guild).ad_channel.set(channel.id) await ctx.send(f"Ad channel has been set to {channel.mention}") @morsset.command(name="reviewchannel") async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel): """Set the channel where final reviews are sent.""" if not ctx.guild: return await self.config.guild(ctx.guild).review_channel.set(channel.id) await ctx.send(f"Review channel has been set to {channel.mention}") @morsset.command(name="donechannel") async def set_done_channel(self, ctx: commands.Context, channel: discord.TextChannel): """Set the channel for the /done command embeds.""" if not ctx.guild: return await self.config.guild(ctx.guild).done_channel.set(channel.id) await ctx.send(f"Done channel has been set to {channel.mention}") @morsset.command(name="queuechannel") async def set_queue_channel(self, ctx: commands.Context, channel: discord.TextChannel): """Set the channel for queue confirmation messages.""" if not ctx.guild: return await self.config.guild(ctx.guild).queue_channel.set(channel.id) await ctx.send(f"Queue channel has been set to {channel.mention}") @morsset.command(name="accessrole") async def set_access_role(self, ctx: commands.Context, role: discord.Role): """Set the role required to use the /game command.""" if not ctx.guild: return await self.config.guild(ctx.guild).access_role.set(role.id) await ctx.send(f"Access role has been set to {role.name}") @morsset.command(name="bypassrole") async def set_bypass_role(self, ctx: commands.Context, role: discord.Role): """Set the bypass role for MORS cooldowns.""" if not ctx.guild: return await self.config.guild(ctx.guild).bypass_role.set(role.id) await ctx.send(f"Bypass role has been set to {role.name}")