feat: add multiple discord bot cogs
Adds new cogs including DataManager, Hiring, KofiShop, Logging, ModMail, MORS, ServiceReview, StaffMsg, and Translator to enhance bot functionality for data management, hiring processes, logging events, and more.
This commit is contained in:
444
mors/mors.py
444
mors/mors.py
@@ -0,0 +1,444 @@
|
||||
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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user