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.
This commit is contained in:
Dan
2024-05-20 09:48:34 -04:00
parent d42975d981
commit efd6cdc0d5
9 changed files with 577 additions and 205 deletions

View File

@ -1,11 +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
- repo: https://github.com/pycqa/flake8
rev: 7.0.0 # Use the latest revision
hooks:
- id: flake8

12
main.py
View File

@ -1,16 +1,20 @@
from nessa.nessa import Nessa
from nessa.database import init_db
import asyncio
from dotenv import load_dotenv
import os
from dotenv import load_dotenv
from nessa.database import init_db
from nessa.nessa import Nessa
load_dotenv()
TOKEN = os.getenv("DISCORD_BOT_TOKEN")
async def main():
await init_db()
client = Nessa()
await client.start(TOKEN)
asyncio.run(main())
asyncio.run(main())

View File

@ -1,28 +1,49 @@
import os
from datetime import datetime
import discord
from discord import app_commands
from .database import add_project, get_project_id, get_project_name, add_task_to_project, update_task, list_projects, list_tasks_for_project, remove_task, remove_project
from .consent import check_user_consent, store_user_consent, revoke_user_consent
from .confirm import ConfirmView, ConfirmTaskDeletionView
from datetime import datetime
from .logger import logger
from dotenv import load_dotenv
import os
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):
async def create_project(
self, interaction: discord.Interaction, name: str, description: str
):
try:
project_id = await add_project(name, description)
await interaction.response.send_message(f"Noted. I've created a new project called '{name}'. You can look it up using the `/project list` command. Your project ID is {project_id}.")
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)
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.")
@ -30,103 +51,213 @@ class ProjectCommands(app_commands.Group):
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.")
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.")
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)
await interaction.response.send_message(
"Whoops! I failed to retrieve the list of projects. Did I "
"misplace that database somewhere? Try again later!.",
ephemeral=True,
)
logger.error(f"Error in list_projects: {e}")
@app_commands.command(name="remove", description="Remove a specific project and all its tasks.")
async def remove_project_command(self, interaction: discord.Interaction, project_id: int):
# Send a message with the confirmation view
@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 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:
await interaction.response.send_message(f"Whoops! I failed to remove the project. Error: {e}", ephemeral=True)
logger.error(f"Preparation error in remove_project_command for Project ID {project_id}: {e}")
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 a new task to a project.")
@app_commands.choices(priority=[
app_commands.Choice(name="Low", value="Low"),
app_commands.Choice(name="Medium", value="Medium"),
app_commands.Choice(name="High", value="High"),
app_commands.Choice(name="Critical", value="Critical")
], status=[
app_commands.Choice(name="Not Started", value="Not Started"),
app_commands.Choice(name="In Progress", value="In Progress"),
app_commands.Choice(name="Blocked", value="Blocked"),
app_commands.Choice(name="In Testing", value="In Testing"),
app_commands.Choice(name="Needs Review", value="Needs Review"),
app_commands.Choice(name="Completed", value="Completed")
])
async def add_task(self, interaction: discord.Interaction, project_name: str, description: str, assignee: str, deadline: str, status: str, priority: str, reminder_time: str = None):
@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 deadline date format
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") # 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)
if project_id:
await add_task_to_project(project_id, description, assignee, deadline, status, priority, reminder_dt)
response_msg = f"Okay, I've added Task '{description}' to your project,'{project_name}'."
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 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)
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:
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}")
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 the right format, i.e. MM/DD/YYYY for dates, and HH:MM for times. I can't do anything otherwise!", ephemeral=True)
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)
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}")
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):
@app_commands.choices(
priority=[
app_commands.Choice(name="Low", value="Low"),
app_commands.Choice(name="Medium", value="Medium"),
app_commands.Choice(name="High", value="High"),
app_commands.Choice(name="Critical", value="Critical"),
],
status=[
app_commands.Choice(name="Not Started", value="Not Started"),
app_commands.Choice(name="In Progress", value="In Progress"),
app_commands.Choice(name="Blocked", value="Blocked"),
app_commands.Choice(name="In Testing", value="In Testing"),
app_commands.Choice(name="Needs Review", value="Needs Review"),
app_commands.Choice(name="Completed", value="Completed"),
],
)
async def update_task_command(
self,
interaction: discord.Interaction,
task_id: int,
description: str,
assignee: str,
deadline: str,
status: str,
priority: str,
reminder_time: str = None,
):
try:
datetime.strptime(deadline, "%m/%d/%Y") # Validate deadline date format
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") # Validate and convert reminder time if provided
await update_task(task_id, description, assignee, deadline, status, priority, reminder_dt)
response_msg = f"Okay hun, so I updated the task with ID {task_id} with the changes you wanted made!"
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 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)
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:
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)
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)
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)
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.")
@ -134,49 +265,110 @@ class TaskCommands(app_commands.Group):
try:
project_name = await get_project_name(project_id)
if not project_name:
await interaction.response.send_message(f"I may be the godesss of memory, but I don't have a project with that ID {project_id} in my memory. are you sure you entered the right ID?", ephemeral=True)
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: {assignee}, Deadline: {deadline}, Status: {status}, Priority: {priority}"
for id, description, assignee, deadline, status, priority in tasks])
await interaction.response.send_message(f"So, Here are all the tasks I found for that project '{project_name}':\n{message}")
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 '{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:
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}")
@app_commands.command(name="remove", description="Remove a specific task.")
async def remove_task_command(self, interaction: discord.Interaction, task_id: int):
# Send a message with the confirmation view
view = ConfirmTaskDeletionView(task_id)
await interaction.response.send_message(f"So, are you sure you want to remove Task ID {task_id}? Once I forget, that's it! You'll have 180 seconds to confirm.", view=view, ephemeral=True)
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.")
@app_commands.command(
name="opt-in", description="Opt-in to data storage" "and use Nessa's Services."
)
async def opt_in(self, interaction: discord.Interaction):
await store_user_consent(interaction.user.id)
await interaction.response.send_message("Hey there! Thanks for opt-ing in to allowing me to store the data for your projects.", ephemeral=True)
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):
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.")
await interaction.response.send_message(
"Awe. It's ok. I won't store your data but this also means you "
"won't be able to use my services. You can always opt-in again. "
"Bye!",
ephemeral=True,
)
@app_commands.command(
name="privacy-policy", description="View Nessa's data handling policy."
)
async def privacy_policy(self, interaction: discord.Interaction):
# Replace 'YOUR_USER_ID' with the actual numeric ID of the user 'advtech'
policy_details = f"""Hi there! So you're looking to hire Nessa, the godess of memory! Let me tell you what you need to know. You need to consent to me storing what you send me in my 'memory' (better known as a database). What I am storing is: Your Disocrd ID, Projects, Tasks, and other data related to your projects. Why I need it? Well, I need your ID so as track your consent to store your data. If I don't have your consent, You can't use my services and your data isn't stored. As far as project and task management is concerned, I store just what you send me. I do not share it with any third-parties. I do not use any external services. I store it in my local 'memory'. Your data is not used for anything else, including but not limited to machine learning. Your data can be removed at any time using the commands. If you wish to opt-out, please use the `/consent opt-out` command. You need anything else, talk to my boss: <@{contact_user_id}> on Discord."""
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."):
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())
self.add_command(ConsentCommands())

