diff --git a/.gitignore b/.gitignore index 7d6881d..318191a 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -/dolly.db \ No newline at end of file +/dolly.db +/nessa.db \ No newline at end of file diff --git a/Dolly.png b/Dolly.png deleted file mode 100644 index e1f9b1c..0000000 Binary files a/Dolly.png and /dev/null differ diff --git a/Nessa.ico b/Nessa.ico new file mode 100644 index 0000000..33ba1ab Binary files /dev/null and b/Nessa.ico differ diff --git a/Nessa.png b/Nessa.png new file mode 100644 index 0000000..8342f1b Binary files /dev/null and b/Nessa.png differ diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..1a6305c --- /dev/null +++ b/README.MD @@ -0,0 +1,6 @@ +# Nessa + +![Nessa](/Nessa.png) + +# About Me: +Hey there! I am Mnemosyne, better known as Nessa! I am here to help you track your projects and task in Discord. Thanks for checking out my services! diff --git a/main.py b/main.py index c8e693c..d79672e 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -from dolly.dolly import Dolly -from dolly.database import init_db +from nessa.nessa import Nessa +from nessa.database import init_db import asyncio from dotenv import load_dotenv import os @@ -10,7 +10,7 @@ TOKEN = os.getenv("DISCORD_BOT_TOKEN") async def main(): await init_db() - client = Dolly() + client = Nessa() await client.start(TOKEN) asyncio.run(main()) \ No newline at end of file diff --git a/dolly/commands.py b/nessa/commands.py similarity index 50% rename from dolly/commands.py rename to nessa/commands.py index 070151b..facce84 100644 --- a/dolly/commands.py +++ b/nessa/commands.py @@ -2,6 +2,7 @@ import discord from discord import app_commands from .database import add_project, get_project_id, get_project_name, add_task_to_project, update_task, list_projects, list_tasks_for_project, remove_task, remove_project from .consent import check_user_consent, store_user_consent, revoke_user_consent +from .confirm import ConfirmView, ConfirmTaskDeletionView from datetime import datetime import logging from dotenv import load_dotenv @@ -11,7 +12,7 @@ load_dotenv() contact_user_id = int(os.getenv("AUTHORIZED_USER_ID")) -logger = logging.getLogger('Dolly') +logger = logging.getLogger('Nessa') class ProjectCommands(app_commands.Group): def __init__(self): @@ -21,9 +22,9 @@ class ProjectCommands(app_commands.Group): async def create_project(self, interaction: discord.Interaction, name: str, description: str): try: project_id = await add_project(name, description) - await interaction.response.send_message(f"Project '{name}' created successfully with ID: {project_id}.") + await interaction.response.send_message(f"Noted. I've created a new project called '{name}'. You can look it up using the `/project list` command. Your project ID is {project_id}.") except Exception as e: - await interaction.response.send_message("An error occurred while creating the project.", ephemeral=True) + await interaction.response.send_message("Whoops! Looks like we hit a snag! Try creating the project again! :)", ephemeral=True) logger.error(f"Error in create_project: {e}") @app_commands.command(name="list", description="List all projects.") @@ -32,22 +33,23 @@ class ProjectCommands(app_commands.Group): projects = await list_projects() if projects: message = "\n".join([f"ID: {id}, Name: {name}, Description: {description}" for id, name, description in projects]) - await interaction.response.send_message(f"Projects:\n{message}") + await interaction.response.send_message(f"Here's the list of Projects:\n{message} \nIf you'd like to add a new project, use the `/project create` command.") else: - await interaction.response.send_message("No projects found.") + await interaction.response.send_message("Mmmm, I couldn't find any projects. If you'd like to add one, use the `/project create` command.") except Exception as e: - await interaction.response.send_message("Failed to retrieve projects.", ephemeral=True) + await interaction.response.send_message("Whoops! I failed to retrieve the list of projects. Did I misplace that database somewhere? Try again later!.", ephemeral=True) logger.error(f"Error in list_projects: {e}") @app_commands.command(name="remove", description="Remove a specific project and all its tasks.") async def remove_project_command(self, interaction: discord.Interaction, project_id: int): + # Send a message with the confirmation view try: - await remove_project(project_id) - await interaction.response.send_message(f"Project ID {project_id} and all associated tasks have been successfully removed.") - logger.info(f"Project ID {project_id} and all associated tasks removed successfully.") + view = ConfirmView(project_id) + await interaction.response.send_message(f"Hey there! Are you sure you wanna me to forgot the project with ID {project_id}? All associated tasks will be removed as well. Once I remove the project, I won't be able to retrieve it again. You have 180 seconds to confirm.", view=view, ephemeral=True) except Exception as e: - await interaction.response.send_message("Failed to remove the project.", ephemeral=True) - logger.error(f"Error in remove_project_command: {e}") + await interaction.response.send_message(f"Whoops! I failed to remove the project. Error: {e}", ephemeral=True) + logger.error(f"Preparation error in remove_project_command for Project ID {project_id}: {e}") + class TaskCommands(app_commands.Group): def __init__(self): @@ -60,16 +62,16 @@ class TaskCommands(app_commands.Group): project_id = await get_project_id(project_name) if project_id: await add_task_to_project(project_id, description, assignee, deadline, status, priority) - await interaction.response.send_message(f"Task '{description}' added to project '{project_name}'.") + await interaction.response.send_message(f"Okay so we are adding the task called '{description}' to project '{project_name}'. got it!. Make sure you filled out your project and task details correctly. \nIf you need to update the task, use the `/task update` command.") logger.info(f"Task added to project {project_name}: {description}") else: - await interaction.response.send_message(f"Project '{project_name}' not found.", ephemeral=True) + await interaction.response.send_message(f"Whoops! I don't have that project named '{project_name}'. Did you mean to use the `/project create` command? or did you enter the name wrong?", ephemeral=True) logger.warning(f"Attempted to add task to non-existent project: {project_name}") except ValueError: - await interaction.response.send_message("Invalid date format. Please use MM/DD/YYYY format.", ephemeral=True) + await interaction.response.send_message("Silly! I know that's not a date! Please use the right format, i.e. MM/DD/YYYY format!", ephemeral=True) logger.error(f"Invalid date format provided by user: {deadline}") except Exception as e: - await interaction.response.send_message("An error occurred while adding the task.", ephemeral=True) + await interaction.response.send_message("Huh, that's never happened before... Try again!", ephemeral=True) logger.error(f"Unexpected error in add_task: {e}") @app_commands.command(name="update", description="Update an existing task.") @@ -77,13 +79,13 @@ class TaskCommands(app_commands.Group): try: datetime.strptime(deadline, "%m/%d/%Y") # Validate date format await update_task(task_id, description, assignee, deadline, status, priority) - await interaction.response.send_message(f"Task ID {task_id} updated successfully.") + await interaction.response.send_message(f"Okay hun, so I updated the task with ID {task_id} with the changes you wanted made!") logger.info(f"Task ID {task_id} updated: {description}") except ValueError: - await interaction.response.send_message("Invalid date format. Please use MM/DD/YYYY format.", ephemeral=True) + await interaction.response.send_message("Silly! I know that's not a date! Please use the right format, i.e. MM/DD/YYYY format!", ephemeral=True) logger.error(f"Invalid date format provided by user for task update: {deadline}") except Exception as e: - await interaction.response.send_message("An error occurred while updating the task.", ephemeral=True) + await interaction.response.send_message("Huh, that's never happened before... Try again!", ephemeral=True) logger.error(f"Unexpected error in update_task_command: {e}") @app_commands.command(name="list", description="List tasks for a project.") @@ -91,53 +93,50 @@ class TaskCommands(app_commands.Group): try: project_name = await get_project_name(project_id) if not project_name: - await interaction.response.send_message(f"No project found with ID {project_id}.", ephemeral=True) + await interaction.response.send_message(f"I may be the godesss of memory, but I don't have a project with that ID {project_id} in my memory. are you sure you entered the right ID?", ephemeral=True) return tasks = await list_tasks_for_project(project_id) if tasks: message = "\n".join([f"ID: {id}, Description: {description}, Assignee: {assignee}, Deadline: {deadline}, Status: {status}, Priority: {priority}" for id, description, assignee, deadline, status, priority in tasks]) - await interaction.response.send_message(f"Tasks for Project '{project_name}':\n{message}") + await interaction.response.send_message(f"So, Here are all the tasks I found for that project '{project_name}':\n{message}") else: - await interaction.response.send_message(f"No tasks found for project '{project_name}'.") + await interaction.response.send_message(f"Whoops! I couldn't find any tasks for that project '{project_name}' Is it brand new?.") except Exception as e: - await interaction.response.send_message(f"Failed to retrieve tasks for project ID {project_id}.", ephemeral=True) + await interaction.response.send_message(f"My memory must be slipping, I think I couldn't find any tasks for that project {project_id}.", ephemeral=True) logger.error(f"Error in list_tasks: {e}") @app_commands.command(name="remove", description="Remove a specific task.") async def remove_task_command(self, interaction: discord.Interaction, task_id: int): - try: - await remove_task(task_id) - await interaction.response.send_message(f"Task ID {task_id} has been successfully removed.") - logger.info(f"Task ID {task_id} removed successfully.") - except Exception as e: - await interaction.response.send_message("Failed to remove the task.", ephemeral=True) - logger.error(f"Error in remove_task_command: {e}") + # Send a message with the confirmation view + view = ConfirmTaskDeletionView(task_id) + await interaction.response.send_message(f"So, are you sure you want to remove Task ID {task_id}? Once I forget, that's it! You'll have 180 seconds to confirm.", view=view, ephemeral=True) + class ConsentCommands(app_commands.Group): def __init__(self): super().__init__(name="consent", description="Manage data consent settings.") - @app_commands.command(name="opt-in", description="Opt-in to data storage and use the bot.") + @app_commands.command(name="opt-in", description="Opt-in to data storage and use Nessa's Services.") async def opt_in(self, interaction: discord.Interaction): await store_user_consent(interaction.user.id) - await interaction.response.send_message("You have opted in to data storage. You can now use the bot.", ephemeral=True) + await interaction.response.send_message("Hey there! Thanks for opt-ing in to allowing me to store the data for your projects.", ephemeral=True) - @app_commands.command(name="opt-out", description="Opt-out of data storage and stop using the bot.") + @app_commands.command(name="opt-out", description="Opt-out of data storage and stop using Nessa's Services.") async def opt_out(self, interaction: discord.Interaction): await revoke_user_consent(interaction.user.id) - await interaction.response.send_message("You have opted out of data storage. You will no longer be able to use the bot.", ephemeral=True) + await interaction.response.send_message("Awe. It's ok. I won't store your data but this also means you won't be able to use my services. You can always opt-in again. Bye!", ephemeral=True) - @app_commands.command(name="privacy-policy", description="View the bot's data handling policy.") + @app_commands.command(name="privacy-policy", description="View Nessa's data handling policy.") async def privacy_policy(self, interaction: discord.Interaction): # Replace 'YOUR_USER_ID' with the actual numeric ID of the user 'advtech' - policy_details = f"""Your data, including your Discord ID, task entries, and other related data, is collected for the purpose of providing task management functionalities. All data is stored securely and is not shared with third parties. You can withdraw your consent at any time by using the `/opt-out` command. For any data requests or inquiries, please contact <@{contact_user_id}> on Discord.""" + policy_details = f"""Hi there! So you're looking to hire Nessa, the godess of memory! Let me tell you what you need to know. You need to consent to me storing what you send me in my 'memory' (better known as a database). What I am storing is: Your Disocrd ID, Projects, Tasks, and other data related to your projects. Why I need it? Well, I need your ID so as track your consent to store your data. If I don't have your consent, You can't use my services and your data isn't stored. As far as project and task management is concerned, I store just what you send me. I do not share it with any third-parties. I do not use any external services. I store it in my local 'memory'. Your data is not used for anything else, including but not limited to machine learning. Your data can be removed at any time using the commands. If you wish to opt-out, please use the `/consent opt-out` command. You need anything else, talk to my boss: <@{contact_user_id}> on Discord.""" await interaction.response.send_message(policy_details, ephemeral=True) -class DollyTracker(app_commands.Group, name="dolly", description="Dolly the Project Tracker."): +class NessaTracker(app_commands.Group, name="nessa", description="Nessa the Project Tracker."): def __init__(self): super().__init__() self.add_command(ProjectCommands()) diff --git a/nessa/confirm.py b/nessa/confirm.py new file mode 100644 index 0000000..5ac0cb5 --- /dev/null +++ b/nessa/confirm.py @@ -0,0 +1,48 @@ +from discord.ui import View, Button + +class ConfirmView(View): + def __init__(self, project_id, timeout=180): # 180 seconds until the view expires + super().__init__(timeout=timeout) + self.project_id = project_id + self.value = None + + @discord.ui.button(label="Confirm", style=discord.ButtonStyle.danger) + async def confirm(self, interaction: discord.Interaction, button: Button): + # Attempt to remove the project + try: + await remove_project(self.project_id) + await interaction.response.edit_message(content=f"Project ID {self.project_id} and all associated tasks have been successfully removed.", view=None) + logger.info(f"Project ID {self.project_id} and all associated tasks removed successfully.") + except Exception as e: + error_message = f"Failed to remove the project. Error: {e}" + await interaction.response.edit_message(content=error_message, view=None) + logger.error(f"Error in remove_project_command: {e}") + self.stop() # Stopping the view after an error is usually a good practice to clean up the UI. + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary) + async def cancel(self, interaction: discord.Interaction, button: Button): + await interaction.response.edit_message(content="Project removal cancelled.", view=None) + self.stop() + +class ConfirmTaskDeletionView(View): + def __init__(self, task_id, timeout=180): # 180 seconds until the view expires + super().__init__(timeout=timeout) + self.task_id = task_id + self.value = None + + @discord.ui.button(label="Confirm", style=discord.ButtonStyle.danger) + async def confirm(self, interaction: discord.Interaction, button: Button): + # Attempt to remove the task + try: + await remove_task(self.task_id) + await interaction.response.edit_message(content=f"Task ID {self.task_id} has been successfully removed.", view=None) + logger.info(f"Task ID {self.task_id} removed successfully.") + except Exception as e: + error_message = f"Failed to remove the task. Error: {e}" + await interaction.response.edit_message(content=error_message, view=None) + logger.error(f"Error in remove_task_command: {e}") + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary) + async def cancel(self, interaction: discord.Interaction, button: Button): + await interaction.response.edit_message(content="Task removal cancelled.", view=None) + self.stop() \ No newline at end of file diff --git a/dolly/consent.py b/nessa/consent.py similarity index 95% rename from dolly/consent.py rename to nessa/consent.py index 2788e17..89b8c6f 100644 --- a/dolly/consent.py +++ b/nessa/consent.py @@ -19,7 +19,7 @@ class ConsentView(View): @discord.ui.button(label="Decline Data Storage", style=discord.ButtonStyle.grey) async def cancel(self, interaction: discord.Interaction, button: Button): self.value = False - await interaction.response.edit_message(content="You have declined data storage. You cannot use this bot without consenting.", view=None) + await interaction.response.edit_message(content="You have declined data storage. Since you have, You can't use my services.", view=None) self.stop() async def check_user_consent(user_id): diff --git a/dolly/database.py b/nessa/database.py similarity index 99% rename from dolly/database.py rename to nessa/database.py index 5e94bbc..653c151 100644 --- a/dolly/database.py +++ b/nessa/database.py @@ -1,7 +1,7 @@ import aiosqlite from datetime import datetime -DATABASE = "dolly.db" +DATABASE = "nessa.db" async def init_db(): async with aiosqlite.connect(DATABASE) as db: diff --git a/dolly/logger.py b/nessa/logger.py similarity index 100% rename from dolly/logger.py rename to nessa/logger.py diff --git a/dolly/dolly.py b/nessa/nessa.py similarity index 71% rename from dolly/dolly.py rename to nessa/nessa.py index a5d78c4..71614f5 100644 --- a/dolly/dolly.py +++ b/nessa/nessa.py @@ -1,6 +1,6 @@ import discord from discord import app_commands -from .commands import DollyTracker +from .commands import NessaTracker from .consent import ConsentView, check_user_consent, store_user_consent from dotenv import load_dotenv import os @@ -9,23 +9,23 @@ load_dotenv() GUILD_ID = int(os.getenv("DISCORD_GUILD_ID")) OWNER_ID = int(os.getenv("AUTHORIZED_USER_ID")) -class Dolly(discord.Client): +class Nessa(discord.Client): def __init__(self): super().__init__(intents=discord.Intents.default()) self.tree = app_commands.CommandTree(self) async def setup_hook(self): - self.tree.add_command(DollyTracker()) + self.tree.add_command(NessaTracker()) self.tree.copy_global_to(guild=discord.Object(id=GUILD_ID)) await self.tree.sync(guild=discord.Object(id=GUILD_ID)) - @self.tree.command(name="shutdown", description="Shut down the bot", guild=discord.Object(id=GUILD_ID)) + @self.tree.command(name="shutdown", description="Shut down Nessa", guild=discord.Object(id=GUILD_ID)) async def shutdown(interaction: discord.Interaction): if interaction.user.id == OWNER_ID: - await interaction.response.send_message("Shutting down...") + await interaction.response.send_message("Night, Night. Shutting down...") await self.close() else: - await interaction.response.send_message("You do not have permission to use this command.", ephemeral=True) + await interaction.response.send_message("Hey! I don't know you! You don't have permission to use this command. Only the owner can do that.", ephemeral=True) async def on_ready(self): print(f"Logged on as {self.user}!") @@ -45,7 +45,7 @@ class Dolly(discord.Client): return view = ConsentView() await interaction.response.send_message( - "By using this bot, you consent to the storage of your data necessary for functionality. Please confirm your consent. See /dolly consent privacy-policy for more details.", + "By using the services I, Nessa, provide, you consent to the storage of your data necessary for functionality. Please confirm your consent. See /nessa consent privacy-policy for more details.", view=view, ephemeral=True ) @@ -53,11 +53,11 @@ class Dolly(discord.Client): if view.value: await store_user_consent(interaction.user.id) else: - await interaction.followup.send("You must consent to data storage to use this bot.", ephemeral=True) + await interaction.followup.send("Whoops! You have to give me your okay to do store your data before you can use my services!", ephemeral=True) return # Stop processing if they do not consent # For opt-in command, check if they're trying to opt-in after opting out. if interaction.command.name == 'opt-in': if consented: - await interaction.response.send_message("You have already consented.", ephemeral=True) + await interaction.response.send_message("Hey! Thanks, but you've already opted in.", ephemeral=True) return \ No newline at end of file