20 Commits

Author SHA1 Message Date
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
e552ba7552 feat: implement configurable welcomer cog with settings
Refactors simple welcomer to support custom messages, channels, and toggles.

Adds command group for admins to manage server-specific settings, placeholders for personalization, and built-in safety checks for permissions and channel validity.

Enhances event handling with better error management and logging.
2025-09-11 19:46:27 -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
8a7621836f feat: add welcomer cog for greeting new members
Introduces a simple cog that welcomes new users via customizable messages in designated channels, enhancing community engagement.

Handles channel selection with fallbacks and includes error resilience for permission issues.
2025-09-11 17:24:56 -04:00
eec57c7e23 feat: adds ServiceReview cog
Introduces functionality for users to submit independent service reviews, sent to a designated staff channel.

Includes metadata and initial structure files.
2025-09-11 17:24:31 -04:00
e0958f6f2a feat: Add RPG cog with character and inventory features
Sets up RPG system as a Redbot cog with core mechanics
Implements character creation, class selection, and stat management
Adds action commands for attacking and healing
Provides inventory and shop functionality for item management
Introduces interactive UI menus for user engagement
2025-09-11 17:23:45 -04:00
859ac84b68 feat: add password protection cog for channels
Adds new Discord bot cog enabling password-protected channels.
Includes verification, channel management, whitelist/blacklist,
and admin password recovery features.
2025-09-11 17:22:36 -04:00
30 changed files with 2112 additions and 1 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.

253
hiring/hiring.py Normal file
View 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
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"], "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", "name" : "unstable-cogs",
"short" : "Cogs for Red-DiscordBot!", "short" : "Cogs for Red-DiscordBot!",
"description" : "Cogs for Red-DiscordBot!", "description" : "Cogs for Red-DiscordBot!",

1
kofishop/__init__.py Normal file
View File

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

175
kofishop/kofishop.py Normal file
View 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))

View File

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

View File

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

5
pp/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .passwdprotect import Passwd
async def setup(bot):
cog = Passwd(bot)
await bot.add_cog(cog)

10
pp/info.json Normal file
View File

@@ -0,0 +1,10 @@
{
"author": ["kitsunic"],
"description": "A cog for password protected channels",
"end_user_data_statement": "This cog stores user, avatar, and guild data. A simple delete request to bot to remove your data or guild data",
"install_msg": "Thanks for installing & testing.",
"min_bot_version": "3.5.0",
"short": "A pwd channel protection cog",
"tags": ["embed"],
"type": "COG"
}

366
pp/passwdprotect.py Normal file
View File

