Compare commits
15 Commits
backup-ref
...
48c3793768
| Author | SHA1 | Date | |
|---|---|---|---|
| 48c3793768 | |||
| 141efc4253 | |||
| 61b81068ba | |||
| a0df694243 | |||
| ca7dba5af6 | |||
| d65197e552 | |||
| 0909717fb6 | |||
| 84a2b41a79 | |||
| 82e48c2383 | |||
| 2d199d9247 | |||
| f546eaa633 | |||
| e0330148c2 | |||
| 3955e61a62 | |||
| a3e210a7ce | |||
| 5fd4e08d90 |
4
hiring/__init__.py
Normal file
4
hiring/__init__.py
Normal file
@@ -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.
|
||||
253
hiring/hiring.py
Normal file
253
hiring/hiring.py
Normal file
@@ -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))
|
||||
|
||||
15
hiring/info.json
Normal file
15
hiring/info.json
Normal file
@@ -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."
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"author" : ["KWUK"],
|
||||
"install_msg" : "Thank you for installing my repo! If you need support, create an issue on Gitea or ping me in KWDS.",
|
||||
"install_msg" : "Thank you for installing my repo! If you need support, create an issue on Gitea or ping me in KWDS. Cogs: hiring, kbump, kofishop, modmail, mors, pp, rpg, iservice, welcomer. ",
|
||||
"name" : "unstable-cogs",
|
||||
"short" : "Cogs for Red-DiscordBot!",
|
||||
"description" : "Cogs for Red-DiscordBot!",
|
||||
|
||||
1
kofishop/__init__.py
Normal file
1
kofishop/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .kofishop import setup
|
||||
175
kofishop/kofishop.py
Normal file
175
kofishop/kofishop.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import discord
|
||||
from redbot.core import commands, Config
|
||||
from redbot.core.bot import Red
|
||||
from typing import Optional
|
||||
|
||||
# --- Modals for the Commands ---
|
||||
|
||||
class OrderModal(discord.ui.Modal, title="Commission/Shop Order"):
|
||||
def __init__(self, cog: "KofiShop"):
|
||||
super().__init__()
|
||||
self.cog = cog
|
||||
|
||||
comm_type = discord.ui.TextInput(label="What type of commission/item is this?")
|
||||
payment_status = discord.ui.TextInput(label="Is this free or paid?")
|
||||
description = discord.ui.TextInput(label="Please describe your request.", style=discord.TextStyle.paragraph)
|
||||
questions = discord.ui.TextInput(label="Any questions for the artist?", style=discord.TextStyle.paragraph, required=False)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
if not interaction.guild:
|
||||
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
|
||||
|
||||
order_channel_id = await self.cog.config.guild(interaction.guild).order_channel()
|
||||
if not order_channel_id:
|
||||
return await interaction.response.send_message("The order channel has not been set by an admin.", ephemeral=True)
|
||||
|
||||
order_channel = interaction.guild.get_channel(order_channel_id)
|
||||
if not isinstance(order_channel, discord.TextChannel):
|
||||
return await interaction.response.send_message("The configured order channel is invalid.", ephemeral=True)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="New Order Placed",
|
||||
description=f"Submitted by {interaction.user.mention}",
|
||||
color=0x00ff00 # Green for new orders
|
||||
)
|
||||
embed.add_field(name="Item/Commission Type", value=self.comm_type.value, inline=False)
|
||||
embed.add_field(name="Payment Status", value=self.payment_status.value, inline=False)
|
||||
embed.add_field(name="Description", value=self.description.value, inline=False)
|
||||
if self.questions.value:
|
||||
embed.add_field(name="Questions", value=self.questions.value, inline=False)
|
||||
|
||||
try:
|
||||
await order_channel.send(embed=embed)
|
||||
await interaction.response.send_message("Your order has been successfully submitted!", ephemeral=True)
|
||||
except discord.Forbidden:
|
||||
await interaction.response.send_message("I don't have permission to send messages in the order channel.", ephemeral=True)
|
||||
|
||||
class ReviewModal(discord.ui.Modal, title="Leave a Review"):
|
||||
def __init__(self, cog: "KofiShop"):
|
||||
super().__init__()
|
||||
self.cog = cog
|
||||
|
||||
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
|
||||
rating = discord.ui.TextInput(label="Rating (e.g., 10/10)")
|
||||
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
if not interaction.guild:
|
||||
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
|
||||
|
||||
review_channel_id = await self.cog.config.guild(interaction.guild).review_channel()
|
||||
if not review_channel_id:
|
||||
return await interaction.response.send_message("The review channel has not been set by an admin.", ephemeral=True)
|
||||
|
||||
review_channel = interaction.guild.get_channel(review_channel_id)
|
||||
if not isinstance(review_channel, discord.TextChannel):
|
||||
return await interaction.response.send_message("The configured review channel is invalid.", ephemeral=True)
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"New Review for: {self.item_name.value}",
|
||||
description=f"Submitted by {interaction.user.mention}",
|
||||
color=0xadd8e6 # Pastel Blue
|
||||
)
|
||||
embed.add_field(name="Rating", value=self.rating.value, inline=False)
|
||||
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
||||
|
||||
try:
|
||||
await review_channel.send(embed=embed)
|
||||
await interaction.response.send_message("Thank you! Your review has been submitted.", ephemeral=True)
|
||||
except discord.Forbidden:
|
||||
await interaction.response.send_message("I don't have permission to send messages in the review channel.", ephemeral=True)
|
||||
|
||||
|
||||
class KofiShop(commands.Cog):
|
||||
"""
|
||||
A cog to manage Ko-fi shop orders and reviews.
|
||||
"""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, identifier=5566778899, force_registration=True)
|
||||
|
||||
default_guild = {
|
||||
"order_channel": None,
|
||||
"review_channel": None,
|
||||
"waitlist_channel": None
|
||||
}
|
||||
self.config.register_guild(**default_guild)
|
||||
|
||||
# --- Commands ---
|
||||
@commands.hybrid_command()
|
||||
@commands.guild_only()
|
||||
async def order(self, ctx: commands.Context):
|
||||
"""Place an order for a shop or commission item."""
|
||||
if not ctx.interaction:
|
||||
return
|
||||
# We pass `self` (the cog instance) to the modal
|
||||
await ctx.interaction.response.send_modal(OrderModal(self))
|
||||
|
||||
@commands.hybrid_command()
|
||||
@commands.guild_only()
|
||||
async def review(self, ctx: commands.Context):
|
||||
"""Leave a review for a completed shop or commission item."""
|
||||
if not ctx.interaction:
|
||||
return
|
||||
await ctx.interaction.response.send_modal(ReviewModal(self))
|
||||
|
||||
@commands.hybrid_command() # type: ignore
|
||||
@commands.guild_only()
|
||||
@commands.admin_or_permissions(manage_guild=True)
|
||||
async def waitlist(self, ctx: commands.Context, user: discord.Member, *, item: str):
|
||||
"""Add a user and their requested item to the waitlist."""
|
||||
if not ctx.guild or not isinstance(ctx.channel, discord.TextChannel):
|
||||
return await ctx.send("This command must be used in a server's text channel.", ephemeral=True)
|
||||
|
||||
waitlist_channel_id = await self.config.guild(ctx.guild).waitlist_channel()
|
||||
if not waitlist_channel_id:
|
||||
return await ctx.send("The waitlist channel has not been set by an admin.", ephemeral=True)
|
||||
|
||||
waitlist_channel = ctx.guild.get_channel(waitlist_channel_id)
|
||||
if not isinstance(waitlist_channel, discord.TextChannel):
|
||||
return await ctx.send("The configured waitlist channel is invalid.", ephemeral=True)
|
||||
|
||||
message = f"**{item}** ིྀ {user.mention} ✧ in {ctx.channel.mention}"
|
||||
|
||||
try:
|
||||
await waitlist_channel.send(message)
|
||||
await ctx.send(f"{user.mention} has been added to the waitlist for '{item}'.", ephemeral=True)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(f"I don't have permission to send messages in the waitlist channel.", ephemeral=True)
|
||||
|
||||
# --- Settings Commands ---
|
||||
@commands.group(aliases=["kset"]) # type: ignore
|
||||
@commands.guild_only()
|
||||
@commands.admin_or_permissions(manage_guild=True)
|
||||
async def kofiset(self, ctx: commands.Context):
|
||||
"""
|
||||
Configure the KofiShop settings.
|
||||
"""
|
||||
pass
|
||||
|
||||
@kofiset.command(name="orderchannel")
|
||||
async def kofiset_order(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""Set the channel where new orders will be sent."""
|
||||
if not ctx.guild: return
|
||||
await self.config.guild(ctx.guild).order_channel.set(channel.id)
|
||||
await ctx.send(f"Order channel has been set to {channel.mention}.")
|
||||
|
||||
@kofiset.command(name="reviewchannel")
|
||||
async def kofiset_review(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""Set the channel where new reviews will be 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}.")
|
||||
|
||||
@kofiset.command(name="waitlistchannel")
|
||||
async def kofiset_waitlist(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""Set the channel where waitlist notifications will be sent."""
|
||||
if not ctx.guild: return
|
||||
await self.config.guild(ctx.guild).waitlist_channel.set(channel.id)
|
||||
await ctx.send(f"Waitlist channel has been set to {channel.mention}.")
|
||||
|
||||
|
||||
async def setup(bot: Red):
|
||||
await bot.add_cog(KofiShop(bot))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .modmail import setup
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user