View File

@ -1,10 +1,13 @@
import discord
from discord.ui import View, Button
from .logger import logger
from discord.ui import Button, View
from .database import remove_project, remove_task
from .logger import logger
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)
self.project_id = project_id
self.value = None
@ -14,21 +17,36 @@ class ConfirmView(View):
# Attempt to remove the project
try:
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)
logger.info(f"Project ID {self.project_id} and all associated tasks removed successfully.")
await interaction.response.edit_message(
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:
error_message = f"Failed to remove the project. Error: {e}"
await interaction.response.edit_message(content=error_message, view=None)
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)
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()
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)
self.task_id = task_id
self.value = None
@ -38,7 +56,13 @@ class ConfirmTaskDeletionView(View):
# Attempt to remove the task
try:
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.")
except Exception as 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)
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)
self.stop()
await interaction.response.edit_message(
content="Wait! You changed your mind! I won't forget the task.", view=None
)
self.stop()

View File

@ -1,9 +1,10 @@
import discord
from discord.ui import View, Button
import aiosqlite
import discord
from discord.ui import Button, View
DATABASE = "nessa.db"
class ConsentView(View):
def __init__(self):
super().__init__()
@ -13,30 +14,55 @@ class ConsentView(View):
async def confirm(self, interaction: discord.Interaction, button: Button):
self.value = True
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()
@discord.ui.button(label="Decline Data Storage", style=discord.ButtonStyle.grey)
async def cancel(self, interaction: discord.Interaction, button: Button):
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()
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:
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()
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):
"""Store the user's consent to data storage."""
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()
async def revoke_user_consent(user_id):
"""Revoke the user's consent to data storage."""
async with aiosqlite.connect(DATABASE) as db:
await db.execute("UPDATE user_consents SET consent_given = FALSE WHERE user_id = ?", (user_id,))
await db.commit()
await db.execute(
"UPDATE user_consents SET consent_given = FALSE" "WHERE user_id = ?",
(user_id,),
)
await db.commit()

