Files
Nessa/nessa/commands.py
Dan efd6cdc0d5 Fix: Changed everything to conform to Flake8.
Fix: Added some code to fix the reconnect/disconnect error.
Docs: Added setup.cfg to set everything to be 120.
2024-05-20 09:48:34 -04:00

375 lines
16 KiB
Python

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