182 lines
13 KiB
Python
182 lines
13 KiB
Python
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
|
|
from .logger import logger
|
|
from dotenv import load_dotenv
|
|
import os
|
|
|
|
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 look it up using the `/project list` command. Your project 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):
|
|
# Send a message with the confirmation view
|
|
try:
|
|
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(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):
|
|
super().__init__(name="task", description="Manage tasks.")
|
|
|
|
@app_commands.command(name="add", description="Add a new task to a 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 deadline date format
|
|
reminder_dt = None
|
|
if reminder_time:
|
|
reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") # Validate and convert reminder time if provided
|
|
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,'{project_name}'."
|
|
if reminder_time:
|
|
response_msg += f" And you wanted a reminder about this taks at {reminder_time}. Got it!"
|
|
await interaction.response.send_message(response_msg)
|
|
logger.info(f"Task added to project {project_name}: {description} with reminder set for {reminder_time if reminder_time else 'No reminder set'}.")
|
|
else:
|
|
await interaction.response.send_message(f"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: {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 the right 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 deadline date format
|
|
reminder_dt = None
|
|
if reminder_time:
|
|
reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") # Validate and convert reminder time if provided
|
|
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 changes you wanted made!"
|
|
if reminder_time:
|
|
response_msg += f" And you wanted a reminder about this taks at {reminder_time}. Got it!"
|
|
await interaction.response.send_message(response_msg)
|
|
logger.info(f"Task ID {task_id} updated: {description} with reminder set 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 the right 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(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"So, Here are all the tasks I found for that project '{project_name}':\n{message}")
|
|
else:
|
|
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"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):
|
|
# 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 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 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 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):
|
|
# Replace 'YOUR_USER_ID' with the actual numeric ID of the user 'advtech'
|
|
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 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()) |