Doing a lot of overhaul to add reminders

This commit is contained in:
Dan
2024-05-05 19:09:19 -04:00
parent ff15da0061
commit 9ce8c910c8
6 changed files with 94 additions and 48 deletions

View File

@ -54,36 +54,54 @@ class TaskCommands(app_commands.Group):
super().__init__(name="task", description="Manage tasks.") super().__init__(name="task", description="Manage tasks.")
@app_commands.command(name="add", description="Add a new task to a project.") @app_commands.command(name="add", description="Add a new task to a project.")
async def add_task(self, interaction: discord.Interaction, project_name: str, description: str, assignee: str, deadline: str, status: str, priority: str): 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: try:
datetime.strptime(deadline, "%m/%d/%Y") # Validate date format 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) project_id = await get_project_id(project_name)
if project_id: if project_id:
await add_task_to_project(project_id, description, assignee, deadline, status, priority) await add_task_to_project(project_id, description, assignee, deadline, status, priority, reminder_dt)
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.") response_msg = f"Okay, I've added Task '{description}' to your project,'{project_name}'."
logger.info(f"Task added to project {project_name}: {description}") 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: else:
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) 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}") logger.warning(f"Attempted to add task to non-existent project: {project_name}")
except ValueError: except ValueError as ve:
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) if 'does not match format' in str(ve):
logger.error(f"Invalid date format provided by user: {deadline}") 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: except Exception as e:
await interaction.response.send_message("Huh, that's never happened before... Try again!", ephemeral=True) await interaction.response.send_message("An unexpected error occurred. Please try again!", ephemeral=True)
logger.error(f"Unexpected error in add_task: {e}") logger.error(f"Unexpected error in add_task: {e}")
@app_commands.command(name="update", description="Update an existing task.") @app_commands.command(name="update", description="Update an existing task.")
async def update_task_command(self, interaction: discord.Interaction, task_id: int, description: str, assignee: str, deadline: str, status: str, priority: str): 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: try:
datetime.strptime(deadline, "%m/%d/%Y") # Validate date format datetime.strptime(deadline, "%m/%d/%Y") # Validate deadline date format
await update_task(task_id, description, assignee, deadline, status, priority) reminder_dt = None
await interaction.response.send_message(f"Okay hun, so I updated the task with ID {task_id} with the changes you wanted made!") if reminder_time:
logger.info(f"Task ID {task_id} updated: {description}") reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") # Validate and convert reminder time if provided
except ValueError: await update_task(task_id, description, assignee, deadline, status, priority, reminder_dt)
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) response_msg = f"Okay hun, so I updated the task with ID {task_id} with the changes you wanted made!"
logger.error(f"Invalid date format provided by user for task update: {deadline}") 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: except Exception as e:
await interaction.response.send_message("Huh, that's never happened before... Try again!", ephemeral=True) await interaction.response.send_message("An unexpected error occurred. Please try again!", ephemeral=True)
logger.error(f"Unexpected error in update_task_command: {e}") logger.error(f"Unexpected error in update_task_command: {e}")
@app_commands.command(name="list", description="List tasks for a project.") @app_commands.command(name="list", description="List tasks for a project.")
@ -111,7 +129,6 @@ class TaskCommands(app_commands.Group):
view = ConfirmTaskDeletionView(task_id) 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) 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): class ConsentCommands(app_commands.Group):
def __init__(self): def __init__(self):
super().__init__(name="consent", description="Manage data consent settings.") super().__init__(name="consent", description="Manage data consent settings.")

View File

