diff --git a/modmail/README.md b/modmail/README.md new file mode 100644 index 0000000..e69de29 diff --git a/modmail/__init__.py b/modmail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modmail/info.json b/modmail/info.json new file mode 100644 index 0000000..995c543 --- /dev/null +++ b/modmail/info.json @@ -0,0 +1,16 @@ +{ + "author": [ "unstableCogs" ], + "install_msg": "Thank you for installing the ModMail cog! Please use `[p]help ModMail` for setup commands.", + "name": "ModMail", + "short": "A private, forum-based ModMail system.", + "description": "A comprehensive ModMail system that relays user DMs to a private forum channel for staff to review and respond to. This serves as the core for all ticket-based interactions.", + "tags": [ + "modmail", + "moderation", + "support", + "tickets", + "utility" + ], + "requirements": [ "rich" ], + "end_user_data_statement": "This cog persistently stores user IDs and message content from ModMail threads for moderation and support history." +} \ No newline at end of file diff --git a/modmail/modmail.py b/modmail/modmail.py new file mode 100644 index 0000000..e69de29 diff --git a/mors/README.md b/mors/README.md new file mode 100644 index 0000000..e69de29 diff --git a/mors/__init__.py b/mors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mors/info.json b/mors/info.json new file mode 100644 index 0000000..b0c5a76 --- /dev/null +++ b/mors/info.json @@ -0,0 +1,16 @@ +{ + "author": [ "unstableCogs" ], + "install_msg": "Thank you for installing the Mass Outreach & Review System! For a full command list, please see the wiki.", + "name": "MassOutreach", + "short": "A multi-step system for mass outreach and reviews.", + "description": "Manages a complete workflow for user outreach. The process begins with /game, uses /over for time selection, and concludes with a /pudding-head command for the user to submit a mass review.", + "tags": [ + "mass", + "outreach", + "review", + "tickets", + "utility" + ], + "requirements": [ "rich" ], + "end_user_data_statement": "This cog stores user IDs and ticket information temporarily for the duration of the outreach process. Submitted reviews are stored persistently." +} \ No newline at end of file diff --git a/mors/mors.py b/mors/mors.py new file mode 100644 index 0000000..e69de29 diff --git a/pp/__init__.py b/pp/__init__.py new file mode 100644 index 0000000..6277d92 --- /dev/null +++ b/pp/__init__.py @@ -0,0 +1,5 @@ +from .passwdprotect import Passwd + +async def setup(bot): + cog = Passwd(bot) + await bot.add_cog(cog) diff --git a/pp/info.json b/pp/info.json new file mode 100644 index 0000000..a407a96 --- /dev/null +++ b/pp/info.json @@ -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" +} diff --git a/pp/passwdprotect.py b/pp/passwdprotect.py new file mode 100644 index 0000000..d19e936 --- /dev/null +++ b/pp/passwdprotect.py @@ -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) \ No newline at end of file diff --git a/rpg/__init__.py b/rpg/__init__.py new file mode 100644 index 0000000..a963130 --- /dev/null +++ b/rpg/__init__.py @@ -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) \ No newline at end of file diff --git a/rpg/actions.py b/rpg/actions.py new file mode 100644 index 0000000..29472be --- /dev/null +++ b/rpg/actions.py @@ -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 `.") + 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) \ No newline at end of file diff --git a/rpg/check.py b/rpg/check.py new file mode 100644 index 0000000..e096dc2 --- /dev/null +++ b/rpg/check.py @@ -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") \ No newline at end of file diff --git a/rpg/commands.py b/rpg/commands.py new file mode 100644 index 0000000..f7ac838 --- /dev/null +++ b/rpg/commands.py @@ -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) \ No newline at end of file diff --git a/rpg/info.json b/rpg/info.json new file mode 100644 index 0000000..253e79d --- /dev/null +++ b/rpg/info.json @@ -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" : "" +} \ No newline at end of file diff --git a/rpg/inventory.py b/rpg/inventory.py new file mode 100644 index 0000000..87c8580 --- /dev/null +++ b/rpg/inventory.py @@ -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) \ No newline at end of file diff --git a/rpg/menu.py b/rpg/menu.py new file mode 100644 index 0000000..7f4eb47 --- /dev/null +++ b/rpg/menu.py @@ -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}") \ No newline at end of file diff --git a/rpg/rpg.py b/rpg/rpg.py new file mode 100644 index 0000000..336c0fa --- /dev/null +++ b/rpg/rpg.py @@ -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) \ No newline at end of file diff --git a/rpg/shop.py b/rpg/shop.py new file mode 100644 index 0000000..27b1995 --- /dev/null +++ b/rpg/shop.py @@ -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}'!") diff --git a/servicereview/README.md b/servicereview/README.md new file mode 100644 index 0000000..e69de29 diff --git a/servicereview/__init__.py b/servicereview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/servicereview/info.json b/servicereview/info.json new file mode 100644 index 0000000..5ca3a70 --- /dev/null +++ b/servicereview/info.json @@ -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." +} \ No newline at end of file diff --git a/servicereview/iservice.py b/servicereview/iservice.py new file mode 100644 index 0000000..e69de29