Compare commits
10 Commits
4464e0107d
...
efd6cdc0d5
Author | SHA1 | Date | |
---|---|---|---|
efd6cdc0d5 | |||
d42975d981 | |||
02c03fe992 | |||
2aa3b20b6d | |||
5d3068ebc9 | |||
de18257c3e | |||
bf0e320402 | |||
b904b1087d | |||
bb5c034309 | |||
bff5b4387b |
12
.pre-commit-config.yaml
Normal file
12
.pre-commit-config.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: 7.0.0 # Use the latest revision
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 24.4.2
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
language_version: python3.10.6
|
||||||
|
|
||||||
|
|
BIN
Nessa.ico
BIN
Nessa.ico
Binary file not shown.
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 113 KiB |
BIN
Nessa.png
BIN
Nessa.png
Binary file not shown.
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 536 KiB |
12
main.py
12
main.py
@ -1,16 +1,20 @@
|
|||||||
from nessa.nessa import Nessa
|
|
||||||
from nessa.database import init_db
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from nessa.database import init_db
|
||||||
|
from nessa.nessa import Nessa
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
TOKEN = os.getenv("DISCORD_BOT_TOKEN")
|
TOKEN = os.getenv("DISCORD_BOT_TOKEN")
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
await init_db()
|
await init_db()
|
||||||
client = Nessa()
|
client = Nessa()
|
||||||
await client.start(TOKEN)
|
await client.start(TOKEN)
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
|
asyncio.run(main())
|
||||||
|
@ -1,159 +1,374 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
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
|
from dotenv import load_dotenv
|
||||||
import os
|
|
||||||
|
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()
|
load_dotenv()
|
||||||
|
|
||||||
contact_user_id = int(os.getenv("AUTHORIZED_USER_ID"))
|
contact_user_id = int(os.getenv("AUTHORIZED_USER_ID"))
|
||||||
|
|
||||||
|
|
||||||
class ProjectCommands(app_commands.Group):
|
class ProjectCommands(app_commands.Group):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(name="project", description="Manage projects.")
|
super().__init__(name="project", description="Manage projects.")
|
||||||
|
|
||||||
@app_commands.command(name="create", description="Create a new project.")
|
@app_commands.command(name="create", description="Create a new project.")
|
||||||
async def create_project(self, interaction: discord.Interaction, name: str, description: str):
|
async def create_project(
|
||||||
|
self, interaction: discord.Interaction, name: str, description: str
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
project_id = await add_project(name, description)
|
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}.")
|
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:
|
except Exception as e:
|
||||||
await interaction.response.send_message("Whoops! Looks like we hit a snag! Try creating the project again! :)", ephemeral=True)
|
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}")
|
logger.error(f"Error in create_project: {e}")
|
||||||
|
|
||||||
@app_commands.command(name="list", description="List all projects.")
|
@app_commands.command(name="list", description="List all projects.")
|
||||||
async def list_projects(self, interaction: discord.Interaction):
|
async def list_projects(self, interaction: discord.Interaction):
|
||||||
try:
|
try:
|
||||||
projects = await list_projects()
|
projects = await list_projects()
|
||||||
if projects:
|
if projects:
|
||||||
message = "\n".join([f"ID: {id}, Name: {name}, Description: {description}" for id, name, description in projects])
|
message = "\n".join(
|
||||||
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.")
|
[
|
||||||
|
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:
|
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.")
|
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:
|
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)
|
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}")
|
logger.error(f"Error in list_projects: {e}")
|
||||||
|
|
||||||
@app_commands.command(name="remove", description="Remove a specific project and all its tasks.")
|
@app_commands.command(
|
||||||
async def remove_project_command(self, interaction: discord.Interaction, project_id: int):
|
name="remove", description="Remove a specific project" "and all its tasks."
|
||||||
# Send a message with the confirmation view
|
)
|
||||||
|
async def remove_project_command(
|
||||||
|
self, interaction: discord.Interaction, project_id: int
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
view = ConfirmView(project_id)
|
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)
|
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:
|
except Exception as e:
|
||||||
await interaction.response.send_message(f"Whoops! I failed to remove the project. Error: {e}", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
logger.error(f"Preparation error in remove_project_command for Project ID {project_id}: {e}")
|
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):
|
class TaskCommands(app_commands.Group):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
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 new task to project.")
|
||||||
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):
|
@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:
|
try:
|
||||||
datetime.strptime(deadline, "%m/%d/%Y") # Validate deadline date format
|
datetime.strptime(deadline, "%m/%d/%Y") # Validate date format
|
||||||
reminder_dt = None
|
reminder_dt = None
|
||||||
if reminder_time:
|
if reminder_time:
|
||||||
reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") # Validate and convert reminder time if provided
|
reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M")
|
||||||
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, reminder_dt)
|
await add_task_to_project(
|
||||||
response_msg = f"Okay, I've added Task '{description}' to your project,'{project_name}'."
|
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:
|
if reminder_time:
|
||||||
response_msg += f" And you wanted a reminder about this taks at {reminder_time}. Got it!"
|
response_msg += (
|
||||||
|
f" And you wanted a reminder about this task at "
|
||||||
|
f"{reminder_time}. Got it!"
|
||||||
|
)
|
||||||
await interaction.response.send_message(response_msg)
|
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'}.")
|
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:
|
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)
|
await interaction.response.send_message(
|
||||||
logger.warning(f"Attempted to add task to non-existent project: {project_name}")
|
"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:
|
except ValueError as ve:
|
||||||
if 'does not match format' in str(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)
|
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:
|
else:
|
||||||
await interaction.response.send_message("Invalid date or time format!", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
|
"Invalid date or time format!", ephemeral=True
|
||||||
|
)
|
||||||
logger.error(f"Invalid date or time format provided by user: {ve}")
|
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("An unexpected error occurred. Please try again!", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
logger.error(f"Unexpected error in add_task: {e}")
|
"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.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, reminder_time: str = None):
|
@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:
|
try:
|
||||||
datetime.strptime(deadline, "%m/%d/%Y") # Validate deadline date format
|
datetime.strptime(deadline, "%m/%d/%Y") # Validate date format
|
||||||
reminder_dt = None
|
reminder_dt = None
|
||||||
if reminder_time:
|
if reminder_time:
|
||||||
reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M") # Validate and convert reminder time if provided
|
reminder_dt = datetime.strptime(reminder_time, "%m/%d/%Y %H:%M")
|
||||||
await update_task(task_id, description, assignee, deadline, status, priority, reminder_dt)
|
await update_task(
|
||||||
response_msg = f"Okay hun, so I updated the task with ID {task_id} with the changes you wanted made!"
|
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:
|
if reminder_time:
|
||||||
response_msg += f" And you wanted a reminder about this taks at {reminder_time}. Got it!"
|
response_msg += (
|
||||||
|
f" And you wanted a reminder about this task at "
|
||||||
|
f"{reminder_time}. Got it!"
|
||||||
|
)
|
||||||
await interaction.response.send_message(response_msg)
|
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'}.")
|
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:
|
except ValueError as ve:
|
||||||
if 'does not match format' in str(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)
|
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:
|
else:
|
||||||
await interaction.response.send_message("Invalid date or time format!", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
|
"Invalid date or time format!", ephemeral=True
|
||||||
|
)
|
||||||
logger.error(f"Invalid date or time format provided by user: {ve}")
|
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("An unexpected error occurred. Please try again!", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
logger.error(f"Unexpected error in update_task_command: {e}")
|
"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.")
|
@app_commands.command(name="list", description="List tasks for a project.")
|
||||||
async def list_tasks(self, interaction: discord.Interaction, project_id: int):
|
async def list_tasks(self, interaction: discord.Interaction, project_id: int):
|
||||||
try:
|
try:
|
||||||
project_name = await get_project_name(project_id)
|
project_name = await get_project_name(project_id)
|
||||||
if not project_name:
|
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)
|
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
|
return
|
||||||
|
|
||||||
tasks = await list_tasks_for_project(project_id)
|
tasks = await list_tasks_for_project(project_id)
|
||||||
if tasks:
|
if tasks:
|
||||||
message = "\n".join([f"ID: {id}, Description: {description}, Assignee: {assignee}, Deadline: {deadline}, Status: {status}, Priority: {priority}"
|
message = "\n".join(
|
||||||
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}")
|
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:
|
else:
|
||||||
await interaction.response.send_message(f"Whoops! I couldn't find any tasks for that project '{project_name}' Is it brand new?.")
|
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:
|
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)
|
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}")
|
logger.error(f"Error in list_tasks: {e}")
|
||||||
|
|
||||||
@app_commands.command(name="remove", description="Remove a specific task.")
|
@app_commands.command(name="remove", description="Remove a specific task.")
|
||||||
async def remove_task_command(self, interaction: discord.Interaction, task_id: int):
|
async def remove_task_command(self, interaction: discord.Interaction, task_id: int):
|
||||||
# Send a message with the confirmation view
|
|
||||||
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.")
|
||||||
|
|
||||||
@app_commands.command(name="opt-in", description="Opt-in to data storage and use Nessa's Services.")
|
@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):
|
async def opt_in(self, interaction: discord.Interaction):
|
||||||
await store_user_consent(interaction.user.id)
|
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)
|
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.")
|
@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):
|
async def opt_out(self, interaction: discord.Interaction):
|
||||||
await revoke_user_consent(interaction.user.id)
|
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)
|
await interaction.response.send_message(
|
||||||
|
"Awe. It's ok. I won't store your data but this also means you "
|
||||||
@app_commands.command(name="privacy-policy", description="View Nessa's data handling policy.")
|
"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):
|
async def privacy_policy(self, interaction: discord.Interaction):
|
||||||
# Replace 'YOUR_USER_ID' with the actual numeric ID of the user 'advtech'
|
policy_details = (
|
||||||
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."""
|
"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)
|
await interaction.response.send_message(policy_details, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
class NessaTracker(
|
||||||
class NessaTracker(app_commands.Group, name="nessa", description="Nessa the Project Tracker."):
|
app_commands.Group, name="nessa", description="Nessa the Project Tracker."
|
||||||
|
):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_command(ProjectCommands())
|
self.add_command(ProjectCommands())
|
||||||
self.add_command(TaskCommands())
|
self.add_command(TaskCommands())
|
||||||
self.add_command(ConsentCommands())
|
self.add_command(ConsentCommands())
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ui import View, Button
|
from discord.ui import Button, View
|
||||||
from .logger import logger
|
|
||||||
from .database import remove_project, remove_task
|
from .database import remove_project, remove_task
|
||||||
|
from .logger import logger
|
||||||
|
|
||||||
|
|
||||||
class ConfirmView(View):
|
class ConfirmView(View):
|
||||||
def __init__(self, project_id, timeout=180): # 180 seconds until the view expires
|
def __init__(self, project_id, timeout=180):
|
||||||
|
# 180 seconds until the view expires
|
||||||
super().__init__(timeout=timeout)
|
super().__init__(timeout=timeout)
|
||||||
self.project_id = project_id
|
self.project_id = project_id
|
||||||
self.value = None
|
self.value = None
|
||||||
@ -14,21 +17,36 @@ 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"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)
|
await interaction.response.edit_message(
|
||||||
logger.info(f"Project ID {self.project_id} and all associated tasks removed successfully.")
|
content=(
|
||||||
|
f"Alright hun, I have completely forgotten about the "
|
||||||
|
f"Project {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."
|
||||||
|
)
|
||||||
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}"
|
||||||
await interaction.response.edit_message(content=error_message, view=None)
|
await interaction.response.edit_message(content=error_message, view=None)
|
||||||
logger.error(f"Error in remove_project_command: {e}")
|
logger.error(f"Error in remove_project_command: {e}")
|
||||||
self.stop() # Stopping the view after an error is usually a good practice to clean up the UI.
|
self.stop() # Stopping the view after an error to clean up the UI
|
||||||
|
|
||||||
@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="I see you changed your mind! I won't forgot the project after all..", view=None)
|
await interaction.response.edit_message(
|
||||||
|
content="I see you changed your mind! I won't forget the project.",
|
||||||
|
view=None,
|
||||||
|
)
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
class ConfirmTaskDeletionView(View):
|
class ConfirmTaskDeletionView(View):
|
||||||
def __init__(self, task_id, timeout=180): # 180 seconds until the view expires
|
def __init__(self, task_id, timeout=180):
|
||||||
|
# 180 seconds until the view expires
|
||||||
super().__init__(timeout=timeout)
|
super().__init__(timeout=timeout)
|
||||||
self.task_id = task_id
|
self.task_id = task_id
|
||||||
self.value = None
|
self.value = None
|
||||||
@ -38,7 +56,13 @@ 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"Okay, I have completely forgotten about the Task by the ID {self.task_id}, and it has been successfully removed.", view=None)
|
await interaction.response.edit_message(
|
||||||
|
content=(
|
||||||
|
f"Okay, I have completely forgotten about the Task by the "
|
||||||
|
f"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 +71,7 @@ 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="Wait! I see you changed your mind! I won't forgot the task after all..", view=None)
|
await interaction.response.edit_message(
|
||||||
self.stop()
|
content="Wait! You changed your mind! I won't forget the task.", view=None
|
||||||
|
)
|
||||||
|
self.stop()
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import discord
|
|
||||||
from discord.ui import View, Button
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
import discord
|
||||||
|
from discord.ui import Button, View
|
||||||
|
|
||||||
DATABASE = "nessa.db"
|
DATABASE = "nessa.db"
|
||||||
|
|
||||||
|
|
||||||
class ConsentView(View):
|
class ConsentView(View):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -13,30 +14,55 @@ 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="Hey there! Thanks for opt-ing in to allowing me to store the data for your projects.", 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="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)
|
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!",
|
||||||
|
view=None,
|
||||||
|
)
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
async def check_user_consent(user_id):
|
async def check_user_consent(user_id):
|
||||||
"""Check if the user has given consent to data storage. Assume no consent if no record exists."""
|
"""Check if the user has given consent to data storage.
|
||||||
|
Assume no consent if no record exists."""
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute("SELECT consent_given FROM user_consents WHERE user_id = ?", (user_id,))
|
cursor = await db.execute(
|
||||||
|
"SELECT consent_given FROM user_consents " "WHERE user_id = ?", (user_id,)
|
||||||
|
)
|
||||||
result = await cursor.fetchone()
|
result = await cursor.fetchone()
|
||||||
return result[0] if result else False # Return False if no record exists, prompting consent dialogue
|
return result[0] if result else False
|
||||||
|
|
||||||
|
|
||||||
async def store_user_consent(user_id):
|
async def store_user_consent(user_id):
|
||||||
"""Store the user's consent to data storage."""
|
"""Store the user's consent to data storage."""
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
await db.execute("INSERT OR REPLACE INTO user_consents (user_id, consent_given) VALUES (?, TRUE)", (user_id,))
|
await db.execute(
|
||||||
|
"INSERT OR REPLACE INTO user_consents (user_id, "
|
||||||
|
"consent_given) VALUES (?, TRUE)",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def revoke_user_consent(user_id):
|
async def revoke_user_consent(user_id):
|
||||||
"""Revoke the user's consent to data storage."""
|
"""Revoke the user's consent to data storage."""
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
await db.execute("UPDATE user_consents SET consent_given = FALSE WHERE user_id = ?", (user_id,))
|
await db.execute(
|
||||||
await db.commit()
|
"UPDATE user_consents SET consent_given = FALSE" "WHERE user_id = ?",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import aiosqlite
|
import aiosqlite
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
DATABASE = "nessa.db"
|
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("PRAGMA foreign_keys = ON") # Enable foreign key constraint support
|
await db.execute("PRAGMA foreign_keys = ON")
|
||||||
await db.execute('''
|
await db.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS projects(
|
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,
|
||||||
@ -25,67 +28,130 @@ async def init_db():
|
|||||||
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):
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute("INSERT INTO projects(name, description) VALUES(?, ?)", (name, description))
|
cursor = await db.execute(
|
||||||
|
"INSERT INTO projects(name, description)" "VALUES(?, ?)",
|
||||||
|
(name, description),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return cursor.lastrowid # Returns the ID of the newly created project
|
return cursor.lastrowid # Returns the ID of the newly created project
|
||||||
|
|
||||||
|
|
||||||
async def get_project_id(name):
|
async def get_project_id(name):
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute("SELECT id FROM projects WHERE name = ?", (name,))
|
cursor = await db.execute("SELECT id FROM projects WHERE name = ?", (name,))
|
||||||
result = await cursor.fetchone()
|
result = await cursor.fetchone()
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
|
|
||||||
|
|
||||||
async def get_project_name(project_id):
|
async def get_project_name(project_id):
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute("SELECT name FROM projects WHERE id = ?", (project_id,))
|
cursor = await db.execute(
|
||||||
|
"SELECT name FROM projects WHERE id = ?", (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, notification_channel_id=None, 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, notification_channel_id, reminder_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO tasks (project_id, description, assignee, deadline, "
|
||||||
(project_id, description, assignee, deadline, status, priority, notification_channel_id, reminder_time)
|
"status, priority, notification_channel_id, reminder_time) VALUES "
|
||||||
|
"(?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
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, notification_channel_id=None, 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=?, notification_channel_id=?, reminder_time=? WHERE id=?",
|
"UPDATE tasks SET description=?, assignee=?, deadline=?, status=?,"
|
||||||
(description, assignee, deadline, status, priority, notification_channel_id, reminder_time, task_id)
|
"priority=?, notification_channel_id=?,reminder_time=? WHERE id=?",
|
||||||
|
(
|
||||||
|
description,
|
||||||
|
assignee,
|
||||||
|
deadline,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
notification_channel_id,
|
||||||
|
reminder_time,
|
||||||
|
task_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def list_projects():
|
async def list_projects():
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute("SELECT id, name, description FROM projects")
|
cursor = await db.execute("SELECT id, name, description FROM projects")
|
||||||
projects = await cursor.fetchall()
|
projects = await cursor.fetchall()
|
||||||
return projects
|
return projects
|
||||||
|
|
||||||
|
|
||||||
async def list_tasks_for_project(project_id):
|
async def list_tasks_for_project(project_id):
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute("SELECT id, description, assignee, deadline, status, priority FROM tasks WHERE project_id = ?", (project_id,))
|
cursor = await db.execute(
|
||||||
|
"SELECT id, description, assignee, deadline,"
|
||||||
|
"status, priority FROM tasks "
|
||||||
|
"WHERE project_id = ?",
|
||||||
|
(project_id,),
|
||||||
|
)
|
||||||
tasks = await cursor.fetchall()
|
tasks = await cursor.fetchall()
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
async def remove_task(task_id):
|
async def remove_task(task_id):
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
await db.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
|
await db.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def remove_project(project_id):
|
async def remove_project(project_id):
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
await db.execute("DELETE FROM tasks WHERE project_id = ?", (project_id,)) # Remove all tasks under the project
|
await db.execute(
|
||||||
|
"DELETE FROM tasks WHERE project_id = ?", (project_id,)
|
||||||
|
) # Remove all tasks under the project
|
||||||
await db.execute("DELETE FROM projects WHERE id = ?", (project_id,))
|
await db.execute("DELETE FROM projects WHERE id = ?", (project_id,))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
@ -2,17 +2,19 @@ import logging
|
|||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
# Setup basic configuration
|
# Setup basic configuration
|
||||||
logging.basicConfig(level=logging.ERROR, # Change to DEBUG for detailed output during development
|
logging.basicConfig(
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
level=logging.ERROR,
|
||||||
datefmt='%Y-%m-%d %H:%M:%S')
|
format="%(asctime)s - %(name)s - " "%(levelname)s - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a rotating file handler
|
# Create a rotating file handler
|
||||||
log_file = 'nessa.log'
|
log_file = "nessa.log"
|
||||||
handler = RotatingFileHandler(log_file, maxBytes=1e6, backupCount=5) # Increased file size limit and backup count
|
handler = RotatingFileHandler(log_file, maxBytes=1e6, backupCount=5)
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
# Configure the logger
|
# Configure the logger
|
||||||
logger = logging.getLogger('Nessa')
|
logger = logging.getLogger("Nessa")
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logger.setLevel(logging.ERROR) # Set to DEBUG for comprehensive logging
|
logger.setLevel(logging.ERROR) # Set to DEBUG for comprehensive logging
|
||||||
|
152
nessa/nessa.py
152
nessa/nessa.py
@ -1,18 +1,22 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import aiosqlite
|
||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
import aiosqlite
|
from dotenv import load_dotenv
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
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 .database import DATABASE
|
from .database import DATABASE
|
||||||
from dotenv import load_dotenv
|
from .logger import logger
|
||||||
import os
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
GUILD_ID = int(os.getenv("DISCORD_GUILD_ID"))
|
GUILD_ID = int(os.getenv("DISCORD_GUILD_ID"))
|
||||||
OWNER_ID = int(os.getenv("AUTHORIZED_USER_ID"))
|
OWNER_ID = int(os.getenv("AUTHORIZED_USER_ID"))
|
||||||
|
|
||||||
|
|
||||||
class Nessa(discord.Client):
|
class Nessa(discord.Client):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(intents=discord.Intents.default())
|
super().__init__(intents=discord.Intents.default())
|
||||||
@ -23,69 +27,153 @@ class Nessa(discord.Client):
|
|||||||
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))
|
||||||
|
|
||||||
@self.tree.command(name="shutdown", description="Shut down Nessa", guild=discord.Object(id=GUILD_ID))
|
def is_owner_or_admin(interaction: discord.Interaction):
|
||||||
|
"""Check if the user is the bot owner or an administrator
|
||||||
|
in the guild."""
|
||||||
|
return (
|
||||||
|
interaction.user.id == OWNER_ID
|
||||||
|
or interaction.user.guild_permissions.administrator
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.tree.command(
|
||||||
|
name="shutdown",
|
||||||
|
description="Shut down Nessa",
|
||||||
|
guild=discord.Object(id=GUILD_ID),
|
||||||
|
)
|
||||||
async def shutdown(interaction: discord.Interaction):
|
async def shutdown(interaction: discord.Interaction):
|
||||||
if interaction.user.id == OWNER_ID:
|
if interaction.user.id == OWNER_ID:
|
||||||
await interaction.response.send_message("Night, Night. Shutting down...")
|
await interaction.response.send_message(
|
||||||
|
"Night, Night. Shutting down..."
|
||||||
|
)
|
||||||
await self.close()
|
await self.close()
|
||||||
else:
|
else:
|
||||||
await interaction.response.send_message("Hey! I don't know you! You don't have permission to use this command. Only the owner can do that.", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
|
"Hey! I don't know you! You don't have permission to use "
|
||||||
|
"this command. Only the owner can do that.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.tree.command(
|
||||||
|
name="report_issue", description="Send an anonymous issue report."
|
||||||
|
)
|
||||||
|
async def report_issue(interaction: discord.Interaction, message: str):
|
||||||
|
if len(message) > 1000:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Wowie, that's a long message there. Chief asked for "
|
||||||
|
"messages to be no longer than 1000 characters. Please "
|
||||||
|
"shorten it and try again.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
owner = await self.fetch_user(OWNER_ID)
|
||||||
|
await owner.send(f"Hey Chief, I got a report for you: {message}")
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"I've passed on your issue to the Chief!", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.tree.command(
|
||||||
|
name="sync_commands", description="Force Sync to chosen guild."
|
||||||
|
)
|
||||||
|
@app_commands.check(is_owner_or_admin)
|
||||||
|
async def sync_commands(interaction: discord.Interaction, guild_id: str):
|
||||||
|
try:
|
||||||
|
guild_id_int = int(guild_id)
|
||||||
|
guild = self.get_guild(guild_id_int)
|
||||||
|
if guild is None:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Guild not found.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await self.tree.sync(guild=discord.Object(id=guild_id_int))
|
||||||
|
self.tree.copy_global_to(guild=discord.Object(id=guild_id_int))
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Commands synced successfully for guild {guild.name}.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Invalid guild ID.", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f"Logged on as {self.user}!")
|
logger.info(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))
|
||||||
self.loop.create_task(reminder_worker())
|
self.loop.create_task(self.reminder_worker())
|
||||||
print("Starting reminder worker...")
|
logger.info("Starting reminder worker...")
|
||||||
|
|
||||||
|
async def on_disconnect(self):
|
||||||
|
logger.info(f"{self.user} has disconnected. ", "Attempting to reconnect...")
|
||||||
|
|
||||||
|
async def on_resumed(self):
|
||||||
|
logger.info(f"{self.user} has resumed the connection.")
|
||||||
|
|
||||||
|
async def on_error(self, event, *args, **kwargs):
|
||||||
|
logger.error(f"An error occurred: {event}")
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def on_connect(self):
|
||||||
|
logger.info(f"{self.user} has connected.")
|
||||||
|
|
||||||
async def reminder_worker(self):
|
async def reminder_worker(self):
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(60) # check every minute
|
await asyncio.sleep(60) # check every minute
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
async with aiosqlite.connect(DATABASE) as db:
|
async with aiosqlite.connect(DATABASE) as db:
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT id, description, reminder_time, notification_channel_id FROM tasks WHERE reminder_time <= ? AND reminder_sent = FALSE",
|
"SELECT id, description, reminder_time, "
|
||||||
(now,)
|
"notification_channel_id FROM tasks WHERE reminder_time "
|
||||||
|
"<= ? AND reminder_sent = FALSE",
|
||||||
|
(now,),
|
||||||
)
|
)
|
||||||
tasks = await cursor.fetchall()
|
tasks = await cursor.fetchall()
|
||||||
for task_id, description, reminder_time, channel_id in tasks:
|
for task_id, description, reminder_time, channel_id in tasks:
|
||||||
if channel_id:
|
if channel_id:
|
||||||
channel = client.get_channel(int(channel_id))
|
channel = self.get_channel(int(channel_id))
|
||||||
if channel:
|
if channel:
|
||||||
await channel.send(f"Reminder for task: {description}")
|
await channel.send(f"Reminder for task: {description}")
|
||||||
await db.execute("UPDATE tasks SET reminder_sent = TRUE WHERE id = ?", (task_id,))
|
await db.execute(
|
||||||
|
"UPDATE tasks SET reminder_sent = TRUE " "WHERE id = ?",
|
||||||
|
(task_id,),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Failed to find channel with ID {channel_id}")
|
print(f"Couldn't Find Channel {channel_id}")
|
||||||
else:
|
else:
|
||||||
print(f"No channel ID provided for task ID {task_id}")
|
print(f"No channel ID provided for task ID {task_id}")
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
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:
|
||||||
# First, check if the user has consented to data storage.
|
|
||||||
consented = await check_user_consent(interaction.user.id)
|
consented = await check_user_consent(interaction.user.id)
|
||||||
|
|
||||||
if not consented:
|
if not consented:
|
||||||
# If there is no consent, show the consent dialog,
|
if interaction.command.name == "opt-out":
|
||||||
# unless the command is to opt-out, which should be accessible without prior consent.
|
|
||||||
if interaction.command.name == 'opt-out':
|
|
||||||
# Allow users to opt-out directly if they mistakenly initiated any command.
|
|
||||||
return
|
return
|
||||||
|
|
||||||
view = ConsentView()
|
view = ConsentView()
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"By using the services I, Nessa, provide, you consent to the storage of your data necessary for functionality. Please confirm your consent. See /nessa consent privacy-policy for more details.",
|
"By using the services I, Nessa, provide, you consent to "
|
||||||
view=view,
|
"the storage of your data necessary for functionality. "
|
||||||
ephemeral=True
|
"Please confirm your consent. "
|
||||||
|
"See /nessa consent privacy-policy for more details.",
|
||||||
|
view=view,
|
||||||
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
await view.wait()
|
await view.wait()
|
||||||
if view.value:
|
if view.value:
|
||||||
await store_user_consent(interaction.user.id)
|
await store_user_consent(interaction.user.id)
|
||||||
else:
|
else:
|
||||||
await interaction.followup.send("Whoops! You have to give me your okay to do store your data before you can use my services!", ephemeral=True)
|
await interaction.followup.send(
|
||||||
return # Stop processing if they do not consent
|
"Whoops! You have to give me your okay to do store "
|
||||||
|
"your data before you can use my services!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# For opt-in command, check if they're trying to opt-in after opting out.
|
if interaction.command.name == "opt-in":
|
||||||
if interaction.command.name == 'opt-in':
|
|
||||||
if consented:
|
if consented:
|
||||||
await interaction.response.send_message("Hey! Thanks, but you've already opted in.", ephemeral=True)
|
await interaction.response.send_message(
|
||||||
return
|
"Hey! Thanks, but you've already opted in.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Reference in New Issue
Block a user