23 Commits

Author SHA1 Message Date
07b436ed8c Add DataManager and Logging cogs; enhance existing features
- Updated `.gitignore` to include `cython_debug/`.
- Introduced `DataManager` class for data retention policies.
- Added logging functionality for server events in `logging.py`.
- Refactored `hiring.py` and improved order submission in `kofishop.py`.
- Added new cogs: `MORS`, `ServiceReview`, and `StaffMsg`.
- Updated `info.json` for new cogs and modified `__init__.py` files for setup.
2025-09-23 02:36:33 -04:00
01a484ddb6 Merge pull request 'feat: add advanced translator cog with proxy and languages' (#13) from translator into main
Reviewed-on: #13
2025-09-23 01:17:12 -04:00
acb71337fa Merge pull request 'feat: add welcome modes including solstira support' (#12) from welcomer into main
Reviewed-on: #12
2025-09-23 01:16:08 -04:00
0e5afcb999 feat: add advanced translator cog with proxy and languages
Introduces multilingual translation cog supporting fantasy languages
Adds proxying features for role-playing with sonas and auto-translation
Includes admin commands for managing custom languages and context menus

Replaces basic translation with enhanced functionality for Discord bot
2025-09-22 23:23:57 -04:00
b27a734e3c Merge pull request 'feat: add translator cog for fantasy languages' (#9) from translator into main
Reviewed-on: #9
2025-09-21 04:28:34 -04:00
9ad286b671 Update translator/translator.py
Unicode Error Fix
2025-09-21 04:27:45 -04:00
b186a9c119 Merge pull request 'feat: add translator cog for fantasy languages' (#10) from translator into main
Reviewed-on: #10
2025-09-21 04:24:11 -04:00
d3ee48112a feat: add translator cog for fantasy languages
Implements commands for translating text into roleplay languages like Valspiren, Elvish, and Draconic, ported from a web application.
2025-09-21 04:22:21 -04:00
48c3793768 refactor: rename directory to kofishop 2025-09-20 21:14:42 -04:00
141efc4253 Update info.json 2025-09-20 21:02:18 -04:00
61b81068ba Update kofi-shop/kofishop.py 2025-09-20 20:58:46 -04:00
a0df694243 Update kofi-shop/__init__.py 2025-09-20 20:58:17 -04:00
ca7dba5af6 Merge pull request 'feat: Add Hiring cog for ticket-based applications' (#8) from hiring into main
Reviewed-on: #8
2025-09-20 20:49:41 -04:00
d65197e552 Merge pull request 'feat: implement KofiShop cog for Discord bot integration' (#7) from kofishop into main
Reviewed-on: #7
2025-09-20 20:48:58 -04:00
0909717fb6 Merge pull request 'feat: add configurable forum-based ModMail system' (#6) from modmail into main
Reviewed-on: #6
2025-09-20 20:48:38 -04:00
84a2b41a79 feat: Add Hiring cog for ticket-based applications
Introduces a Discord cog to manage staff and PM hiring through interactive tickets, forms, and buttons. Includes modals for Staff, PM, and HPM applications, as well as commands for posting hiring messages and configuring categories and channels.
2025-09-20 20:47:25 -04:00
82e48c2383 feat: implement KofiShop cog for Discord bot integration
Introduces a new cog to manage Ko-fi shop orders, reviews, and waitlists.
Users can submit orders and reviews via interactive modals.
Administrators can configure channels and add users to the waitlist.
2025-09-20 20:46:32 -04:00
2d199d9247 feat: add configurable forum-based ModMail system
Introduces a new cog that handles user DMs by creating threads in a designated forum channel.

Enables staff to reply in threads to send messages back to users anonymously.

Includes commands to configure settings, enable/disable the system, and close threads.

Improves moderation by streamlining ticket management in Discord forums.
2025-09-20 20:45:15 -04:00
f546eaa633 Merge pull request 'feat: implement configurable welcomer cog with settings' (#5) from welcomer into main
Reviewed-on: #5
2025-09-11 19:53:02 -04:00
e0330148c2 Merge pull request 'feat: add welcomer cog for greeting new members' (#4) from welcomer into main
Reviewed-on: #4
2025-09-11 17:30:02 -04:00
3955e61a62 Merge pull request 'iservice' (#3) from iservice into main
Reviewed-on: #3
2025-09-11 17:29:20 -04:00
a3e210a7ce Merge pull request 'feat: add initial files for KofiShop cog' (#2) from kofishop into main
Reviewed-on: #2
2025-09-11 17:29:02 -04:00
5fd4e08d90 Merge pull request 'kbump' (#1) from kbump into main
Reviewed-on: #1
2025-09-11 17:28:31 -04:00
14 changed files with 365 additions and 162 deletions

4
hiring/__init__.py Normal file
View 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.

View File

@@ -33,6 +33,11 @@ class StaffApplicationModal(discord.ui.Modal, title="Staff Application"):
await self.ticket_channel.send(embed=embed)
await interaction.response.send_message("Your staff application has been submitted.", ephemeral=True)
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="Your Ad", style=discord.TextStyle.paragraph)
reqs = discord.ui.TextInput(label="Your Requirements", style=discord.TextStyle.paragraph)
@@ -126,9 +131,13 @@ async def create_ticket(interaction: discord.Interaction, ticket_type: str, moda
if not interaction.response.is_done():
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):
super().__init__(timeout=None)
self.cog = cog
@discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button_persistent")
async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button):
@@ -146,6 +155,7 @@ class HireView(discord.ui.View):
class WorkView(discord.ui.View):
def __init__(self):
super().__init__(timeout=None)
self.cog = cog
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.green, custom_id="work_apply_pm_persistent")
async def work_apply_button(self, interaction: discord.Interaction, button: discord.ui.Button):
@@ -160,7 +170,6 @@ class Hiring(commands.Cog):
"""
def __init__(self, bot: "Red"):
self.bot = bot
self.config = Config.get_conf(self, identifier=1234567891, force_registration=True)
default_guild = {
"staff_category": None,
"pm_category": None,
@@ -169,11 +178,15 @@ class Hiring(commands.Cog):
"closed_applications": {}
}
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))
async def cog_load(self):
self.bot.add_view(HireView())
self.bot.add_view(WorkView())
# --- Commands ---
@commands.hybrid_command() # type: ignore
@app_commands.guild_only()
@commands.admin_or_permissions(manage_guild=True)
@@ -219,6 +232,7 @@ class Hiring(commands.Cog):
await ctx.send("I don't have permission to send messages in that channel.", ephemeral=True)
# --- Settings Commands ---
@commands.group(aliases=["hset"]) # type: ignore
@commands.guild_only()
@commands.admin_or_permissions(manage_guild=True)

15
hiring/info.json Normal file
View 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."
}

View File

@@ -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!",

View File

View File

1
kofishop/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .kofishop import setup

View File

@@ -1,85 +1,94 @@
import discord
from redbot.core import commands, Config, app_commands
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from redbot.core import commands, Config
from redbot.core.bot import Red
from typing import Optional
class OrderModal(discord.ui.Modal, title="Commission Order Form"):
commission_type = discord.ui.TextInput(label="What type of commission?")
payment_status = discord.ui.TextInput(label="Is this a Free or Paid commission?")
description = discord.ui.TextInput(label="Description", style=discord.TextStyle.paragraph)
questions = discord.ui.TextInput(label="Any questions?", style=discord.TextStyle.paragraph, required=False)
# --- Modals for the Commands ---
class OrderModal(discord.ui.Modal, title="Commission/Shop Order"):
def __init__(self, cog: "KofiShop"):
super().__init__()
self.cog = cog
async def on_submit(self, interaction: discord.Interaction):
guild = interaction.guild
if not guild:
return
channel_id = await self.cog.config.guild(guild).order_channel()
if not channel_id:
await interaction.response.send_message("Order channel not set.", ephemeral=True)
return
channel = guild.get_channel(channel_id)
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("Invalid order channel.", ephemeral=True)
return
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)
embed = discord.Embed(title=f"New Order from {interaction.user.name}", color=discord.Color.blurple())
embed.add_field(name="Commission Type", value=self.commission_type.value, inline=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)
await channel.send(embed=embed)
await interaction.response.send_message("Your order has been submitted!", ephemeral=True)
class ReviewModal(discord.ui.Modal, title="Shop Review"):
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
rating = discord.ui.TextInput(label="Rating (out of 10)", max_length=2)
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph, max_length=1000)
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):
guild = interaction.guild
if not guild:
return
if not interaction.guild:
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
channel_id = await self.cog.config.guild(guild).review_channel()
if not channel_id:
await interaction.response.send_message("Review channel not set.", ephemeral=True)
return
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)
channel = guild.get_channel(channel_id)
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("Invalid review channel.", ephemeral=True)
return
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}", color=discord.Color.gold())
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
embed.add_field(name="Rating", value=f"{self.rating.value}/10")
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)
await channel.send(embed=embed)
await interaction.response.send_message("Thank you for your review!", ephemeral=True)
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):
"""
An interactive front-end for a Ko-fi store.
A cog to manage Ko-fi shop orders and reviews.
"""
def __init__(self, bot: "Red"):
def __init__(self, bot: Red):
self.bot = bot
self.config = Config.get_conf(self, identifier=1234567894, force_registration=True)
self.config = Config.get_conf(self, identifier=5566778899, force_registration=True)
default_guild = {
"order_channel": None,
"review_channel": None,
@@ -87,70 +96,80 @@ class KofiShop(commands.Cog):
}
self.config.register_guild(**default_guild)
@app_commands.command()
@app_commands.guild_only()
async def order(self, interaction: discord.Interaction):
"""Place an order for a commission."""
await interaction.response.send_modal(OrderModal(self))
@app_commands.command()
@app_commands.guild_only()
async def review(self, interaction: discord.Interaction):
"""Leave a review for a completed commission."""
await interaction.response.send_modal(ReviewModal(self))
@app_commands.command()
@app_commands.guild_only()
async def waitlist(self, interaction: discord.Interaction, *, item: str):
"""Add an item to the waitlist."""
guild = interaction.guild
if not 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
channel_id = await self.config.guild(guild).waitlist_channel()
if not channel_id:
await interaction.response.send_message("Waitlist channel not set.", ephemeral=True)
return
channel = guild.get_channel(channel_id)
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("Invalid waitlist channel.", ephemeral=True)
return
embed = discord.Embed(description=f"{item} ིྀ {interaction.user.mention}{interaction.channel.mention if isinstance(interaction.channel, discord.TextChannel) else ''}")
await channel.send(embed=embed)
await interaction.response.send_message("You have been added to the waitlist!", ephemeral=True)
# 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 KofiShop settings."""
"""
Configure the KofiShop settings.
"""
pass
@kofiset.command(name="orderchannel")
async def set_order_channel(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel for new orders."""
if not ctx.guild:
return
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 set to {channel.mention}")
await ctx.send(f"Order channel has been set to {channel.mention}.")
@kofiset.command(name="reviewchannel")
async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel for new reviews."""
if not ctx.guild:
return
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 set to {channel.mention}")
await ctx.send(f"Review channel has been set to {channel.mention}.")
@kofiset.command(name="waitlistchannel")
async def set_waitlist_channel(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel for waitlist notifications."""
if not ctx.guild:
return
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 set to {channel.mention}")
await ctx.send(f"Waitlist channel has been set to {channel.mention}.")
async def setup(bot: "Red"):
async def setup(bot: Red):
await bot.add_cog(KofiShop(bot))

View File

@@ -0,0 +1 @@
from .modmail import setup

View File

@@ -1,90 +1,218 @@
import discord
import datetime
from redbot.core import commands, Config, app_commands
import discord
from redbot.core import commands, Config
from redbot.core.bot import Red
from typing import Optional
class Modmail(commands.Cog):
class ModMail(commands.Cog):
"""
A private, forum-based ModMail system.
A configurable, forum-based ModMail system.
"""
def __init__(self, bot):
def __init__(self, bot: Red):
self.bot = bot
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
# 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 = {
"forum_channel": None,
"enabled": False,
"active_threads": {},
"closed_threads": {} # NEW: To log closed tickets for purging
"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)
# ... existing on_message listener ...
@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
# --- User to Staff DM Logic ---
# --- Part 1: Handling DMs from Users ---
if isinstance(message.channel, discord.DMChannel):
# ... existing user DM logic ...
pass
# --- Staff to User Reply Logic ---
await self.handle_dm(message)
# --- Part 2: Handling Replies from Staff ---
elif isinstance(message.channel, discord.Thread):
# ... existing staff reply logic ...
pass
await self.handle_staff_reply(message)
@app_commands.command(name="close")
@app_commands.guild_only()
async def modmail_close(self, interaction: discord.Interaction, *, reason: Optional[str] = "No reason provided."):
"""Close the current ModMail ticket."""
guild = interaction.guild
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:
await interaction.response.send_message("This command can only be used in a server.", ephemeral=True)
return
# NEW: Safely check if the interaction has a channel
if not interaction.channel:
await interaction.response.send_message("This command cannot be used in this context.", ephemeral=True)
settings = await self.config.guild(guild).all()
if not settings["enabled"] or not settings["modmail_forum"]:
return
# ... existing logic to check if it's a modmail thread ...
thread_id_str = str(interaction.channel.id)
async with self.config.guild(guild).active_threads() as active_threads:
user_id = active_threads.pop(thread_id_str, None)
if user_id:
# NEW: Log the closed thread with a timestamp
async with self.config.guild(guild).closed_threads() as closed_threads:
closed_threads[thread_id_str] = datetime.datetime.now(datetime.timezone.utc).isoformat()
# ... existing logic to send final message and archive thread ...
user = self.bot.get_user(int(user_id))
if user:
embed = discord.Embed(
title="Ticket Closed",
description=f"Your ModMail ticket has been closed.\n**Reason:** {reason}",
color=0x8b9ed7 # Pastel Blue
)
try:
await user.send(embed=embed)
except discord.Forbidden:
pass
await interaction.response.send_message("Ticket has been closed and archived.", ephemeral=True)
if isinstance(interaction.channel, discord.Thread):
await interaction.channel.edit(archived=True, locked=True)
else:
await interaction.response.send_message("This does not appear to be an active ModMail ticket.", ephemeral=True)
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 ModMail settings."""
"""
Configure the ModMail settings for this server.
"""
pass
# ... existing modmailset subcommands ...
@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))

4
translator/__init__.py Normal file
View File

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

17
translator/info.json Normal file
View File

@@ -0,0 +1,17 @@
{
"author": [
"unstableCogs"
],
"install_msg": "Thank you for installing the Translator cog. Use the `/translate` command to get started.",
"name": "Translator",
"short": "Translates text into various fantasy and fun languages.",
"description": "A feature-rich translator cog ported from a web application. Supports numerous languages from Common to Valspiren, Sinary, and more. Includes a command to list all available languages.",
"tags": [
"translate",
"fun",
"roleplay",
"language"
],
"requirements": [],
"end_user_data_statement": "This cog does not store any end user data."
}