mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
797 lines
19 KiB
Python
797 lines
19 KiB
Python
#-*- coding: utf-8 -*-
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from sqlite3 import IntegrityError
|
|
from threading import Timer
|
|
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union
|
|
|
|
from apprise import Apprise
|
|
from dateutil.relativedelta import relativedelta
|
|
from dateutil.relativedelta import weekday as du_weekday
|
|
|
|
from backend.custom_exceptions import (InvalidKeyValue, InvalidTime,
|
|
NotificationServiceNotFound,
|
|
ReminderNotFound)
|
|
from backend.db import get_db
|
|
from backend.helpers import (RepeatQuantity, Singleton, SortingMethod,
|
|
search_filter, when_not_none)
|
|
from backend.logging import LOGGER
|
|
|
|
if TYPE_CHECKING:
|
|
from flask.ctx import AppContext
|
|
|
|
|
|
def __next_selected_day(
|
|
weekdays: List[int],
|
|
weekday: int
|
|
) -> int:
|
|
"""Find the next allowed day in the week.
|
|
|
|
Args:
|
|
weekdays (List[int]): The days of the week that are allowed.
|
|
Monday is 0, Sunday is 6.
|
|
weekday (int): The current weekday.
|
|
|
|
Returns:
|
|
int: The next allowed weekday.
|
|
"""
|
|
return (
|
|
# Get all days later than current, then grab first one.
|
|
[d for d in weekdays if weekday < d]
|
|
or
|
|
# weekday is last allowed day, so it should grab the first
|
|
# allowed day of the week.
|
|
weekdays
|
|
)[0]
|
|
|
|
def _find_next_time(
|
|
original_time: int,
|
|
repeat_quantity: Union[RepeatQuantity, None],
|
|
repeat_interval: Union[int, None],
|
|
weekdays: Union[List[int], None]
|
|
) -> int:
|
|
"""Calculate the next timestep based on original time and repeat/interval
|
|
values.
|
|
|
|
Args:
|
|
original_time (int): The original time of the repeating timestamp.
|
|
|
|
repeat_quantity (Union[RepeatQuantity, None]): If set, what the quantity
|
|
is of the repetition.
|
|
|
|
repeat_interval (Union[int, None]): If set, the value of the repetition.
|
|
|
|
weekdays (Union[List[int], None]): If set, on which days the time can
|
|
continue. Monday is 0, Sunday is 6.
|
|
|
|
Returns:
|
|
int: The next timestamp in the future.
|
|
"""
|
|
if weekdays is not None:
|
|
weekdays.sort()
|
|
|
|
new_time = datetime.fromtimestamp(original_time)
|
|
current_time = datetime.fromtimestamp(datetime.utcnow().timestamp())
|
|
|
|
if repeat_quantity is not None:
|
|
td = relativedelta(**{repeat_quantity.value: repeat_interval})
|
|
while new_time <= current_time:
|
|
new_time += td
|
|
|
|
elif weekdays is not None:
|
|
# We run the loop contents at least once and then actually use the cond.
|
|
# This is because we need to force the 'free' date to go to one of the
|
|
# selected weekdays.
|
|
# Say it's Monday, we set a reminder for Wednesday and make it repeat
|
|
# on Tuesday and Thursday. Then the first notification needs to go on
|
|
# Thurday, not Wednesday. So run code at least once to force that.
|
|
# Afterwards, it can run normally to push the timestamp into the future.
|
|
one_to_go = True
|
|
while one_to_go or new_time <= current_time:
|
|
next_day = __next_selected_day(weekdays, new_time.weekday())
|
|
proposed_time = new_time + relativedelta(weekday=du_weekday(next_day))
|
|
if proposed_time == new_time:
|
|
proposed_time += relativedelta(weekday=du_weekday(next_day, 2))
|
|
new_time = proposed_time
|
|
one_to_go = False
|
|
|
|
result = int(new_time.timestamp())
|
|
LOGGER.debug(
|
|
f'{original_time=}, {current_time=} ' +
|
|
f'and interval of {repeat_interval} {repeat_quantity} ' +
|
|
f'leads to {result}'
|
|
)
|
|
return result
|
|
|
|
|
|
class Reminder:
|
|
"""Represents a reminder
|
|
"""
|
|
def __init__(self, user_id: int, reminder_id: int) -> None:
|
|
"""Create an instance.
|
|
|
|
Args:
|
|
user_id (int): The ID of the user.
|
|
reminder_id (int): The ID of the reminder.
|
|
|
|
Raises:
|
|
ReminderNotFound: Reminder with given ID does not exist or is not
|
|
owned by user.
|
|
"""
|
|
self.id = reminder_id
|
|
|
|
# Check if reminder exists
|
|
if not get_db().execute(
|
|
"SELECT 1 FROM reminders WHERE id = ? AND user_id = ? LIMIT 1",
|
|
(self.id, user_id)
|
|
).fetchone():
|
|
raise ReminderNotFound
|
|
|
|
return
|
|
|
|
def _get_notification_services(self) -> List[int]:
|
|
"""Get ID's of notification services linked to the reminder.
|
|
|
|
Returns:
|
|
List[int]: The list with ID's.
|
|
"""
|
|
result = [
|
|
r[0]
|
|
for r in get_db().execute("""
|
|
SELECT notification_service_id
|
|
FROM reminder_services
|
|
WHERE reminder_id = ?;
|
|
""",
|
|
(self.id,)
|
|
)
|
|
]
|
|
return result
|
|
|
|
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,
|
|
weekdays,
|
|
color
|
|
FROM reminders
|
|
WHERE id = ?
|
|
LIMIT 1;
|
|
""",
|
|
(self.id,)
|
|
).fetchone()
|
|
reminder = dict(reminder)
|
|
|
|
reminder["weekdays"] = [
|
|
int(n)
|
|
for n in reminder["weekdays"].split(",")
|
|
if n
|
|
] if reminder["weekdays"] else None
|
|
reminder['notification_services'] = self._get_notification_services()
|
|
|
|
return reminder
|
|
|
|
def update(
|
|
self,
|
|
title: Union[None, str] = None,
|
|
time: Union[None, int] = None,
|
|
notification_services: Union[None, List[int]] = None,
|
|
text: Union[None, str] = None,
|
|
repeat_quantity: Union[None, RepeatQuantity] = None,
|
|
repeat_interval: Union[None, int] = None,
|
|
weekdays: Union[None, List[int]] = None,
|
|
color: Union[None, str] = None
|
|
) -> dict:
|
|
"""Edit the reminder.
|
|
|
|
Args:
|
|
title (Union[None, str]): The new title of the entry.
|
|
Defaults to None.
|
|
|
|
time (Union[None, int]): The new UTC epoch timestamp when the
|
|
reminder should be send.
|
|
Defaults to None.
|
|
|
|
notification_services (Union[None, List[int]]): The new list
|
|
of id's of the notification services to use to send the reminder.
|
|
Defaults to None.
|
|
|
|
text (Union[None, str], optional): The new body of the reminder.
|
|
Defaults to None.
|
|
|
|
repeat_quantity (Union[None, RepeatQuantity], optional): The new
|
|
quantity of the repeat specified for the reminder.
|
|
Defaults to None.
|
|
|
|
repeat_interval (Union[None, int], optional): The new amount of
|
|
repeat_quantity, like "5" (hours).
|
|
Defaults to None.
|
|
|
|
weekdays (Union[None, List[int]], optional): The new indexes of
|
|
the days of the week that the reminder should run.
|
|
Defaults to None.
|
|
|
|
color (Union[None, str], optional): The new hex code of the color
|
|
of the reminder, which is shown in the web-ui.
|
|
Defaults to None.
|
|
|
|
Note about args:
|
|
Either repeat_quantity and repeat_interval are given, weekdays is
|
|
given or neither, but not both.
|
|
|
|
Raises:
|
|
NotificationServiceNotFound: One of the notification services was not found.
|
|
InvalidKeyValue: The value of one of the keys is not valid or
|
|
the "Note about args" is violated.
|
|
|
|
Returns:
|
|
dict: The new reminder info.
|
|
"""
|
|
LOGGER.info(
|
|
f'Updating notification service {self.id}: '
|
|
+ f'{title=}, {time=}, {notification_services=}, {text=}, '
|
|
+ f'{repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}'
|
|
)
|
|
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)
|
|
elif weekdays is not None and repeat_quantity is not None:
|
|
raise InvalidKeyValue('weekdays', weekdays)
|
|
|
|
repeated_reminder = (
|
|
(repeat_quantity is not None and repeat_interval is not None)
|
|
or weekdays 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,
|
|
'weekdays': when_not_none(
|
|
weekdays,
|
|
lambda w: ",".join(map(str, sorted(w)))
|
|
),
|
|
'color': color
|
|
}
|
|
for k, v in new_values.items():
|
|
if (
|
|
k in ('repeat_quantity', 'repeat_interval', 'weekdays', 'color')
|
|
or v is not None
|
|
):
|
|
data[k] = v
|
|
|
|
# Update database
|
|
rq = when_not_none(
|
|
data["repeat_quantity"],
|
|
lambda q: q.value
|
|
)
|
|
if repeated_reminder:
|
|
next_time = _find_next_time(
|
|
data["time"],
|
|
data["repeat_quantity"],
|
|
data["repeat_interval"],
|
|
weekdays
|
|
)
|
|
cursor.execute("""
|
|
UPDATE reminders
|
|
SET
|
|
title=?,
|
|
text=?,
|
|
time=?,
|
|
repeat_quantity=?,
|
|
repeat_interval=?,
|
|
weekdays=?,
|
|
original_time=?,
|
|
color=?
|
|
WHERE id = ?;
|
|
""", (
|
|
data["title"],
|
|
data["text"],
|
|
next_time,
|
|
rq,
|
|
data["repeat_interval"],
|
|
data["weekdays"],
|
|
data["time"],
|
|
data["color"],
|
|
self.id
|
|
))
|
|
|
|
else:
|
|
next_time = data["time"]
|
|
cursor.execute("""
|
|
UPDATE reminders
|
|
SET
|
|
title=?,
|
|
text=?,
|
|
time=?,
|
|
repeat_quantity=?,
|
|
repeat_interval=?,
|
|
weekdays=?,
|
|
color=?
|
|
WHERE id = ?;
|
|
""", (
|
|
data["title"],
|
|
data["text"],
|
|
data["time"],
|
|
rq,
|
|
data["repeat_interval"],
|
|
data["weekdays"],
|
|
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
|
|
|
|
finally:
|
|
cursor.connection.isolation_level = ""
|
|
|
|
ReminderHandler().find_next_reminder(next_time)
|
|
return self.get()
|
|
|
|
def delete(self) -> None:
|
|
"""Delete the reminder
|
|
"""
|
|
LOGGER.info(f'Deleting reminder {self.id}')
|
|
get_db().execute("DELETE FROM reminders WHERE id = ?", (self.id,))
|
|
ReminderHandler().find_next_reminder()
|
|
return
|
|
|
|
class Reminders:
|
|
"""Represents the reminder library of the user account
|
|
"""
|
|
|
|
def __init__(self, user_id: int) -> None:
|
|
"""Create an instance.
|
|
|
|
Args:
|
|
user_id (int): The ID of the user.
|
|
"""
|
|
self.user_id = user_id
|
|
return
|
|
|
|
def fetchall(
|
|
self,
|
|
sort_by: SortingMethod = SortingMethod.TIME
|
|
) -> List[dict]:
|
|
"""Get all reminders
|
|
|
|
Args:
|
|
sort_by (SortingMethod, optional): How to sort the result.
|
|
Defaults to SortingMethod.TIME.
|
|
|
|
Returns:
|
|
List[dict]: The id, title, text, time and color of each reminder
|
|
"""
|
|
reminders = [
|
|
dict(r)
|
|
for r in get_db(dict).execute("""
|
|
SELECT
|
|
id,
|
|
title, text,
|
|
time,
|
|
repeat_quantity,
|
|
repeat_interval,
|
|
weekdays,
|
|
color
|
|
FROM reminders
|
|
WHERE user_id = ?;
|
|
""",
|
|
(self.user_id,)
|
|
)
|
|
]
|
|
for r in reminders:
|
|
r["weekdays"] = [
|
|
int(n)
|
|
for n in r["weekdays"].split(",")
|
|
if n
|
|
] if r["weekdays"] else None
|
|
|
|
# Sort result
|
|
reminders.sort(key=sort_by.value[0], reverse=sort_by.value[1])
|
|
|
|
return reminders
|
|
|
|
def search(
|
|
self,
|
|
query: str,
|
|
sort_by: SortingMethod = SortingMethod.TIME) -> List[dict]:
|
|
"""Search for reminders
|
|
|
|
Args:
|
|
query (str): The term to search for.
|
|
sort_by (SortingMethod, optional): How to sort the result.
|
|
Defaults to SortingMethod.TIME.
|
|
|
|
Returns:
|
|
List[dict]: All reminders that match. Similar output to self.fetchall
|
|
"""
|
|
reminders = [
|
|
r for r in self.fetchall(sort_by)
|
|
if search_filter(query, r)
|
|
]
|
|
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(self.user_id, id)
|
|
|
|
def add(
|
|
self,
|
|
title: str,
|
|
time: int,
|
|
notification_services: List[int],
|
|
text: str = '',
|
|
repeat_quantity: Union[None, RepeatQuantity] = None,
|
|
repeat_interval: Union[None, int] = None,
|
|
weekdays: Union[None, List[int]] = None,
|
|
color: Union[None, 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 (Union[None, RepeatQuantity], optional): The quantity
|
|
of the repeat specified for the reminder.
|
|
Defaults to None.
|
|
|
|
repeat_interval (Union[None, int], optional): The amount of repeat_quantity,
|
|
like "5" (hours).
|
|
Defaults to None.
|
|
|
|
weekdays (Union[None, List[int]], optional): The indexes of the days
|
|
of the week that the reminder should run.
|
|
Defaults to None.
|
|
|
|
color (Union[None, str], optional): The hex code of the color of the
|
|
reminder, which is shown in the web-ui.
|
|
Defaults to None.
|
|
|
|
Note about args:
|
|
Either repeat_quantity and repeat_interval are given,
|
|
weekdays is given or neither, but not both.
|
|
|
|
Raises:
|
|
NotificationServiceNotFound: One of the notification services was not found.
|
|
InvalidKeyValue: The value of one of the keys is not valid
|
|
or the "Note about args" is violated.
|
|
|
|
Returns:
|
|
dict: The info about the reminder.
|
|
"""
|
|
LOGGER.info(
|
|
f'Adding reminder with {title=}, {time=}, {notification_services=}, '
|
|
+ f'{text=}, {repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}'
|
|
)
|
|
|
|
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)
|
|
elif (
|
|
weekdays is not None
|
|
and repeat_quantity is not None
|
|
and repeat_interval is not None
|
|
):
|
|
raise InvalidKeyValue('weekdays', weekdays)
|
|
|
|
cursor = get_db()
|
|
for service in notification_services:
|
|
if not cursor.execute("""
|
|
SELECT 1
|
|
FROM notification_services
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
LIMIT 1;
|
|
""",
|
|
(service, self.user_id)
|
|
).fetchone():
|
|
raise NotificationServiceNotFound
|
|
|
|
# Prepare args
|
|
if any((repeat_quantity, weekdays)):
|
|
original_time = time
|
|
time = _find_next_time(
|
|
original_time,
|
|
repeat_quantity,
|
|
repeat_interval,
|
|
weekdays
|
|
)
|
|
else:
|
|
original_time = None
|
|
|
|
weekdays_str = when_not_none(
|
|
weekdays,
|
|
lambda w: ",".join(map(str, sorted(w)))
|
|
)
|
|
repeat_quantity_str = when_not_none(
|
|
repeat_quantity,
|
|
lambda q: q.value
|
|
)
|
|
|
|
cursor.connection.isolation_level = None
|
|
cursor.execute("BEGIN TRANSACTION;")
|
|
|
|
id = cursor.execute("""
|
|
INSERT INTO reminders(
|
|
user_id,
|
|
title, text,
|
|
time,
|
|
repeat_quantity, repeat_interval,
|
|
weekdays,
|
|
original_time,
|
|
color
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
|
""", (
|
|
self.user_id,
|
|
title, text,
|
|
time,
|
|
repeat_quantity_str,
|
|
repeat_interval,
|
|
weekdays_str,
|
|
original_time,
|
|
color
|
|
)).lastrowid
|
|
|
|
try:
|
|
cursor.executemany("""
|
|
INSERT INTO reminder_services(
|
|
reminder_id,
|
|
notification_service_id
|
|
)
|
|
VALUES (?, ?);
|
|
""",
|
|
((id, service) for service in notification_services)
|
|
)
|
|
cursor.execute("COMMIT;")
|
|
|
|
except IntegrityError:
|
|
raise NotificationServiceNotFound
|
|
|
|
finally:
|
|
cursor.connection.isolation_level = ''
|
|
|
|
ReminderHandler().find_next_reminder(time)
|
|
|
|
return self.fetchone(id)
|
|
|
|
def test_reminder(
|
|
self,
|
|
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 ''.
|
|
"""
|
|
LOGGER.info(f'Testing reminder with {title=}, {notification_services=}, {text=}')
|
|
a = Apprise()
|
|
cursor = get_db(dict)
|
|
|
|
for service in notification_services:
|
|
url = cursor.execute("""
|
|
SELECT url
|
|
FROM notification_services
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
LIMIT 1;
|
|
""",
|
|
(service, self.user_id)
|
|
).fetchone()
|
|
if not url:
|
|
raise NotificationServiceNotFound
|
|
a.add(url[0])
|
|
|
|
a.notify(title=title, body=text)
|
|
return
|
|
|
|
|
|
class ReminderHandler(metaclass=Singleton):
|
|
"""Handle set reminders.
|
|
|
|
Note: Singleton.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
context: Callable[[], AppContext]
|
|
) -> None:
|
|
"""Create instance of handler.
|
|
|
|
Args:
|
|
context (Optional[AppContext], optional): `Flask.app_context`.
|
|
Defaults to None.
|
|
"""
|
|
self.context = context
|
|
self.thread: Union[Timer, None] = None
|
|
self.time: Union[int, None] = 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)
|
|
reminders = [
|
|
dict(r)
|
|
for r in cursor.execute("""
|
|
SELECT
|
|
id, user_id,
|
|
title, text,
|
|
repeat_quantity, repeat_interval,
|
|
weekdays,
|
|
original_time
|
|
FROM reminders
|
|
WHERE time = ?;
|
|
""",
|
|
(time,)
|
|
)
|
|
]
|
|
|
|
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 reminder
|
|
a = Apprise()
|
|
for url in cursor:
|
|
a.add(url['url'])
|
|
a.notify(title=reminder["title"], body=reminder["text"])
|
|
|
|
self.thread = None
|
|
self.time = None
|
|
|
|
if (reminder['repeat_quantity'], reminder['weekdays']) == (None, None):
|
|
# Delete the reminder from the database
|
|
Reminder(reminder["user_id"], reminder["id"]).delete()
|
|
|
|
else:
|
|
# Set next time
|
|
new_time = _find_next_time(
|
|
reminder['original_time'],
|
|
when_not_none(
|
|
reminder["repeat_quantity"],
|
|
lambda q: RepeatQuantity(q)
|
|
),
|
|
reminder['repeat_interval'],
|
|
when_not_none(
|
|
reminder["weekdays"],
|
|
lambda w: [int(d) for d in w.split(',')]
|
|
)
|
|
)
|
|
cursor.execute(
|
|
"UPDATE reminders SET time = ? WHERE id = ?;",
|
|
(new_time, reminder['id'])
|
|
)
|
|
|
|
self.find_next_reminder()
|
|
return
|
|
|
|
def find_next_reminder(self, time: Optional[int] = None) -> None:
|
|
"""Determine when the soonest reminder is and set the timer to that time
|
|
|
|
Args:
|
|
time (Optional[int], optional): The timestamp to check for.
|
|
Otherwise check soonest in database.
|
|
Defaults to None.
|
|
"""
|
|
if time is None:
|
|
with self.context():
|
|
soonest_time: Union[Tuple[int], None] = 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 soonest_time is None:
|
|
return
|
|
time = soonest_time[0]
|
|
|
|
if (
|
|
self.thread is None
|
|
or time < self.time
|
|
):
|
|
if self.thread is not None:
|
|
self.thread.cancel()
|
|
|
|
t = time - datetime.utcnow().timestamp()
|
|
self.thread = Timer(
|
|
t,
|
|
self.__trigger_reminders,
|
|
(time,)
|
|
)
|
|
self.thread.name = "ReminderHandler"
|
|
self.thread.start()
|
|
self.time = time
|
|
|
|
return
|
|
|
|
def stop_handling(self) -> None:
|
|
"""Stop the timer if it's active
|
|
"""
|
|
if self.thread is not None:
|
|
self.thread.cancel()
|
|
return
|