@ -14,7 +14,7 @@ class ConfirmView(View):
# Attempt to remove the project # Attempt to remove the project
try: try:
await remove_project(self.project_id) 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) await interaction.response.edit_message(content=f"Alright hun, I have completely forgotten about the Project by the ID of {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.") logger.info(f"Project ID {self.project_id} and all associated tasks removed successfully.")
except Exception as e: except Exception as e:
error_message = f"Failed to remove the project. Error: {e}" error_message = f"Failed to remove the project. Error: {e}"
@ -24,7 +24,7 @@ class ConfirmView(View):
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary) @discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary)
async def cancel(self, interaction: discord.Interaction, button: Button): async def cancel(self, interaction: discord.Interaction, button: Button):
await interaction.response.edit_message(content="Project removal cancelled.", view=None) await interaction.response.edit_message(content="I see you changed your mind! I won't forgot the project after all..", view=None)
self.stop() self.stop()
class ConfirmTaskDeletionView(View): class ConfirmTaskDeletionView(View):
@ -38,7 +38,7 @@ class ConfirmTaskDeletionView(View):
# Attempt to remove the task # Attempt to remove the task
try: try:
await remove_task(self.task_id) await remove_task(self.task_id)
await interaction.response.edit_message(content=f"Task ID {self.task_id} has been successfully removed.", view=None) await interaction.response.edit_message(content=f"Okay, I have completely forgotten about the Task by the ID {self.task_id}, and it has been successfully removed.", view=None)
logger.info(f"Task ID {self.task_id} removed successfully.") logger.info(f"Task ID {self.task_id} removed successfully.")
except Exception as e: except Exception as e:
error_message = f"Failed to remove the task. Error: {e}" error_message = f"Failed to remove the task. Error: {e}"
@ -47,5 +47,5 @@ class ConfirmTaskDeletionView(View):
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary) @discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary)
async def cancel(self, interaction: discord.Interaction, button: Button): async def cancel(self, interaction: discord.Interaction, button: Button):
await interaction.response.edit_message(content="Task removal cancelled.", view=None) await interaction.response.edit_message(content="Wait! I see you changed your mind! I won't forgot the task after all..", view=None)
self.stop() self.stop()

View File

@ -13,13 +13,13 @@ class ConsentView(View):
async def confirm(self, interaction: discord.Interaction, button: Button): async def confirm(self, interaction: discord.Interaction, button: Button):
self.value = True self.value = True
await store_user_consent(interaction.user.id) await store_user_consent(interaction.user.id)
await interaction.response.edit_message(content="Thank you for consenting to data storage!", view=None) await interaction.response.edit_message(content="Hey there! Thanks for opt-ing in to allowing me to store the data for your projects.", view=None)
self.stop() self.stop()
@discord.ui.button(label="Decline Data Storage", style=discord.ButtonStyle.grey) @discord.ui.button(label="Decline Data Storage", style=discord.ButtonStyle.grey)
async def cancel(self, interaction: discord.Interaction, button: Button): async def cancel(self, interaction: discord.Interaction, button: Button):
self.value = False self.value = False
await interaction.response.edit_message(content="You have declined data storage. Since you have, You can't use my services.", view=None) await interaction.response.edit_message(content="Awe. It's ok. I won't store your data but this also means you won't be able to use my services. Bye, bye!", view=None)
self.stop() self.stop()
async def check_user_consent(user_id): async def check_user_consent(user_id):

View File

