import os from datetime import datetime import discord from discord import app_commands from dotenv import load_dotenv from .confirm import ConfirmTaskDeletionView, ConfirmView from .consent import revoke_user_consent, store_user_consent from .database import ( add_project, add_task_to_project, get_project_id, get_project_name, list_projects, list_tasks_for_project, update_task, ) from .logger import logger load_dotenv() contact_user_id = int(os.getenv("AUTHORIZED_USER_ID")) class ProjectCommands(app_commands.Group): def __init__(self): super().__init__(name="project", description="Manage projects.") @app_commands.command(name="create", description="Create a new project.") 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"Noted. I've created a new project called '{name}'. You can " f"look it up using the `/project list` command. Your project " f"ID is {project_id}." ) except Exception as e: 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.") async def list_projects(self, interaction: discord.Interaction): try: 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"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( "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( "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 ): try: view = ConfirmView(project_id) await interaction.response.send_message( f"Hey there! Are you sure you wanna me to forget the project " f"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( f"Whoops! I failed to remove the project. Error: {e}", ephemeral=True ) logger.error( f"Preparation error in remove_project_command for Project ID " f"{project_id}: {e}" ) class TaskCommands(app_commands.Group): def __init__(self): super().__init__(name="task", description="Manage tasks.") @app_commands.command(name="add", description="Add new task to project.") @app_commands.choices( priority=[ app_commands.Choice(name="Low", value="Low"), app_commands.Choice(name="Medium", value="Medium"), app_commands.Choice(name="High", value="High"), app_commands.Choice(name="Critical", value="Critical"), ], status=[ app_commands.Choice(name="Not Started", value="Not Started"), app_commands.Choice(name="In Progress", value="In Progress"), app_commands.Choice(name="Blocked", value="Blocked"), app_commands.Choice(name="In Testing", value="In Testing"), app_commands.Choice(name="Needs Review", value="Needs Review"), app_commands.Choice(name="Completed", value="Completed"), ], ) async def add_task( self, interaction: discord.Interaction, project_name: str, description: str, assignee: str, deadline: str, status: str, priority: str, reminder_time: str = None, ): try: datetime.strptime(deadline, "%m/%d/%Y") # Validate date format reminder_dt = None if reminder_time: reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") project_id = await get_project_id(project_name) if project_id: await add_task_to_project( project_id, description, assignee, deadline, status, priority, reminder_dt, ) response_msg = ( f"Okay, I've added Task '{description}' to your project," f"'{project_name}'." ) if reminder_time: response_msg += ( f" And you wanted a reminder about this task at " f"{reminder_time}. Got it!" ) await interaction.response.send_message(response_msg) logger.info( f"Task added to project {project_name}: {description} " f"with reminder set for {reminder_time if reminder_time else 'No reminder set'}." ) else: await interaction.response.send_message( "Silly, you can't add a task to a project that doesn't " "exist. I don't have a Project '{project_name}' in my " "memory. Use `/project create` to add it first.", ephemeral=True, ) logger.warning( f"Attempted to add task to non-existent project: " f"{project_name}" ) except ValueError as ve: if "does not match format" in str(ve): await interaction.response.send_message( "Wait a sec! That's not right! Please use this format, " "i.e. MM/DD/YYYY for dates, and HH:MM for times. I can't " "do anything otherwise!", ephemeral=True, ) else: await interaction.response.send_message( "Invalid date or time format!", ephemeral=True ) logger.error(f"Invalid date or time format provided by user: {ve}") except Exception as e: await interaction.response.send_message( "An unexpected error occurred. Please try again!", ephemeral=True ) logger.error(f"Unexpected error in add_task: {e}") @app_commands.command(name="update", description="Update an existing task.") @app_commands.choices( priority=[ app_commands.Choice(name="Low", value="Low"), app_commands.Choice(name="Medium", value="Medium"), app_commands.Choice(name="High", value="High"), app_commands.Choice(name="Critical", value="Critical"), ], status=[ app_commands.Choice(name="Not Started", value="Not Started"), app_commands.Choice(name="In Progress", value="In Progress"), app_commands.Choice(name="Blocked", value="Blocked"), app_commands.Choice(name="In Testing", value="In Testing"), app_commands.Choice(name="Needs Review", value="Needs Review"), app_commands.Choice(name="Completed", value="Completed"), ], ) async def update_task_command( self, interaction: discord.Interaction, task_id: int, description: str, assignee: str, deadline: str, status: str, priority: str, reminder_time: str = None, ): try: datetime.strptime(deadline, "%m/%d/%Y") # Validate date format reminder_dt = None if reminder_time: reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") await update_task( task_id, description, assignee, deadline, status, priority, reminder_dt ) response_msg = ( f"Okay hun, so I updated the task with ID {task_id} with the " f"changes you wanted made!" ) if reminder_time: response_msg += ( f" And you wanted a reminder about this task at " f"{reminder_time}. Got it!" ) await interaction.response.send_message(response_msg) logger.info( f"Task ID {task_id} updated: {description} with reminder set " f"for {reminder_time if reminder_time else 'No reminder set'}." ) except ValueError as ve: if "does not match format" in str(ve): await interaction.response.send_message( "Wait a sec! That's not right! Please use this format, " "i.e. MM/DD/YYYY for dates, and HH:MM for times. I can't " "do anything otherwise!", ephemeral=True, ) else: await interaction.response.send_message( "Invalid date or time format!", ephemeral=True ) logger.error(f"Invalid date or time format provided by user: {ve}") except Exception as e: await interaction.response.send_message( "An unexpected error occurred. Please 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.") async def list_tasks(self, interaction: discord.Interaction, project_id: int): try: project_name = await get_project_name(project_id) if not project_name: await interaction.response.send_message( "I may be the goddess of memory, but there's no project " f"with that ID {project_id} in my memory. 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: " f"{assignee}, Deadline: {deadline}, Status: {status}, " f"Priority: {priority}" for id, description, assignee, deadline, status, priority in tasks ] ) await interaction.response.send_message( f"So, Here are all the tasks I found for that project " f"'{project_name}':\n{message}" ) else: await interaction.response.send_message( f"Whoops! I couldn't find any tasks for that project " f"'{project_name}'. Is it brand new?." ) except Exception as e: await interaction.response.send_message( f"My memory must be slipping, I think I couldn't find any " f"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): 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 Nessa's Services." ) async def opt_in(self, interaction: discord.Interaction): await store_user_consent(interaction.user.id) await interaction.response.send_message( "Hey there! Thanks for opting 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 Nessa's Services.", ) async def opt_out(self, interaction: discord.Interaction): await revoke_user_consent(interaction.user.id) 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 Nessa's data handling policy." ) async def privacy_policy(self, interaction: discord.Interaction): policy_details = ( "Hi there! So you're looking to hire Nessa, the goddess 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 Discord ID, " "Projects, Tasks, and other data related to your projects. Why I " "need it? Well, I need your ID so as to 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 " f"need anything else, talk to my boss: <@{contact_user_id}> on " "Discord." ) await interaction.response.send_message(policy_details, ephemeral=True) class NessaTracker( app_commands.Group, name="nessa", description="Nessa the Project Tracker." ): def __init__(self): super().__init__() self.add_command(ProjectCommands()) self.add_command(TaskCommands()) self.add_command(ConsentCommands())