mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
471 lines
13 KiB
Python
471 lines
13 KiB
Python
#-*- coding: utf-8 -*-
|
|
|
|
from datetime import datetime
|
|
from sqlite3 import IntegrityError
|
|
from threading import Timer
|
|
from typing import List, Literal
|
|
|
|
from apprise import Apprise
|
|
from dateutil.relativedelta import relativedelta
|
|
from flask import Flask
|
|
|
|
from backend.custom_exceptions import (InvalidKeyValue, InvalidTime,
|
|
NotificationServiceNotFound,
|
|
ReminderNotFound)
|
|
from backend.db import close_db, get_db
|
|
|
|
filter_function = lambda query, p: (
|
|
query in p["title"].lower()
|
|
or query in p["text"].lower()
|
|
or query in p["notification_service_title"].lower()
|
|
)
|
|
|
|
def _find_next_time(
|
|
original_time: int,
|
|
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"],
|
|
repeat_interval: int
|
|
) -> int:
|
|
td = relativedelta(**{repeat_quantity: repeat_interval})
|
|
new_time = datetime.fromtimestamp(original_time)
|
|
current_time = datetime.fromtimestamp(datetime.utcnow().timestamp())
|
|
while new_time <= current_time:
|
|
new_time += td
|
|
return int(new_time.timestamp())
|
|
|
|
class ReminderHandler:
|
|
"""Handle set reminders
|
|
"""
|
|
def __init__(self, context) -> None:
|
|
self.context = context
|
|
self.next_trigger = {
|
|
'thread': None,
|
|
'time': None
|
|
}
|
|
|
|
return
|
|
|
|
def __trigger_reminders(self, time: int) -> None:
|
|
"""Trigger all reminders that are set for a certain time
|
|
|
|
Args:
|
|
time (int): The time of the reminders to trigger
|
|
"""
|
|
with self.context():
|
|
cursor = get_db(dict)
|
|
cursor.execute("""
|
|
SELECT
|
|
r.id,
|
|
r.title, r.text,
|
|
r.repeat_quantity, r.repeat_interval, r.original_time
|
|
FROM reminders r
|
|
WHERE time = ?;
|
|
""", (time,))
|
|
reminders = list(map(dict, cursor))
|
|
|
|
for reminder in reminders:
|
|
cursor.execute("""
|
|
SELECT url
|
|
FROM reminder_services rs
|
|
INNER JOIN notification_services ns
|
|
ON rs.notification_service_id = ns.id
|
|
WHERE rs.reminder_id = ?;
|
|
""", (reminder['id'],))
|
|
|
|
# Send of reminder
|
|
a = Apprise()
|
|
for url in cursor:
|
|
a.add(url['url'])
|
|
a.notify(title=reminder["title"], body=reminder["text"])
|
|
|
|
if reminder['repeat_quantity'] is None:
|
|
# Delete the reminder from the database
|
|
cursor.execute(
|
|
"DELETE FROM reminders WHERE id = ?;",
|
|
(reminder['id'],)
|
|
)
|
|
else:
|
|
# Set next time
|
|
new_time = _find_next_time(
|
|
reminder['original_time'],
|
|
reminder['repeat_quantity'],
|
|
reminder['repeat_interval']
|
|
)
|
|
cursor.execute(
|
|
"UPDATE reminders SET time = ? WHERE id = ?;",
|
|
(new_time, reminder['id'])
|
|
)
|
|
|
|
self.next_trigger.update({
|
|
'thread': None,
|
|
'time': None
|
|
})
|
|
self.find_next_reminder()
|
|
|
|
def find_next_reminder(self, time: int=None) -> None:
|
|
"""Determine when the soonest reminder is and set the timer to that time
|
|
|
|
Args:
|
|
time (int, optional): The timestamp to check for. Otherwise check soonest in database. Defaults to None.
|
|
"""
|
|
if not time:
|
|
with self.context():
|
|
time = get_db().execute("""
|
|
SELECT DISTINCT r1.time
|
|
FROM reminders r1
|
|
LEFT JOIN reminders r2
|
|
ON r1.time > r2.time
|
|
WHERE r2.id IS NULL;
|
|
""").fetchone()
|
|
if time is None:
|
|
return
|
|
time = time[0]
|
|
|
|
if (self.next_trigger['thread'] is None
|
|
or time < self.next_trigger['time']):
|
|
if self.next_trigger['thread'] is not None:
|
|
self.next_trigger['thread'].cancel()
|
|
t = time - datetime.utcnow().timestamp()
|
|
self.next_trigger['thread'] = Timer(t, self.__trigger_reminders, (time,))
|
|
self.next_trigger['thread'].start()
|
|
self.next_trigger['time'] = time
|
|
|
|
def stop_handling(self) -> None:
|
|
"""Stop the timer if it's active
|
|
"""
|
|
if self.next_trigger['thread'] is not None:
|
|
self.next_trigger['thread'].cancel()
|
|
return
|
|
|
|
handler_context = Flask('handler')
|
|
handler_context.teardown_appcontext(close_db)
|
|
reminder_handler = ReminderHandler(handler_context.app_context)
|
|
|
|
class Reminder:
|
|
"""Represents a reminder
|
|
"""
|
|
def __init__(self, reminder_id: int):
|
|
self.id = reminder_id
|
|
|
|
# Check if reminder exists
|
|
if not get_db().execute(
|
|
"SELECT 1 FROM reminders WHERE id = ? LIMIT 1",
|
|
(self.id,)
|
|
).fetchone():
|
|
raise ReminderNotFound
|
|
|
|
def get(self) -> dict:
|
|
"""Get info about the reminder
|
|
|
|
Returns:
|
|
dict: The info about the reminder
|
|
"""
|
|
reminder = get_db(dict).execute("""
|
|
SELECT
|
|
id,
|
|
title, text,
|
|
time,
|
|
repeat_quantity,
|
|
repeat_interval,
|
|
color
|
|
FROM reminders
|
|
WHERE id = ?
|
|
LIMIT 1;
|
|
""",
|
|
(self.id,)
|
|
).fetchone()
|
|
reminder = dict(reminder)
|
|
|
|
reminder['notification_services'] = list(map(lambda r: r[0], get_db().execute("""
|
|
SELECT notification_service_id
|
|
FROM reminder_services
|
|
WHERE reminder_id = ?;
|
|
""", (self.id,))))
|
|
|
|
return reminder
|
|
|
|
def update(
|
|
self,
|
|
title: str = None,
|
|
time: int = None,
|
|
notification_services: List[int] = None,
|
|
text: str = None,
|
|
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
|
|
repeat_interval: int = None,
|
|
color: str = None
|
|
) -> dict:
|
|
"""Edit the reminder
|
|
|
|
Args:
|
|
title (str): The new title of the entry. Defaults to None.
|
|
time (int): The new UTC epoch timestamp the the reminder should be send. Defaults to None.
|
|
notification_services (List[int]): The new list of id's of the notification services to use to send the reminder. Defaults to None.
|
|
text (str, optional): The new body of the reminder. Defaults to None.
|
|
repeat_quantity (Literal["years", "months", "weeks", "days", "hours", "minutes"], optional): The new quantity of the repeat specified for the reminder. Defaults to None.
|
|
repeat_interval (int, optional): The new amount of repeat_quantity, like "5" (hours). Defaults to None.
|
|
color (str, optional): The new hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
|
|
|
|
Raises:
|
|
NotificationServiceNotFound: One of the notification services was not found
|
|
|
|
Returns:
|
|
dict: The new reminder info
|
|
"""
|
|
cursor = get_db()
|
|
|
|
# Validate data
|
|
if repeat_quantity is None and repeat_interval is not None:
|
|
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
|
elif repeat_quantity is not None and repeat_interval is None:
|
|
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
|
repeated_reminder = repeat_quantity is not None and repeat_interval is not None
|
|
|
|
if time is not None:
|
|
if not repeated_reminder:
|
|
if time < datetime.utcnow().timestamp():
|
|
raise InvalidTime
|
|
time = round(time)
|
|
|
|
# Get current data and update it with new values
|
|
data = self.get()
|
|
new_values = {
|
|
'title': title,
|
|
'time': time,
|
|
'text': text,
|
|
'repeat_quantity': repeat_quantity,
|
|
'repeat_interval': repeat_interval,
|
|
'color': color
|
|
}
|
|
for k, v in new_values.items():
|
|
if k in ('repeat_quantity', 'repeat_interval', 'color') or v is not None:
|
|
data[k] = v
|
|
|
|
# Update database
|
|
if not repeated_reminder:
|
|
next_time = data["time"]
|
|
cursor.execute("""
|
|
UPDATE reminders
|
|
SET
|
|
title=?, text=?,
|
|
time=?,
|
|
repeat_quantity=?, repeat_interval=?,
|
|
color=?
|
|
WHERE id = ?;
|
|
""", (
|
|
data["title"],
|
|
data["text"],
|
|
data["time"],
|
|
data["repeat_quantity"],
|
|
data["repeat_interval"],
|
|
data["color"],
|
|
self.id
|
|
))
|
|
else:
|
|
next_time = _find_next_time(
|
|
data["time"],
|
|
data["repeat_quantity"], data["repeat_interval"]
|
|
)
|
|
cursor.execute("""
|
|
UPDATE reminders
|
|
SET
|
|
title=?, text=?,
|
|
time=?,
|
|
repeat_quantity=?, repeat_interval=?, original_time=?,
|
|
color=?
|
|
WHERE id = ?;
|
|
""", (
|
|
data["title"],
|
|
data["text"],
|
|
next_time,
|
|
data["repeat_quantity"],
|
|
data["repeat_interval"],
|
|
data["time"],
|
|
data["color"],
|
|
self.id
|
|
))
|
|
|
|
if notification_services:
|
|
cursor.connection.isolation_level = None
|
|
cursor.execute("BEGIN TRANSACTION;")
|
|
cursor.execute("DELETE FROM reminder_services WHERE reminder_id = ?", (self.id,))
|
|
try:
|
|
cursor.executemany(
|
|
"INSERT INTO reminder_services(reminder_id, notification_service_id) VALUES (?,?)",
|
|
((self.id, s) for s in notification_services)
|
|
)
|
|
cursor.execute("COMMIT;")
|
|
except IntegrityError:
|
|
raise NotificationServiceNotFound
|
|
cursor.connection.isolation_level = ""
|
|
|
|
reminder_handler.find_next_reminder(next_time)
|
|
return self.get()
|
|
|
|
def delete(self) -> None:
|
|
"""Delete the reminder
|
|
"""
|
|
get_db().execute("DELETE FROM reminders WHERE id = ?", (self.id,))
|
|
reminder_handler.find_next_reminder()
|
|
return
|
|
|
|
class Reminders:
|
|
"""Represents the reminder library of the user account
|
|
"""
|
|
sort_functions = {
|
|
'title': (lambda r: (r['title'], r['time']), False),
|
|
'title_reversed': (lambda r: (r['title'], r['time']), True),
|
|
'time': (lambda r: r['time'], False),
|
|
'time_reversed': (lambda r: r['time'], True)
|
|
}
|
|
|
|
def __init__(self, user_id: int):
|
|
self.user_id = user_id
|
|
|
|
def fetchall(self, sort_by: Literal["time", "time_reversed", "title", "title_reversed"] = "time") -> List[dict]:
|
|
"""Get all reminders
|
|
|
|
Args:
|
|
sort_by (Literal["time", "time_reversed", "title", "title_reversed"], optional): How to sort the result. Defaults to "time".
|
|
|
|
Returns:
|
|
List[dict]: The id, title, text, time and color of each reminder
|
|
"""
|
|
sort_function = self.sort_functions.get(
|
|
sort_by,
|
|
self.sort_functions['time']
|
|
)
|
|
|
|
# Fetch all reminders
|
|
reminders: list = list(map(dict, get_db(dict).execute("""
|
|
SELECT
|
|
id,
|
|
title, text,
|
|
time,
|
|
repeat_quantity,
|
|
repeat_interval,
|
|
color
|
|
FROM reminders
|
|
WHERE user_id = ?;
|
|
""",
|
|
(self.user_id,)
|
|
)))
|
|
|
|
# Sort result
|
|
reminders.sort(key=sort_function[0], reverse=sort_function[1])
|
|
|
|
return reminders
|
|
|
|
def search(self, query: str) -> List[dict]:
|
|
"""Search for reminders
|
|
|
|
Args:
|
|
query (str): The term to search for
|
|
|
|
Returns:
|
|
List[dict]: All reminders that match. Similar output to self.fetchall
|
|
"""
|
|
query = query.lower()
|
|
reminders = list(filter(
|
|
lambda p: filter_function(query, p),
|
|
self.fetchall()
|
|
))
|
|
return reminders
|
|
|
|
def fetchone(self, id: int) -> Reminder:
|
|
"""Get one reminder
|
|
|
|
Args:
|
|
id (int): The id of the reminder to fetch
|
|
|
|
Returns:
|
|
Reminder: A Reminder instance
|
|
"""
|
|
return Reminder(id)
|
|
|
|
def add(
|
|
self,
|
|
title: str,
|
|
time: int,
|
|
notification_services: List[int],
|
|
text: str = '',
|
|
repeat_quantity: Literal["years", "months", "weeks", "days", "hours", "minutes"] = None,
|
|
repeat_interval: int = None,
|
|
color: str = None
|
|
) -> Reminder:
|
|
"""Add a reminder
|
|
|
|
Args:
|
|
title (str): The title of the entry
|
|
time (int): The UTC epoch timestamp the the reminder should be send.
|
|
notification_services (List[int]): The id's of the notification services to use to send the reminder.
|
|
text (str, optional): The body of the reminder. Defaults to ''.
|
|
repeat_quantity (Literal["years", "months", "weeks", "days", "hours", "minutes"], optional): The quantity of the repeat specified for the reminder. Defaults to None.
|
|
repeat_interval (int, optional): The amount of repeat_quantity, like "5" (hours). Defaults to None.
|
|
color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None.
|
|
|
|
Raises:
|
|
NotificationServiceNotFound: One of the notification services was not found
|
|
|
|
Returns:
|
|
dict: The info about the reminder
|
|
"""
|
|
if time < datetime.utcnow().timestamp():
|
|
raise InvalidTime
|
|
time = round(time)
|
|
|
|
if repeat_quantity is None and repeat_interval is not None:
|
|
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
|
elif repeat_quantity is not None and repeat_interval is None:
|
|
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
|
|
|
cursor = get_db()
|
|
if repeat_quantity is None and repeat_interval is None:
|
|
id = cursor.execute("""
|
|
INSERT INTO reminders(user_id, title, text, time, color)
|
|
VALUES (?, ?, ?, ?, ?);
|
|
""", (self.user_id, title, text, time, color)
|
|
).lastrowid
|
|
else:
|
|
id = cursor.execute("""
|
|
INSERT INTO reminders(user_id, title, text, time, repeat_quantity, repeat_interval, original_time, color)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
|
""", (self.user_id, title, text, time, repeat_quantity, repeat_interval, time, color)
|
|
).lastrowid
|
|
|
|
try:
|
|
cursor.executemany(
|
|
"INSERT INTO reminder_services(reminder_id, notification_service_id) VALUES (?, ?);",
|
|
((id, service) for service in notification_services)
|
|
)
|
|
except IntegrityError:
|
|
raise NotificationServiceNotFound
|
|
|
|
reminder_handler.find_next_reminder(time)
|
|
|
|
# Return info
|
|
return self.fetchone(id)
|
|
|
|
def test_reminder(
|
|
title: str,
|
|
notification_services: List[int],
|
|
text: str = ''
|
|
) -> None:
|
|
"""Test send a reminder draft
|
|
|
|
Args:
|
|
title (str): Title title of the entry
|
|
notification_service (int): The id of the notification service to use to send the reminder
|
|
text (str, optional): The body of the reminder. Defaults to ''.
|
|
"""
|
|
a = Apprise()
|
|
cursor = get_db(dict)
|
|
for service in notification_services:
|
|
url = cursor.execute(
|
|
"SELECT url FROM notification_services WHERE id = ? LIMIT 1;",
|
|
(service,)
|
|
).fetchone()
|
|
if not url:
|
|
raise NotificationServiceNotFound
|
|
a.add(url[0])
|
|
a.notify(title=title, body=text)
|
|
return
|