Fix: Added some code to fix the reconnect/disconnect error. Docs: Added setup.cfg to set everything to be 120.
375 lines
16 KiB
Python
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())
|