diff --git a/hiring/__init__.py b/hiring/__init__.py new file mode 100644 index 0000000..edb2329 --- /dev/null +++ b/hiring/__init__.py @@ -0,0 +1,4 @@ +from .hiring import setup + +# This function is required for the cog to be loaded by Red. +# It simply imports the setup function from the main cog file. \ No newline at end of file diff --git a/hiring/hiring.py b/hiring/hiring.py new file mode 100644 index 0000000..1a79c28 --- /dev/null +++ b/hiring/hiring.py @@ -0,0 +1,253 @@ +import discord +from redbot.core import commands, Config +from redbot.core.bot import Red +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from hiring.hiring import Hiring + +# --- Modals for the Application Forms --- + +class StaffApplicationModal(discord.ui.Modal, title="Staff Application"): + chosen_plan = discord.ui.TextInput(label="What plan are you interested in?") + tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?") + describe_server = discord.ui.TextInput(label="Please describe your server.", style=discord.TextStyle.paragraph) + server_link = discord.ui.TextInput(label="Server Link") + + async def on_submit(self, interaction: discord.Interaction): + embed = discord.Embed( + title="New Staff Application", + description=f"Submitted by {interaction.user.mention}", + color=0xadd8e6 + ) + embed.add_field(name="Chosen Plan", value=self.chosen_plan.value, inline=False) + embed.add_field(name="Toxicity Level", value=self.tox_level.value, inline=False) + embed.add_field(name="Server Description", value=self.describe_server.value, inline=False) + embed.add_field(name="Server Link", value=self.server_link.value, inline=False) + + await interaction.response.send_message("Your application has been submitted!", ephemeral=True) + if isinstance(interaction.channel, discord.TextChannel): + await interaction.channel.send(embed=embed) + + +class PMApplicationModal(discord.ui.Modal, title="PM Application"): + ad = discord.ui.TextInput(label="Please provide your server ad.", style=discord.TextStyle.paragraph) + reqs = discord.ui.TextInput(label="What are your requirements?") + tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?") + chosen_plan = discord.ui.TextInput(label="What plan are you interested in?") + + async def on_submit(self, interaction: discord.Interaction): + embed = discord.Embed( + title="New PM Application", + description=f"Submitted by {interaction.user.mention}", + color=0xadd8e6 + ) + embed.add_field(name="Server Ad", value=f"```\n{self.ad.value}\n```", inline=False) + embed.add_field(name="Requirements", value=self.reqs.value, inline=False) + embed.add_field(name="Toxicity Level", value=self.tox_level.value, inline=False) + embed.add_field(name="Chosen Plan", value=self.chosen_plan.value, inline=False) + await interaction.response.send_message("Your application has been submitted!", ephemeral=True) + if isinstance(interaction.channel, discord.TextChannel): + await interaction.channel.send(embed=embed) + +class HPMApplicationModal(discord.ui.Modal, title="HPM Application"): + ad = discord.ui.TextInput(label="Please provide your server ad.", style=discord.TextStyle.paragraph) + reqs = discord.ui.TextInput(label="What are your requirements?") + + async def on_submit(self, interaction: discord.Interaction): + embed = discord.Embed( + title="New HPM Application", + description=f"Submitted by {interaction.user.mention}", + color=0xadd8e6 + ) + embed.add_field(name="Server Ad", value=f"```\n{self.ad.value}\n```", inline=False) + embed.add_field(name="Requirements", value=self.reqs.value, inline=False) + await interaction.response.send_message("Your application has been submitted!", ephemeral=True) + if isinstance(interaction.channel, discord.TextChannel): + await interaction.channel.send(embed=embed) + + +# --- Reusable Ticket Creation Logic --- +async def create_ticket(interaction: discord.Interaction, role_type: str, category_id: Optional[int], modal: discord.ui.Modal): + if not interaction.guild: + await interaction.response.send_message("This interaction must be used in a server.", ephemeral=True) + return + + if not category_id: + await interaction.response.send_message(f"The category for '{role_type}' applications has not been set by an admin.", ephemeral=True) + return + + category = interaction.guild.get_channel(category_id) + if not isinstance(category, discord.CategoryChannel): + await interaction.response.send_message(f"The category for '{role_type}' applications is invalid or has been deleted.", ephemeral=True) + return + + ticket_name = f"{role_type}-app-{interaction.user.name}" + + try: + overwrites = { + interaction.guild.default_role: discord.PermissionOverwrite(read_messages=False), + interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True), + interaction.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) + } + ticket_channel = await interaction.guild.create_text_channel( + name=ticket_name, + category=category, + overwrites=overwrites + ) + await ticket_channel.send(f"Welcome {interaction.user.mention}! Please fill out the form to complete your application.") + await interaction.response.send_modal(modal) + + except discord.Forbidden: + if interaction.response.is_done(): + await interaction.followup.send("I don't have permission to create channels in that category.", ephemeral=True) + else: + await interaction.response.send_message("I don't have permission to create channels in that category.", ephemeral=True) + except Exception as e: + if interaction.response.is_done(): + await interaction.followup.send(f"An unexpected error occurred: {e}", ephemeral=True) + else: + await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True) + + +# --- Button Views for Commands --- + +class HireView(discord.ui.View): + def __init__(self, cog: "Hiring"): + super().__init__(timeout=None) + self.cog = cog + + @discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button") + async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if not interaction.guild: return + category_id = await self.cog.config.guild(interaction.guild).staff_category() + await create_ticket(interaction, "staff", category_id, StaffApplicationModal()) + + @discord.ui.button(label="PM", style=discord.ButtonStyle.secondary, custom_id="pm_apply_button") + async def pm_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if not interaction.guild: return + category_id = await self.cog.config.guild(interaction.guild).pm_category() + await create_ticket(interaction, "pm", category_id, PMApplicationModal()) + + @discord.ui.button(label="HPM", style=discord.ButtonStyle.success, custom_id="hpm_apply_button") + async def hpm_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if not interaction.guild: return + category_id = await self.cog.config.guild(interaction.guild).hpm_category() + await create_ticket(interaction, "hpm", category_id, HPMApplicationModal()) + + +class WorkView(discord.ui.View): + def __init__(self, cog: "Hiring"): + super().__init__(timeout=None) + self.cog = cog + + @discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.blurple, custom_id="work_apply_button") + async def work_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if not interaction.guild: return + category_id = await self.cog.config.guild(interaction.guild).pm_category() + await create_ticket(interaction, "pm", category_id, PMApplicationModal()) + + +class Hiring(commands.Cog): + """ + A cog for managing staff and PM applications. + """ + + def __init__(self, bot: Red): + self.bot = bot + self.config = Config.get_conf(self, identifier=1122334455, force_registration=True) + + default_guild = { + "hpm_category": None, + "pm_category": None, + "staff_category": None, + "work_channel": None # New setting for the /work command channel + } + self.config.register_guild(**default_guild) + # We need to make sure the views are persistent so buttons work after a restart + self.bot.add_view(HireView(self)) + self.bot.add_view(WorkView(self)) + + + # --- Commands --- + @commands.hybrid_command() # type: ignore + @commands.guild_only() + @commands.admin_or_permissions(manage_guild=True) + async def hire(self, ctx: commands.Context): + """Post the application message with buttons in the current channel.""" + embed = discord.Embed( + title="Start an Application", + description="Click a button below to open a ticket for the role you are interested in.", + color=0xadd8e6 + ) + await ctx.send(embed=embed, view=HireView(self)) + + @commands.hybrid_command() # type: ignore + @commands.guild_only() + @commands.admin_or_permissions(manage_guild=True) + async def work(self, ctx: commands.Context): + """Post the PM hiring message in the configured work channel.""" + if not ctx.guild: + return + work_channel_id = await self.config.guild(ctx.guild).work_channel() + if not work_channel_id: + await ctx.send("The work channel has not been set. Please use `[p]hiringset workchannel` to set it.", ephemeral=True) + return + + work_channel = ctx.guild.get_channel(work_channel_id) + if not isinstance(work_channel, discord.TextChannel): + await ctx.send("The configured work channel is invalid or has been deleted.", ephemeral=True) + return + + embed = discord.Embed( + title="Now Hiring: Partnership Managers", + description="We are for talented Partnership Managers (PMs) to join our team. If you are interested in applying, please click the button below to begin the application process.", + color=0xadd8e6 + ) + try: + await work_channel.send(embed=embed, view=WorkView(self)) + await ctx.send(f"Hiring message posted in {work_channel.mention}.", ephemeral=True) + except discord.Forbidden: + await ctx.send(f"I don't have permission to send messages in {work_channel.mention}.", ephemeral=True) + + + # --- Settings Commands --- + @commands.group(aliases=["hset"]) # type: ignore + @commands.guild_only() + @commands.admin_or_permissions(manage_guild=True) + async def hiringset(self, ctx: commands.Context): + """Configure the Hiring cog settings.""" + pass + + @hiringset.command(name="staffcategory") + async def set_staff_category(self, ctx: commands.Context, category: discord.CategoryChannel): + """Set the category for Staff applications.""" + if not ctx.guild: return + await self.config.guild(ctx.guild).staff_category.set(category.id) + await ctx.send(f"Staff application category set to **{category.name}**.") + + @hiringset.command(name="pmcategory") + async def set_pm_category(self, ctx: commands.Context, category: discord.CategoryChannel): + """Set the category for PM applications.""" + if not ctx.guild: return + await self.config.guild(ctx.guild).pm_category.set(category.id) + await ctx.send(f"PM application category set to **{category.name}**.") + + @hiringset.command(name="hpmcategory") + async def set_hpm_category(self, ctx: commands.Context, category: discord.CategoryChannel): + """Set the category for HPM applications.""" + if not ctx.guild: return + await self.config.guild(ctx.guild).hpm_category.set(category.id) + await ctx.send(f"HPM application category set to **{category.name}**.") + + @hiringset.command(name="workchannel") + async def set_work_channel(self, ctx: commands.Context, channel: discord.TextChannel): + """Set the channel where the /work hiring message will be posted.""" + if not ctx.guild: return + await self.config.guild(ctx.guild).work_channel.set(channel.id) + await ctx.send(f"The work channel has been set to {channel.mention}.") + + +async def setup(bot: Red): + await bot.add_cog(Hiring(bot)) + diff --git a/hiring/info.json b/hiring/info.json new file mode 100644 index 0000000..3a9ba26 --- /dev/null +++ b/hiring/info.json @@ -0,0 +1,15 @@ +{ + "author": [ "unstableCogs" ], + "install_msg": "Thank you for installing the Hiring cog! Use `[p]help Hiring` for a list of commands.", + "name": "Hiring", + "short": "A ticket-based system for staff and PM applications.", + "description": "Provides /hire and /work commands to manage the staff and PM hiring process through a ticket system with forms and buttons.", + "tags": [ + "hiring", + "tickets", + "utility", + "modmail" + ], + "requirements": [], + "end_user_data_statement": "This cog stores user IDs and the content of their applications for the duration of the hiring process." +}