@ -5,14 +5,15 @@ DATABASE = "nessa.db"
async def init_db(): async def init_db():
async with aiosqlite.connect(DATABASE) as db: async with aiosqlite.connect(DATABASE) as db:
await db.execute( await db.execute("PRAGMA foreign_keys = ON") # Enable foreign key constraint support
'''CREATE TABLE IF NOT EXISTS projects( await db.execute('''
CREATE TABLE IF NOT EXISTS projects(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE, name TEXT UNIQUE,
description TEXT)''' description TEXT)
) ''')
await db.execute( await db.execute('''
'''CREATE TABLE IF NOT EXISTS tasks( CREATE TABLE IF NOT EXISTS tasks(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER, project_id INTEGER,
description TEXT, description TEXT,
@ -20,17 +21,16 @@ async def init_db():
deadline TEXT, deadline TEXT,
status TEXT, status TEXT,
priority TEXT, priority TEXT,
notification_channel_id TEXT,
reminder_time DATETIME, reminder_time DATETIME,
reminder_sent BOOLEAN DEFAULT FALSE, reminder_sent BOOLEAN DEFAULT FALSE,
FOREIGN KEY(project_id) REFERENCES projects(id)) FOREIGN KEY(project_id) REFERENCES projects(id))
''' ''')
) await db.execute('''
await db.execute( CREATE TABLE IF NOT EXISTS user_consents(
'''CREATE TABLE IF NOT EXISTS user_consents(
user_id INTEGER PRIMARY KEY, user_id INTEGER PRIMARY KEY,
consent_given BOOLEAN NOT NULL consent_given BOOLEAN NOT NULL)
)''' ''')
)
await db.commit() await db.commit()
async def add_project(name, description): async def add_project(name, description):
@ -51,19 +51,19 @@ async def get_project_name(project_id):
result = await cursor.fetchone() result = await cursor.fetchone()
return result[0] if result else None return result[0] if result else None
async def add_task_to_project(project_id, description, assignee, deadline, status, priority, reminder_time=None): async def add_task_to_project(project_id, description, assignee, deadline, status, priority, notification_channel_id=None, reminder_time=None):
async with aiosqlite.connect(DATABASE) as db: async with aiosqlite.connect(DATABASE) as db:
await db.execute( await db.execute(
"INSERT INTO tasks (project_id, description, assignee, deadline, status, priority, reminder_time) VALUES (?, ?, ?, ?, ?, ?, ?)", "INSERT INTO tasks (project_id, description, assignee, deadline, status, priority, notification_channel_id, reminder_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(project_id, description, assignee, deadline, status, priority, reminder_time) (project_id, description, assignee, deadline, status, priority, notification_channel_id, reminder_time)
) )
await db.commit() await db.commit()
async def update_task(task_id, description, assignee, deadline, status, priority, reminder_time=None): async def update_task(task_id, description, assignee, deadline, status, priority, notification_channel_id=None, reminder_time=None):
async with aiosqlite.connect(DATABASE) as db: async with aiosqlite.connect(DATABASE) as db:
await db.execute( await db.execute(
"UPDATE tasks SET description=?, assignee=?, deadline=?, status=?, priority=?, reminder_time=? WHERE id=?", "UPDATE tasks SET description=?, assignee=?, deadline=?, status=?, priority=?, notification_channel_id=?, reminder_time=? WHERE id=?",
(description, assignee, deadline, status, priority, reminder_time, task_id) (description, assignee, deadline, status, priority, notification_channel_id, reminder_time, task_id)
) )
await db.commit() await db.commit()

View File

@ -2,6 +2,7 @@ import discord
from discord import app_commands from discord import app_commands
from .commands import NessaTracker from .commands import NessaTracker
from .consent import ConsentView, check_user_consent, store_user_consent from .consent import ConsentView, check_user_consent, store_user_consent
from .reminder_tracker import reminder_worker
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
@ -31,6 +32,8 @@ class Nessa(discord.Client):
print(f"Logged on as {self.user}!") print(f"Logged on as {self.user}!")
self.tree.copy_global_to(guild=discord.Object(id=GUILD_ID)) self.tree.copy_global_to(guild=discord.Object(id=GUILD_ID))
await self.tree.sync(guild=discord.Object(id=GUILD_ID)) await self.tree.sync(guild=discord.Object(id=GUILD_ID))
print("Starting reminder worker...")
await self.loop.create_task(reminder_worker())
async def on_interaction(self, interaction: discord.Interaction): async def on_interaction(self, interaction: discord.Interaction):
if interaction.type == discord.InteractionType.application_command: if interaction.type == discord.InteractionType.application_command:

26
nessa/reminder_tracker.py Normal file
View File

@ -0,0 +1,26 @@
import asyncio
from datetime import datetime
import aiosqlite
from .database import DATABASE
async def reminder_worker(client):
while True:
await asyncio.sleep(60) # check every minute
now = datetime.now()
async with aiosqlite.connect(DATABASE) as db:
cursor = await db.execute(
"SELECT id, description, reminder_time, notification_channel_id FROM tasks WHERE reminder_time <= ? AND reminder_sent = FALSE",
(now,)
)
tasks = await cursor.fetchall()
for task_id, description, reminder_time, channel_id in tasks:
if channel_id:
channel = client.get_channel(int(channel_id))
if channel:
await channel.send(f"Reminder for task: {description}")
await db.execute("UPDATE tasks SET reminder_sent = TRUE WHERE id = ?", (task_id,))
else:
print(f"Failed to find channel with ID {channel_id}")
else:
print(f"No channel ID provided for task ID {task_id}")
await db.commit()