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:
2025-09-23 00:28:29 -04:00
parent c2367369f1
commit 81f2eee409
63 changed files with 2824 additions and 6 deletions

View File

@@ -0,0 +1,4 @@
from .mors import MORS
async def setup(bot):
await bot.add_cog(MORS(bot))

View File

@@ -1,9 +1,11 @@
{
"author": [ "unstableCogs" ],
"install_msg": "Thank you for installing the Mass Outreach & Review System! For a full command list, please see the wiki.",
"name": "MassOutreach",
"author": [
"unstableCogs"
],
"install_msg": "Thank you for installing the Mass Outreach & Review System cog!",
"name": "MORS",
"short": "A multi-step system for mass outreach and reviews.",
"description": "Manages a complete workflow for user outreach. The process begins with /game, uses /over for time selection, and concludes with a /pudding-head command for the user to submit a mass review.",
"description": "Manages a complete workflow for user outreach, including ad submission, time selection, and final reviews.",
"tags": [
"mass",
"outreach",
@@ -11,6 +13,6 @@
"tickets",
"utility"
],
"requirements": [ "rich" ],
"end_user_data_statement": "This cog stores user IDs and ticket information temporarily for the duration of the outreach process. Submitted reviews are stored persistently."
"requirements": [],
"end_user_data_statement": "This cog stores user IDs, ticket information, and submitted reviews persistently for record-keeping purposes."
}

View File

@@ -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}")