View File

@ -1,18 +1,21 @@
import aiosqlite
from datetime import datetime
DATABASE = "nessa.db"
async def init_db():
async with aiosqlite.connect(DATABASE) as db:
await db.execute("PRAGMA foreign_keys = ON") # Enable foreign key constraint support
await db.execute('''
await db.execute("PRAGMA foreign_keys = ON")
await db.execute(
"""
CREATE TABLE IF NOT EXISTS projects(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
description TEXT)
''')
await db.execute('''
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS tasks(
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER,
@ -25,67 +28,130 @@ async def init_db():
reminder_time DATETIME,
reminder_sent BOOLEAN DEFAULT FALSE,
FOREIGN KEY(project_id) REFERENCES projects(id))
''')
await db.execute('''
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS user_consents(
user_id INTEGER PRIMARY KEY,
consent_given BOOLEAN NOT NULL)
''')
"""
)
await db.commit()
async def add_project(name, description):
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()
return cursor.lastrowid # Returns the ID of the newly created project
async def get_project_id(name):
async with aiosqlite.connect(DATABASE) as db:
cursor = await db.execute("SELECT id FROM projects WHERE name = ?", (name,))
result = await cursor.fetchone()
return result[0] if result else None
async def get_project_name(project_id):
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()
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:
await db.execute(
"INSERT INTO tasks (project_id, description, assignee, deadline, status, priority, notification_channel_id, reminder_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(project_id, description, assignee, deadline, status, priority, notification_channel_id, reminder_time)
"INSERT INTO tasks (project_id, description, assignee, deadline, "
"status, priority, notification_channel_id, reminder_time) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?)",
(
project_id,
description,
assignee,
deadline,
status,
priority,
notification_channel_id,
reminder_time,
),
)
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:
await db.execute(
"UPDATE tasks SET description=?, assignee=?, deadline=?, status=?, priority=?, notification_channel_id=?, reminder_time=? WHERE id=?",
(description, assignee, deadline, status, priority, notification_channel_id, reminder_time, task_id)
"UPDATE tasks SET description=?, assignee=?, deadline=?, status=?,"
"priority=?, notification_channel_id=?,reminder_time=? WHERE id=?",
(
description,
assignee,
deadline,
status,
priority,
notification_channel_id,
reminder_time,
task_id,
),
)
await db.commit()
async def list_projects():
async with aiosqlite.connect(DATABASE) as db:
cursor = await db.execute("SELECT id, name, description FROM projects")
projects = await cursor.fetchall()
return projects
async def list_tasks_for_project(project_id):
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()
return tasks
async def remove_task(task_id):
async with aiosqlite.connect(DATABASE) as db:
await db.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
await db.commit()
async def remove_project(project_id):
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.commit()
await db.commit()

View File

@ -2,17 +2,19 @@ import logging
from logging.handlers import RotatingFileHandler
# Setup basic configuration
logging.basicConfig(level=logging.ERROR, # Change to DEBUG for detailed output during development
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
logging.basicConfig(
level=logging.ERROR,
format="%(asctime)s - %(name)s - " "%(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Create a rotating file handler
log_file = 'nessa.log'
handler = RotatingFileHandler(log_file, maxBytes=1e6, backupCount=5) # Increased file size limit and backup count
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log_file = "nessa.log"
handler = RotatingFileHandler(log_file, maxBytes=1e6, backupCount=5)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
# Configure the logger
logger = logging.getLogger('Nessa')
logger = logging.getLogger("Nessa")
logger.addHandler(handler)
logger.setLevel(logging.ERROR) # Set to DEBUG for comprehensive logging
logger.setLevel(logging.ERROR) # Set to DEBUG for comprehensive logging

View File

@ -1,18 +1,22 @@
import asyncio
import os
from datetime import datetime
import aiosqlite
import discord
from discord import app_commands
import aiosqlite
import asyncio
from datetime import datetime
from dotenv import load_dotenv
from .commands import NessaTracker
from .consent import ConsentView, check_user_consent, store_user_consent
from .database import DATABASE
from dotenv import load_dotenv
import os
from .logger import logger
load_dotenv()
GUILD_ID = int(os.getenv("DISCORD_GUILD_ID"))
OWNER_ID = int(os.getenv("AUTHORIZED_USER_ID"))
class Nessa(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
@ -22,106 +26,154 @@ class Nessa(discord.Client):
self.tree.add_command(NessaTracker())
self.tree.copy_global_to(guild=discord.Object(id=GUILD_ID))
await self.tree.sync(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))
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):
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()
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)
@self.tree.command(name="report_issue", description="Send an anonymous issue report.")
async def report_issue(interaction: discord.Interaction, message: str):
# Check if the message exceeds 1000 characters
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
# Your Discord user ID
your_user_id = OWNER_ID # Make sure OWNER_ID is defined somewhere in your code
# Fetch your user object using your ID
owner = await self.fetch_user(your_user_id)
# Send the DM to you
await owner.send(f"Hey Chief, I got a report for you: {message}")
# Respond to the user to confirm the message has been sent
await interaction.response.send_message("I've passed on your issue to the Chief!", 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="sync_commands", description="Force sync commands in a specific guild.")
@app_commands.check(is_owner_or_admin) # or @discord.app_commands.checks.is_owner()
@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)
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)
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)
await interaction.response.send_message(
"Invalid guild ID.", ephemeral=True
)
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))
await self.tree.sync(guild=discord.Object(id=GUILD_ID))
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):
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,)
"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))
channel = self.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,))
await db.execute(
"UPDATE tasks SET reminder_sent = TRUE " "WHERE id = ?",
(task_id,),
)
else:
print(f"Failed to find channel with ID {channel_id}")
print(f"Couldn't Find Channel {channel_id}")
else:
print(f"No channel ID provided for task ID {task_id}")
await db.commit()
async def on_interaction(self, interaction: discord.Interaction):
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)
if not consented:
if interaction.command.name == 'opt-out':
# Allow opt-out without consent.
if interaction.command.name == "opt-out":
return
# Prompt for consent
view = ConsentView()
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.",
view=view,
ephemeral=True
"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.",
view=view,
ephemeral=True,
)
await view.wait()
if view.value:
await store_user_consent(interaction.user.id)
else:
# This is the correct use of followup after the initial response
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)
return # Stop processing if they do not consent
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,
)
return
if interaction.command.name == 'opt-in':
if interaction.command.name == "opt-in":
if consented:
# Directly send a response if already consented
await interaction.response.send_message("Hey! Thanks, but you've already opted in.", ephemeral=True)
return
await interaction.response.send_message(
"Hey! Thanks, but you've already opted in.", ephemeral=True
)
return

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[flake8]
extend-ignore = E501
max-line-length = 120