@@ -0,0 +1,366 @@
import discord
import hashlib
import asyncio
import random
import string
import datetime
from redbot.core import commands, checks, Config
class Passwd(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.config = Config.get_conf(self, identifier=50204090) # Unique identifier
# Default settings within the config
default_guild = {
"password_channels": {},
"password_attempts": {},
"admin_channel": None,
"password_recovery_requests": {},
"channel_data": {},
"whitelist": [],
"blacklist": [],
}
self.config.register_guild(**default_guild)
#
# -------------------------------------------------------------------------------------------------------------
#
@commands.group(name="passwdprotect", aliases=["pwd"])
async def passwdprotect(self, ctx):
"""Password management commands"""
if not ctx.invoked_subcommand:
await ctx.send("Missing subcommand.")
#
# -------------------------------------------------------------------------------------------------------------
#
@passwdprotect.command(name="verify", aliases=["ver"])
async def verify(self, ctx, password: str):
"""Verifies the password for the current channel."""
channel = ctx.channel
async with self.config.guild(ctx.guild).password_channels() as password_channels:
if channel.id in password_channels:
stored_hash = password_channels[channel.id]["password_hash"]
provided_hash = hashlib.sha256(password.encode("utf-8")).hexdigest()
if stored_hash == provided_hash:
# Password verified successfully
role_id = password_channels[channel.id]["role_id"]
role = ctx.guild.get_role(role_id)
if role:
await ctx.author.add_roles(role)
await ctx.send(f"Password verified! You now have access to {channel.mention}.")
else:
await ctx.send("Password verified, but the access role seems to be missing. Please contact an admin.")
else:
await ctx.send("Incorrect password.")
else:
await ctx.send("This channel is not password protected.")
@passwdprotect.group(name="channels", aliases=["chs", "--ch"])
async def channels(self, ctx):
"""Manage protected channels"""
if not ctx.invoked_subcommand:
await ctx.send("Invalid subcommand.")
#
# -------------------------------------------------------------------------------------------------------------
#
@channels.command(name="add", aliases=["+"])
@commands.has_permissions(manage_channels=True)
async def channels_add(self, ctx, channel: discord.TextChannel, password: str):
"""Adds a password to a channel"""
hashed_password = self.generate_hashed_password(password)
async with self.config.guild(ctx.guild).password_channels() as password_channels:
# Check if the channel already has a password
if channel.id in password_channels:
await ctx.send(f"{channel.mention} already has a password. Use the `remove` command first.")
return
# Create the role
role_name = f"{channel.name}_access"
role = await ctx.guild.create_role(name=role_name)
# Grant permissions to the role
await channel.set_permissions(role, view_channel=True, send_messages=True)
await channel.set_permissions(ctx.guild.default_role, view_channel=False, read_message_history=False) # Restrict access for default role
# Store password, channel ID, and role ID in the config
password_channels[channel.id] = {
"password_hash": hashed_password,
"role_id": role.id
}
await ctx.send(f"Password set for {channel.mention}")
#
# -------------------------------------------------------------------------------------------------------------
#
@channels.command(name="remove", aliases=["-"])
@commands.has_permissions(manage_channels=True)
async def channels_remove(self, ctx, channel: discord.TextChannel):
"""Removes the password from a text channel."""
async with self.config.guild(ctx.guild).password_channels() as password_channels:
if channel.id in password_channels:
# Delete the channel's data from the config
del password_channels[channel.id]
# Delete the associated role (optional)
role_id = channel.get("role_id")
if role_id:
role = ctx.guild.get_role(role_id)
if role:
await role.delete()
await ctx.send(f"Password removed from {channel.mention}")
else:
await ctx.send(f"{channel.mention} does not have a password.")
@channels.command(name="fails", aliases=["--fail"])
@commands.has_permissions(manage_guild=True)
async def channels_fails(self, ctx, limit: int = None):
"""Sets or checks the maximum failed password attempts."""
# ... handle failed attempt limit ...
#
# -------------------------------------------------------------------------------------------------------------
#
@channels.command(name="whitelist", aliases=["wlist"])
@commands.has_permissions(manage_guild=True)
async def channels_white(self, ctx, subcommand: str, user_or_role: discord.Object = None):
"""Manages the whitelist."""
guild_id = str(ctx.guild.id)
password_collection = self.get_password_collection(guild_id)
if not user_or_role:
await ctx.send("Please specify a user or role to add/remove.")
return
if subcommand == "add":
await self.add_to_whitelist(password_collection, user_or_role)
elif subcommand == "remove":
await self.remove_from_whitelist(password_collection, user_or_role)
else:
await ctx.send("Invalid subcommand. Use `add` or `remove`.")
#
# -------------------------------------------------------------------------------------------------------------
#
@channels.command(name="blacklist", aliases=["blist"])
@commands.has_permissions(manage_guild=True)
async def channels_black(self, ctx, subcommand: str, user_or_role: discord.Object = None):
"""Manages the blacklist."""
if not user_or_role:
await ctx.send("Please specify a user or role to add/remove.")
return
if subcommand == "add":
await self.add_to_blacklist(ctx, user_or_role)
elif subcommand == "remove":
await self.remove_from_blacklist(ctx, user_or_role)
else:
await ctx.send("Invalid subcommand. Use `add` or `remove`.")
#
# -------------------------------------------------------------------------------------------------------------
#
@passwdprotect.group(name="admin", aliases=["adm"])
async def admin(self, ctx):
"""Admin commands for password protection"""
if not ctx.invoked_subcommand:
await ctx.send("Missing subcommand.")
#
# -------------------------------------------------------------------------------------------------------------
#
@admin.command(name="recover", aliases=["rec"])
async def admin_recover(self, ctx):
"""Initiates password recovery process."""
requests = await self.config.guild(ctx.guild).password_recovery_requests()
if not requests:
await ctx.send("No pending password recovery requests.")
return
admin_channel_id = await self.config.guild(ctx.guild).admin_channel()
if not admin_channel_id:
await ctx.send("Admin notification channel not set. Use `[p]passwdprotect admin notify` to set it.")
return
admin_channel = self.bot.get_channel(admin_channel_id)
# Add Approve/Deny buttons using View
view = ui.View()
view.add_item(ui.Button(style=discord.ButtonStyle.green, label="Approve", custom_id=f"approve_{user_id}_{channel.id}"))
view.add_item(ui.Button(style=discord.ButtonStyle.red, label="Deny", custom_id=f"deny_{user_id}_{channel.id}"))
for user_id, request_data in requests.items():
user = self.bot.get_user(int(user_id))
channel = self.bot.get_channel(request_data["channel_id"])
timestamp = request_data["timestamp"]
# Log the request to the admin channel
embed = discord.Embed(title="Password Recovery Request", color=discord.Color.gold())
embed.add_field(name="User", value=user.mention if user else f"User ID: {user_id}", inline=False)
embed.add_field(name="Server", value=ctx.guild.name, inline=False)
embed.add_field(name="Channel", value=channel.mention if channel else f"Channel ID: {request_data['channel_id']}", inline=False)
embed.add_field(name="Highest Role", value=user.top_role.mention if user else "Unknown", inline=False)
embed.set_footer(text=f"Requested at {timestamp}")
# Add Approve/Deny buttons
view = SimpleView()
view.add_item(Button(style=discord.ButtonStyle.green, label="Approve", custom_id=f"approve_{user_id}_{channel.id}"))
view.add_item(Button(style=discord.ButtonStyle.red, label="Deny", custom_id=f"deny_{user_id}_{channel.id}"))
# Ping the role (if set) in the admin channel
role_to_ping = await self.config.guild(ctx.guild).get_raw("admin_role") # Assuming you store the role ID in config
if role_to_ping:
await admin_channel.send(f"<@&{role_to_ping}>", embed=embed, view=view)
else:
await admin_channel.send(embed=embed, view=view)
#
# -------------------------------------------------------------------------------------------------------------
#
@admin.command(name="notify", aliases=["noti"])
@commands.has_permissions(manage_guild=True)
async def admin_notify(self, ctx, channel: discord.TextChannel = None):
"""Sets or removes the admin notification channel."""
if channel:
# Set the admin notification channel
await self.config.guild(ctx.guild).admin_channel.set(channel.id)
await ctx.send(f"Admin notification channel set to {channel.mention}")
else:
# Remove the admin notification channel
await self.config.guild(ctx.guild).admin_channel.set(None)
await ctx.send("Admin notification channel removed.")
#
# -------------------------------------------------------------------------------------------------------------
#
@commands.Cog.listener()
async def on_interaction(self, interaction, channel: discord.TextChannel):
if interaction.type == discord.InteractionType.component:
custom_id = interaction.data["custom_id"]
if custom_id.startswith("approve_") or custom_id.startswith("deny_"):
action, user_id, channel_id = custom_id.split("_")
user_id = int(user_id)
channel_id = int(channel_id)
async with self.config.guild(interaction.guild).password_recovery_requests() as requests:
if user_id in requests:
del requests[user_id] # Remove the request
if action == "approve":
temp_code = self.generate_temporary_access_code() # Implement this function
expiry_time = datetime.datetime.now() + datetime.timedelta(hours=1) # 1-hour validity
async with self.config.guild(interaction.guild).temp_access_codes() as temp_codes:
temp_codes[temp_code] = {"user_id": user_id, "channel_id": channel_id, "expiry": expiry_time.isoformat()}
user = self.bot.get_user(user_id)
if user:
await user.send(f"Here's your temporary access code for channel {channel.mention}: `{temp_code}`. It's valid for 1 hour.")
elif user:
await interaction.response.send_message(f"Approved password recovery for {user.mention if user else user_id} on channel {channel.mention}. Temporary code sent.", ephemeral=True)
else:
await interaction.response.send_message("Channel data not found. Cannot reset password.", ephemeral=True)
#
# -------------------------------------------------------------------------------------------------------------
#
# Helper functions
def generate_hashed_password(self, password):
# This remains the same, no changes needed
return hashlib.sha256(password.encode("utf-8")).hexdigest()
#
# -------------------------------------------------------------------------------------------------------------
#
async def set_channel_password(self, ctx: commands.Context, channel_id, password):
"""Sets the password for a channel using the config.
Args:
ctx (commands.Context): The command context to access guild information.
channel_id (int): The ID of the channel.
password (str): The password to set.
"""
hashed_password = self.generate_hashed_password(password)
async with self.config.guild(ctx.guild).password_channels() as password_channels:
password_channels[channel_id] = {
"password_hash": hashed_password
}
#
# -------------------------------------------------------------------------------------------------------------
#
def generate_temporary_access_code(self):
"""Generates a temporary access code."""
characters = string.ascii_letters + string.digits
code = ''.join(random.choice(characters) for i in range(10)) # 10-character code
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"{code}-{timestamp}"
#
# -------------------------------------------------------------------------------------------------------------
#
async def add_to_whitelist(self, ctx, collection, user_or_role):
"""Adds a user or role to the whitelist."""
async with self.config.guild(ctx.guild).whitelist() as whitelist:
user_or_role_id = user_or_role.id
if any(item["id"] == user_or_role_id for item in whitelist):
await ctx.send(f"{user_or_role} is already in the whitelist.")
return
whitelist.append({"id": user_or_role_id, "type": "user" if isinstance(user_or_role, discord.Member) else "role"})
embed = discord.Embed(title="Whitelist Confirmation", description=f"Are you sure you want to add {user_or_role} to the whitelist?")
embed.add_field(name="Action", value="Add to Whitelist")
embed.set_footer(text="React with ✅ to confirm or ❌ to cancel.")
confirmation_msg = await ctx.send(embed=embed)
try:
reaction, user = await ctx.wait_for('reaction_add', timeout=30.0, check=lambda r, u: r.message.id == confirmation_msg.id and u == ctx.author and str(r.emoji) in ['', ''])
if str(reaction.emoji) == '':
await confirmation_msg.delete()
await ctx.send(f"{user_or_role} added to the whitelist.")
else:
await confirmation_msg.delete()
await ctx.send("Action cancelled.")
except asyncio.TimeoutError:
await confirmation_msg.delete()
await ctx.send("Confirmation timed out. Action cancelled.")
#
# -------------------------------------------------------------------------------------------------------------
#
async def remove_from_whitelist(self, ctx, collection, user_or_role):
"""Removes a user or role from the whitelist."""
async with self.config.guild(ctx.guild).whitelist() as whitelist:
user_or_role_id = user_or_role.id
whitelist[:] = [item for item in whitelist if item["id"] != user_or_role_id]
await ctx.send(f"{user_or_role} removed from the whitelist.")
#
# -------------------------------------------------------------------------------------------------------------
#
async def add_to_blacklist(self, ctx, user_or_role):
"""Adds a user or role to the blacklist."""
async with self.config.guild(ctx.guild).blacklist() as blacklist:
user_or_role_id = user_or_role.id
if any(item["id"] == user_or_role_id for item in blacklist):
await ctx.send(f"{user_or_role} is already in the blacklist.")
return
blacklist.append({"id": user_or_role_id, "type": "user" if isinstance(user_or_role, discord.Member) else "role"})
await ctx.send(f"{user_or_role} added to the blacklist.")
#
# -------------------------------------------------------------------------------------------------------------
#
async def remove_from_blacklist(self, ctx, user_or_role):
"""Removes a user or role from the blacklist."""
async with self.config.guild(ctx.guild).blacklist() as blacklist:
user_or_role_id = user_or_role.id
blacklist[:] = [item for item in blacklist if item["id"] != user_or_role_id]
await ctx.send(f"{user_or_role} removed from the blacklist.")
#
# -------------------------------------------------------------------------------------------------------------
#
async def cog_load(self):
self.bot.add_listener(self.on_cog_reload_error, "Package loading failed")
self.bot.add_listener(self.on_cog_reload_error, "SyntaxError:")
self.bot.add_listener(self.on_cog_reload_error, "IndentationError:")
#
# -------------------------------------------------------------------------------------------------------------
#
async def on_cog_reload_error(self, ctx, error):
error_message = f"Error reloading cog '{ctx.cog.qualified_name}':\n```{error}```"
self.error_logs.append(error_message)

6
rpg/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from .rpg import RPG
async def setup(bot):
cog = RPG(bot)
await bot.add_cog(cog)
await bot.add_cog(cog.commands)

126
rpg/actions.py Normal file
View File

@@ -0,0 +1,126 @@
import asyncio
import random
import discord
from .check import Check
from redbot.core import commands, Config
from .inventory import RPGInventory
from typing import Literal
class RPGActions:
"""
Implements the core RPG mechanics and actions.
"""
def __init__(self, rpg_cog):
self.rpg_cog = rpg_cog
self.inventory = RPGInventory(rpg_cog)
async def create_character(self, interaction: discord.Interaction, user, character_name):
"""
Creates a new character for the user.
"""
# Character Name Validation
check = Check(interaction, length=30)
if not check.length_under(interaction.message) or not character_name.isalnum():
return await self.rpg_cog.send_message(interaction, "Invalid character name. Please choose a name between 2 and 30 characters long, containing only letters and numbers.")
# Retrieve the 'characters' group
characters_group = self.rpg_cog.config.member(user).characters
# Then, get all characters within that group (await the .all() call)
existing_characters = await characters_group.all()
# Check for duplicate character names (case-insensitive)
if any(existing_name.lower() == character_name.lower() for existing_name in existing_characters):
return await self.rpg_cog.send_message(interaction, f"You already have a character named '{character_name}'. Choose another name.")
# Retrieve available classes from config
try:
available_classes = await self.rpg_cog.config.guild(user.guild).get_raw("classes", default=[])
except KeyError:
return await self.rpg_cog.send_message(interaction, "No classes have been configured yet. Please contact an admin.")
if not available_classes:
return await self.rpg_cog.send_message(interaction, "No classes are available yet. Please contact an admin.")
# Prompt user to choose a class
class_options = "\n".join([f"{i+1}. {class_name}" for i, class_name in enumerate(available_classes)])
await self.rpg_cog.send_message(interaction, f"Choose a class for your character:\n{class_options}")
def check(m):
return m.author == user and m.channel == interaction.channel and m.content.isdigit() and 1 <= int(m.content) <= len(available_classes)
try:
class_choice = await self.rpg_cog.bot.wait_for("message", check=check, timeout=30.0)
except asyncio.TimeoutError:
return await self.rpg_cog.send_message(interaction, "Class selection timed out. Character creation canceled.")
selected_class = available_classes[int(class_choice.content) - 1]
# Retrieve default stats for the selected class
try:
default_stats = await self.rpg_cog.config.guild(user.guild).get_raw("characters", "classes", selected_class, "stats")
except KeyError:
return await self.rpg_cog.send_message(interaction, f"Default stats for class '{selected_class}' haven't been configured yet. Please contact an admin.")
# Create character data
character_data = {
"name": character_name,
"class": [selected_class], # Store the selected class
"level": 1,
"experience": 0,
"stats": default_stats.copy(),
"inventory": [],
"equipment": {},
"skills": [],
"gold": 0,
"max_health": 100,
"health": 100,
"mana": 50,
"max_mana": 50,
}
# Store character data in config
await characters_group.set_raw(character_name, value=character_data)
await self.rpg_cog.config.member(user).active_character.set(character_name)
await interaction.response.send_message(interaction, f"Character '{character_name}' (Class: {selected_class}) created for {user.mention}!")
async def attack(self, ctx, attacker, target):
"""
Handles the attack action.
"""
# Check if attacker and target have active characters
if not await self._has_active_character(ctx, attacker):
return
if not await self._has_active_character(ctx, target):
return
attacker_data = await self.rpg_cog.config.member(attacker).active_character
target_data = await self.rpg_cog.config.member(target).active_character
# ... (rest of the attack logic)
async def heal(self, ctx, user):
"""
Handles the heal action.
"""
# ... (rest of the heal logic)
# Helper function to check if a user has an active character
async def _has_active_character(self, ctx, user):
active_character = await self.rpg_cog.config.member(user).active_character
if not active_character:
await self.rpg_cog.send_message(ctx, f"{user.mention}, you don't have an active character. Create one using `[p]create_character <name>`.")
return False
return True
async def use_item(self, ctx, user, item_name):
"""
Handles the use of an item from the inventory.
"""
# ... (logic to check if the user has the item and handle its effects)
# Remove the used item from the inventory
await self.inventory.remove_item(ctx, user, item_name)

94
rpg/check.py Normal file
View File

@@ -0,0 +1,94 @@
from redbot.core import Config, commands
from collections.abc import Iterable
import validators as vals
import logging
import discord
log = logging.getLogger("red.rpg")
class Check:
def __init__(self, ctx_or_interaction, custom: Iterable = None, length: int = None):
self.ctx_or_interaction = ctx_or_interaction # Store the context or interaction object
self.custom = custom
self.length = length
def _get_author(self):
"""
Helper function to get the author from either ctx or interaction.
"""
if isinstance(self.ctx_or_interaction, commands.Context):
return self.ctx_or_interaction.author
elif isinstance(self.ctx_or_interaction, discord.Interaction):
return self.ctx_or_interaction.user
else:
log.error(f"Unexpected object type in Check._get_author: {type(self.ctx_or_interaction)}")
return None
def same(self, m):
author = self._get_author()
if author is None:
return False # Handle the case where author couldn't be determined
if isinstance(m, discord.Message):
return author == m.author
elif isinstance(m, discord.Interaction):
return author == m.user
else:
log.error(f"Unexpected object type in Check.same: {type(m)}")
return False
def comfirm(self, m):
return self.same(m) and m.content.lower() in ("yes", "no")
def valid_int(self, m):
return self.same and m.content.isdigit()
def valid_float(self, m):
try:
return self.same(m) and float(m.content) >= 1
except ValueError:
return False
def positive(self, m):
return self.same(m) and m.content.isdigit() and int(m.content) > 0
def role(self, m):
roles = [r.name for r in self.ctx.guild.roles if r.name != "Bot"]
return self.same(m) and m.content in roles
def member(self, m):
return self.same(m) and m.content in [x.name for x in self.ctx.guild.members]
def length_under(self, m):
try:
if isinstance(m, discord.Message):
content = m.content
elif isinstance(m, discord.Interaction):
content = m.data['components'][0]['components'][0]['value'] # Access modal input value
else:
raise ValueError("Unsupported object type for length_under check")
return self.same(m) and len(content) <= self.length
except TypeError:
raise ValueError("Length was not specified in Check")
def valid_image_url(self, m):
url = m.content.strip()
if not vals.url(url):
return False
valid_extensions = (".jpg", ".jpeg", ".png", ".gif")
if not any(url.lower().endswith(ext) for ext in valid_extensions):
return False
return True
def content(self, m):
try:
return self.same(m) and m.content in self.custom
except TypeError:
raise ValueError("A custom iterable was not set in Check")

21
rpg/commands.py Normal file
View File

@@ -0,0 +1,21 @@
import discord
from redbot.core import commands
from .menu import RPGMenu
class RPGCommands(commands.Cog):
"""
Handles user commands (primarily menus) for the RPG system.
"""
def __init__(self, rpg_cog, bot):
self.rpg_cog = rpg_cog
self.bot = bot
super().__init__()
@commands.command(name="menu")
async def menu_command(self, ctx):
"""
Display the RPG menu.
"""
view = RPGMenu(self.rpg_cog)
await ctx.send("Choose an action:", view=view)

11
rpg/info.json Normal file
View File

@@ -0,0 +1,11 @@
{
"author" : ["UnstableKitsune (unstablekitsune)"],
"install _msg" : "Oh you installed me! How dare you obtain me! return me right now!",
"name" : "RPG",
"short" : "Its a TTRPG",
"requirements" : [""],
"permissions" : [""],
"tags" : [""],
"min_python_version" : [3, 1, 1],
"end_user_data_statement" : ""
}

80
rpg/inventory.py Normal file
View File

@@ -0,0 +1,80 @@
import discord
from redbot.core import commands, Config
class RPGInventory:
"""
Manages character inventories for the RPG system.
"""
def __init__(self, rpg_cog):
self.rpg_cog = rpg_cog
super().__init__()
@commands.group()
async def inventory(self, ctx):
"""Main RPG command group."""
pass
@inventory.command(name="add")
async def add_item(self, ctx, user: discord.Member, item_name, quantity=1):
"""
Adds an item to the user's active character's inventory.
"""
character_data = await self.rpg_cog.config.member(user).get_raw("active_character")
if not character_data:
return await self.rpg_cog.send_message(ctx, f"{user.mention}, you don't have an active character.")
inventory = character_data.get("inventory", {})
if item_name in inventory:
inventory[item_name] += quantity
else:
inventory[item_name] = quantity
await self.rpg_cog.config.member(user).set_raw("active_character", "inventory", value=inventory)
await self.rpg_cog.send_message(ctx, f"{quantity} {item_name}(s) added to your inventory!")
@inventory.command(name="remove")
async def remove_item(self, ctx, user: discord.Member, item_name, quantity=1):
"""
Removes an item from the user's active character's inventory.
"""
character_data = await self.rpg_cog.config.member(user).get_raw("active_character")
if not character_data:
return await self.rpg_cog.send_message(ctx, f"{user.mention}, you don't have an active character.")
inventory = character_data.get("inventory", {})
if item_name in inventory:
if inventory[item_name] >= quantity:
inventory[item_name] -= quantity
if inventory[item_name] == 0:
del inventory[item_name]
await self.rpg_cog.config.member(user).set_raw("active_character", "inventory", value=inventory)
await self.rpg_cog.send_message(ctx, f"{quantity} {item_name}(s) removed from your inventory!")
else:
await self.rpg_cog.send_message(ctx, f"You don't have enough {item_name}s.")
else:
await self.rpg_cog.send_message(ctx, f"You don't have any {item_name}s in your inventory.")
@inventory.command(name="bag")
async def get_inventory(self, user):
"""
Retrieves the inventory of the user's active character.
"""
character_data = await self.rpg_cog.config.member(user).get_raw("active_character")
if not character_data:
return None # Or handle the case where the user has no active character
return character_data.get("inventory", {})
@inventory.command(name="showbag")
async def display_inventory(self, ctx, user: discord.Member):
"""
Displays the user's inventory in a user-friendly format.
"""
inventory = await self.get_inventory(user)
if not inventory:
return await self.rpg_cog.send_message(ctx, f"{user.mention}, your inventory is empty!")
# Format the inventory for display (you can customize this)
inventory_str = "\n".join([f"{item}: {quantity}" for item, quantity in inventory.items()])
embed = discord.Embed(title=f"{user.name}'s Inventory", description=inventory_str)
await self.rpg_cog.send_message(ctx, embed=embed)

55
rpg/menu.py Normal file
View File

@@ -0,0 +1,55 @@
import discord
from .check import Check
class RPGMenu(discord.ui.View):
def __init__(self, rpg_cog):
super().__init__()
self.rpg_cog = rpg_cog
@discord.ui.button(label="Create Character", style=discord.ButtonStyle.primary, custom_id="create_character_button")
async def create_character_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_modal(CreateCharacterModal(self.rpg_cog))
@discord.ui.button(label="Attack", style=discord.ButtonStyle.red, custom_id="attack_button")
async def attack_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
# You'll need to implement the target selection logic here (e.g., using a dropdown or another interaction)
target = None # Placeholder for now
if target:
await self.rpg_cog.actions.attack(interaction, interaction.user, target)
else:
await interaction.response.send_message("You need to select a target to attack.", ephemeral=True)
@discord.ui.button(label="Heal", style=discord.ButtonStyle.green, custom_id="heal_button")
async def heal_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.rpg_cog.actions.heal(interaction, interaction.user)
@discord.ui.button(label="Stats", style=discord.ButtonStyle.blurple, custom_id="stats_button")
async def stats_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.rpg_cog.actions.display_stats(interaction, interaction.user)
@discord.ui.button(label="Inventory", style=discord.ButtonStyle.grey, custom_id="inventory_button")
async def inventory_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.rpg_cog.inventory.display_inventory(interaction, interaction.user)
class CreateCharacterModal(discord.ui.Modal):
def __init__(self, rpg_cog):
super().__init__(title="Create Character")
self.rpg_cog = rpg_cog
self.character_name = discord.ui.TextInput(
label="Enter your character's name:",
placeholder="e.g., BraveAdventurer",
required=True,
max_length=30
)
self.add_item(self.character_name)
async def on_submit(self, interaction: discord.Interaction):
character_name = self.character_name.value
#author = interaction.user
try:
await self.rpg_cog.actions.create_character(interaction, interaction.user, character_name)
except Exception as e:
await interaction.response.send_message(f"```An error occurred while creating your character: {e}```")
self.rpg_cog.log.error(f"Error in create_character: {e}")

187
rpg/rpg.py Normal file
View File

@@ -0,0 +1,187 @@
from atexit import register
import logging
import discord
from distutils import config
from redbot.core import commands, Config
from .check import Check
from .commands import RPGCommands
from .actions import RPGActions
from .shop import RPGShops
from .menu import RPGMenu
from .inventory import RPGInventory
class RPG(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.menu = RPGMenu(self)
self.commands = RPGCommands(self, bot)
self.actions = RPGActions(self)
self.shop = RPGShops(self)
self.inventory = RPGInventory(self)
self.log = logging.getLogger("red.rpg")
self.config = Config.get_conf(self, identifier=6857309412)
default_guild_settings = {
"characters": {
"default_stats": { # Nested under 'characters'
"Strength": 1,
"Defense": 1,
"Agility": 1,
"Magic": 1,
"Luck": 1,
"Health": 100,
"Mana": 50,
"Stamina": 50,
"Experience": 0,
"Gold": 0,
"Damage": 1,
"Recovery": 1,
"Resist": 1,
"Critical": 1,
"Speed": 1,
"Level": 1,
"Class": [],
"Skills": [],
"Inventory": [],
"Equipment": {},
"Quests": [],
"Guild": None,
"Location": None,
"Followers": None,
"Pets": None,
},
"default_statsmax": { # Nested under 'characters'
"Strength": 999,
"Defense": 999,
"Agility": 999,
"Magic": 999,
"Luck": 999,
"Health": 999,
"Mana": 999,
"Stamina": 999,
"Experience": 999999999,
"Gold": 999999999,
"Damage": 999,
"Recovery": 999,
"Resist": 999,
"Critical": 999,
"Speed": 999,
"Level": 999999999,
}
},
"classes": { # Definitions of available classes
"Warrior": {
"stats": {
"Strength": 5,
"Defense": 4,
"Agility": 3,
# ... other stats with appropriate values for a warrior
},
"description": "A brave and sturdy warrior, skilled in melee combat."
},
"Mage": {
"stats": {
"Strength": 2,
"Defense": 2,
"Magic": 5,
# ... other stats with appropriate values for a mage
},
"description": "A wise and powerful mage, capable of wielding arcane magic."
},
"Rogue": {
"stats": {
"Strength": 3,
"Defense": 3,
"Agility": 5,
# ... other stats with appropriate values for a rogue
},
"description": "A nimble and cunning rogue, adept at stealth and trickery."
}
# ... add more classes as needed
},
"active_character": {}, # Store the name of the currently active character for each member
}
default_user_settings = {
"active_character": None,
}
default_shop = {
"Items": [],
"Prices": [],
"Currency": "Gold",
"Currency_Symbol": "G",
"Currency_Plural": "Gold",
"Currency_Plural_Symbol": "G",
"Currency_Emoji": "💰",
}
default_quest = {
"Name": None,
"Description": None,
"Reward": None,
"Requirements": None,
"Progress": None,
"Status": None,
"Type": None,
"Repeat": None,
"Time": None,
"Location": None,
"Guild": None,
"Followers": None,
"Pets": None,
}
self.config.register_guild(**default_guild_settings)
self.config.register_user(**default_user_settings)
self.config.register_global(**default_shop)
self.config.register_global(**default_quest)
self.commands = RPGCommands(self, bot)
self.actions = RPGActions(self)
self.shop = RPGShops(self)
self.menu = RPGMenu(self)
self.inventory = RPGInventory(self)
async def register_guild(self, ctx):
await self.config.register_guild(ctx.guild, **default_guild_settings)
await self.send_message(ctx, "Guild registered for RPG system.")
async def register_user(self, ctx):
await self.config.register_user(ctx.author, **default_user_settings)
await self.send_message(ctx, "User registered for RPG system.")
async def register_shop(self, ctx):
await self.config.register_global(**default_shop)
await self.send_message(ctx, "Shop registered for RPG system.")
async def register_quest(self, ctx):
await self.config.register_global(**default_quest)
await self.send_message(ctx, "Quest registered for RPG system.")
async def unregister_guild(self, ctx):
await self.config.unregister_guild(ctx.guild)
await self.send_message(ctx, "Guild unregistered for RPG system.")
async def unregister_user(self, ctx):
await self.config.unregister_user(ctx.author)
await self.send_message(ctx, "User unregistered for RPG system.")
async def unregister_shop(self, ctx):
await self.config.unregister_global("Shop")
await self.send_message(ctx, "Shop unregistered for RPG system.")
async def unregister_quest(self, ctx):
await self.config.unregister_global("Quest")
await self.send_message(ctx, "Quest unregistered for RPG system.")
async def send_message(self, ctx, message, interaction, embed=None):
if embed:
await ctx.send(message, embed=embed)
elif interaction:
await interaction.response.send(message)
else:
await ctx.send(message)

176
rpg/shop.py Normal file
View File

@@ -0,0 +1,176 @@
from ast import alias
from enum import member
from locale import currency
import discord
from redbot.core import commands
from .inventory import RPGInventory
class RPGShops(commands.Cog):
def __init__(self, rpg_cog):
self.rpg_cog = rpg_cog
self.config = self.rpg_cog
default_guild = {
"shops": {},
"status": {
"active": bool,
"deactive": bool,
},
"currency": {
"name": str,
"symbol": str,
"emoji": str,
}
}
#self.config.register_guild(**default_guild_settings)
# -----------------------------------------------------------------------------------------
# --------------- SHOP GROUP COMMANDS -----------------------------------------------------
# -----------------------------------------------------------------------------------------
@commands.group(name="shop", alias=["sp"])
async def shop(self, ctx):
pass
# Comes from Base Shop Group Above
@shop.group(name="manager", alias=["mgr"])
async def manager(self, ctx):
pass
# Comes from Manager Group Above
@manager.group(name="set", alias=["st"])
async def set(self, ctx):
pass
# Comes From Set Group From Above
@set.group(name="status", alias=["sta"])
async def status(self, ctx):
pass
# Comes From Set Group From Above
@set.group(name="currency", alias=["cur"])
async def currency(self, ctx):
pass
# -----------------------------------------------------------------------------------------
# ------------------ SHOP BASE COMMANDS --------------------------------------------------
# -----------------------------------------------------------------------------------------
@shop.command(name="buy", alias=["by"]) # shop buy
async def buy(self, ctx, shop_name: str, item_name: str, quantity: int = 1):
"""
Buy an item from the shop.
"""
# Retrieve shop data
shops = await self.config.guild(ctx.guild).shops.all()
if shop_name not in shops:
return await ctx.send(f"Shop '{shop_name}' not found.")
shop_data = shops[shop_name]
items = shop_data.get("items", {}) # Assuming you store items in an 'items' dictionary within the shop data
if item_name not in items:
return await ctx.send(f"Item '{item_name}' not found in '{shop_name}'.")
item_data = items[item_name]
price = item_data.get("price")
stock = item_data.get("quantity")
# Check if the shop has enough stock
if stock is not None and stock < quantity:
return await ctx.send(f"Not enough '{item_name}' in stock. Only {stock} available.")
# Check if the user has enough currency
currency_name = await self.config.guild(ctx.guild).shops_currency_name.get()
user_balance = await self.rpg_cog.config.member(ctx.author).get_raw("gold") # Assuming gold is the currency
total_cost = price * quantity
if user_balance < total_cost:
return await ctx.send(f"You don't have enough {currency_name} to buy {quantity} {item_name}(s).")
# Deduct currency and add item to inventory
await self.rpg_cog.config.member(ctx.author).set_raw("gold", value=user_balance - total_cost)
await self.rpg_cog.inventory.add_item(ctx, ctx.author, item_name, quantity)
# Update shop inventory (if applicable)
if stock is not None:
shop_data["items"][item_name]["quantity"] -= quantity
await self.config.guild(ctx.guild).shops.set_raw(shop_name, value=shop_data)
await ctx.send(f"You bought {quantity} {item_name}(s) for {total_cost} {currency_name}!")
@shop.command(name="sell", alias=["sl"]) # shop sell
async def buy(self, ctx, item_name: str):
"""
Sell an item back to the shop.
"""
# ... (logic to handle the purchase and currency)
# Add the purchased item to the user's inventory
await self.inventory.remove_item(ctx, ctx.author, item_name)
@shop.command(name="give", alias=["gi"]) # shop give
async def give(self, ctx, item_name: str, target: member):
"""
Give an item to another player.
"""
# ... (logic to handle the transfer of items between players)
await self.inventory.remove_item(ctx, ctx.author, item_name)
await self.inventory.add_item(ctx, target, item_name)
@shop.command(name="list", alias=["ls"]) # shop list
async def list(self, ctx, shop_name: str):
"""
List the items available in a shop.
"""
# ... (logic to retrieve and display the items in the shop)
items = await self.rpg_cog.config.guild(ctx.guild).shops_items.get_raw(shop_name)
prices = await self.rpg_cog.config.guild(ctx.guild).shops_prices.get_raw(shop_name)
if items:
item_list = "\n".join([f"{item} - {price}" for item, price in zip(items, prices)])
await ctx.send(f"Items available in {shop_name}:\n{item_list}")
else:
await ctx.send(f"No items available in {shop_name}.")
# -----------------------------------------------------------------------------------------
# ------------------------- Manager Base Commands -----------------------------------------
# -----------------------------------------------------------------------------------------
@manager.command(name="create", alias=["cr"])
async def create_shop(self, ctx, shop_name: str, *, description: str):
await ctx.send(f"Shop '{shop_name}' created!")
@manager.command(name="delete", alias=["del"])
async def delete_shop(self, ctx, shop_name: str):
await ctx.send(f"Shop '{shop_name}' deleted!")
# -----------------------------------------------------------------------------------------
# --------------------------- SET BASE GROUP ----------------------------------------------
# -----------------------------------------------------------------------------------------
# --------------------------- STATUS BASE COMMANDS ----------------------------------------
# -----------------------------------------------------------------------------------------
@status.command(name="active", alias=["act"])
async def active_shop(self, ctx, shop_name: str):
await ctx.send(f"Shop '{shop_name}' activated!")
@status.command(name="deactive", alias=["deact"])
async def deactive_shop(self, ctx, shop_name: str):
await ctx.send(f"Shop '{shop_name}' deactivated!")
# -----------------------------------------------------------------------------------------
# --------------------------- CURRENCY BASE COMMANDS --------------------------------------
# -----------------------------------------------------------------------------------------
@currency.command(name="name", alias=["nm"]) # Use @currency.command
async def name_currency(self, ctx, currency_name: str):
await ctx.send(f"Currency name set to '{currency_name}'!")
@currency.command(name="symbol", alias=["sym"]) # Use @currency.command
async def symbol_currency(self, ctx, currency_symbol: str):
await ctx.send(f"Currency symbol set to '{currency_symbol}'!")
@currency.command(name="emoji", alias=["emo"]) # Use @currency.command
async def emoji_currency(self, ctx, currency_emoji: str):
await ctx.send(f"Currency emoji set to '{currency_emoji}'!")

15
servicereview/info.json Normal file
View File

@@ -0,0 +1,15 @@
{
"author": [ "unstableCogs" ],
"install_msg": "Thank you for installing the Service Review cog!",
"name": "ServiceReview",
"short": "A command for users to leave a service review.",
"description": "Allows users to submit a service review at any time, independently of any ticket system. Submissions are sent to a designated channel for staff.",
"tags": [
"review",
"service",
"feedback",
"utility"
],
"requirements": [ "rich" ],
"end_user_data_statement": "This cog persistently stores the user's ID and the content of their submitted review for record-keeping purposes."
}

View File

82
welcomer/README.md Normal file
View File

@@ -0,0 +1,82 @@
# Welcomer Cog - ver-1.0.0
A configurable cog to automatically welcome new users when they join your server.
# Features
- Fully Configurable: Set a custom welcome message and channel for each server.
- Enable/Disable: Easily toggle the welcomer on or off without losing your settings.
- Placeholder Support: Personalize your welcome message with user and server details.
- Easy Setup: A simple command group for admins to manage all settings.
- Permissions: All settings commands require `Manage Server` permissions to use.
# Commands
All configuration is handled through the `[p]welcomeset` command group.
[p]welcomeset channel <#channel>
Sets the channel where welcome messages will be sent.
Alias: chnl
Example: [p]welcomeset channel #welcome
[p]welcomeset message <message>
Sets the custom welcome message. See the "Placeholders" section below for available variables.
Alias: msg
Example: [p]welcomeset message Welcome {user.mention} to {server_name}! We're glad you're here.
[p]welcomeset toggle
Toggles the welcomer system on or off for the server.
Aliases: on, off
Example: [p]welcomeset toggle
[p]welcomeset settings
Displays the current settings for the welcomer in an embed.
Aliases: show, status
Example: [p]welcomeset settings
[p]welcomeset test
Sends a preview of the current welcome message to the channel where the command is run.
Example: [p]welcomeset test
[p]welcomeset reset
Resets all welcomer settings for the server to their default values.
Example: [p]welcomeset reset
Quick Setup Guide
Load the Cog:
[p]load welcomer
Set the Welcome Channel:
[p]welcomeset channel #your-welcome-channel
Set Your Custom Message:
[p]welcomeset message Welcome, {user.mention}! Enjoy your stay in {server_name}!
Enable the System:
[p]welcomeset toggle
The bot will now welcome new members in the channel you specified. You can use [p]welcomeset settings at any time to check your configuration.
Placeholders for the Welcome Message
You can use the following placeholders in your custom welcome message. They will be automatically replaced with the correct information when a new user joins.
{user}: The user object itself.
{user.mention}: Pings the new user (e.g., @UnstableKitsune).
{user_name}: The new user's name (e.g., UnstableKitsune).
{server_name}: The name of the server they joined.
For full documentation, please visit the [repository wiki](https://git.kitsunic.org/kitsunicWorks/unstable-cogs/wiki).

1
welcomer/__init__.py Normal file
View File

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

14
welcomer/info.json Normal file
View File

@@ -0,0 +1,14 @@
{
"author": [ "unstableCogs" ],
"install_msg": "Thank you for installing the Welcomer cog!",
"name": "Welcomer",
"short": "A simple cog to welcome new users.",
"description": "Greets new members in a designated channel with a customizable message. This cog is part of the Unified Bot Suite.",
"tags": [
"welcome",
"utility",
"greeting"
],
"requirements": [],
"end_user_data_statement": "This cog does not persistently store any end user data."
}

195
welcomer/welcomer.py Normal file
View File

@@ -0,0 +1,195 @@
# welcomer.py
import discord
from redbot.core import commands, Config
from redbot.core.bot import Red
class Welcomer(commands.Cog):
"""
A configurable cog to welcome new users to a server.
"""
def __init__(self, bot: Red):
self.bot = bot
# Initialize Red's Config system. This will store our settings.
# The "GUILD" identifier means settings will be saved on a per-server basis.
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
# Define the default settings for each server.
default_guild = {
"welcome_channel": None, # The ID of the channel to send welcomes to
"welcome_message": "Welcome to the server, {user.mention}!", # Default message
"enabled": False # Whether the system is on or off
}
# Register the default settings.
self.config.register_guild(**default_guild)
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
"""
The event listener that runs when a new member joins.
"""
guild = member.guild
# Check if the welcomer is enabled for this server.
if not await self.config.guild(guild).enabled():
return
# Get the welcome channel ID from our saved settings.
channel_id = await self.config.guild(guild).welcome_channel()
if not channel_id:
return # If no channel is set, do nothing.
# Try to find the channel object in the server.
channel = guild.get_channel(channel_id)
if not channel:
# The channel might have been deleted.
return
# Explicitly check if the channel is a TextChannel before trying to send a message.
if not isinstance(channel, discord.TextChannel):
print(f"Welcomer: Configured channel '{channel.name}' in {guild.name} is not a text channel.")
return
# Get the custom welcome message from our settings.
message_template = await self.config.guild(guild).welcome_message()
# Format the message with the new member's info.
# .format() is a safe way to replace placeholders.
formatted_message = message_template.format(
user=member,
user_mention=member.mention,
user_name=member.name,
server_name=guild.name
)
# Check if we have permission to send messages in the channel.
if not channel.permissions_for(guild.me).send_messages:
# We can't send a message, so we'll just log this internally.
print(f"Welcomer: No permission to send messages in {channel.name} in {guild.name}.")
return
try:
await channel.send(formatted_message)
except discord.Forbidden:
# This is a final safety check in case permissions change suddenly.
pass
except discord.HTTPException as e:
# This can happen if the message is too long or there's a Discord API error.
print(f"Welcomer: Failed to send welcome message in {guild.name}: {e}")
# Create a command group for all our settings commands.
@commands.group(aliases=["wset"]) # type: ignore
@commands.guild_only() # Ensures this command and its subcommands can only be run in a server
@commands.admin_or_permissions(manage_guild=True)
async def welcomeset(self, ctx: commands.Context):
"""
Configure the welcomer settings for this server.
"""
pass
@welcomeset.command(name="channel", aliases=["chnl"])
async def welcomeset_channel(self, ctx: commands.Context, channel: discord.TextChannel):
"""
Set the channel where welcome messages will be sent.
Example:
[p]welcomeset channel #welcome
"""
if not ctx.guild:
return # This check satisfies the type checker
await self.config.guild(ctx.guild).welcome_channel.set(channel.id)
await ctx.send(f"The welcome channel has been set to {channel.mention}.")
@welcomeset.command(name="message", aliases=["msg"])
async def welcomeset_message(self, ctx: commands.Context, *, message: str):
"""
Set the custom welcome message.
You can use these placeholders:
{user} - The user object.
{user_mention} - Pings the user.
{user_name} - The user's name.
{server_name} - The name of this server.
Example:
[p]welcomeset message Hello {user_mention}, welcome to {server_name}!
"""
if not ctx.guild:
return
await self.config.guild(ctx.guild).welcome_message.set(message)
await ctx.send(f"The welcome message has been updated.")
# Send a preview of the new message.
preview = message.format(
user=ctx.author,
user_mention=ctx.author.mention,
user_name=ctx.author.name,
server_name=ctx.guild.name
)
await ctx.send(f"**Preview:**\n{preview}")
@welcomeset.command(name="toggle", aliases=["on", "off"])
async def welcomeset_toggle(self, ctx: commands.Context):
"""
Enable or disable the welcomer 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 welcomer system has been {status_text}.")
@welcomeset.command(name="settings", aliases=["show", "status"])
async def welcomeset_settings(self, ctx: commands.Context):
"""
Show the current welcomer settings for this server.
"""
if not ctx.guild:
return
settings = await self.config.guild(ctx.guild).all()
channel_id = settings['welcome_channel']
channel = ctx.guild.get_channel(channel_id) if channel_id else None
message = settings['welcome_message']
enabled = "Enabled" if settings['enabled'] else "Disabled"
embed = discord.Embed(title="Welcomer Settings", color=await ctx.embed_color())
embed.add_field(name="Status", value=enabled, inline=False)
embed.add_field(name="Channel", value=channel.mention if isinstance(channel, discord.TextChannel) else "Not Set", inline=False)
embed.add_field(name="Message", value=f"```{message}```", inline=False)
await ctx.send(embed=embed)
@welcomeset.command(name="test")
async def welcomeset_test(self, ctx: commands.Context):
"""
Test the welcome message by sending a preview to this channel.
"""
if not ctx.guild:
return
message_template = await self.config.guild(ctx.guild).welcome_message()
preview = message_template.format(
user=ctx.author,
user_mention=ctx.author.mention,
user_name=ctx.author.name,
server_name=ctx.guild.name
)
await ctx.send(f"**Welcome Message Preview**:\n{preview}")
@welcomeset.command(name="reset")
async def welcomeset_reset(self, ctx: commands.Context):
"""
Reset all welcomer settings to their defaults.
"""
if not ctx.guild:
return
await self.config.guild(ctx.guild).clear()
await ctx.send("The welcomer settings have been reset to their defaults.")
# This function allows Red to load the cog.
# It is required in every cog file.
async def setup(bot: Red):
await bot.add_cog(Welcomer(bot))