From b3efee4ea0c72615bcf95b722182e0894fa8971b Mon Sep 17 00:00:00 2001 From: sherl Date: Fri, 18 Apr 2025 23:50:45 +0200 Subject: [PATCH] chore: commit files --- .gitignore | 6 +++ list.example.toml | 30 ++++++++++++++ notify.py | 86 ++++++++++++++++++++++++++++++++++++++++ scripts/sample_script.py | 19 +++++++++ 4 files changed, 141 insertions(+) create mode 100644 .gitignore create mode 100644 list.example.toml create mode 100644 notify.py create mode 100644 scripts/sample_script.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..797075d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# User configuration +list.toml + +# Python garbage +__pycache__ +scripts/__pycache__ \ No newline at end of file diff --git a/list.example.toml b/list.example.toml new file mode 100644 index 0000000..d2a29f3 --- /dev/null +++ b/list.example.toml @@ -0,0 +1,30 @@ + +preferences = [ + + # Bob prefers to be notified about Alice's birthday + # with SMS, while Alice prefers XMPP. + {user="bob@localhost", channels=[2]}, + # Moreover, Alice prefers to be notified a week and a day before + # in addition to the standard notification on the day of the birthday. + {user="alice@localhost", channels=[1], "additional_reminders"=[1, 7]} + +] + +birthdays = [ + + # Alice will get notified about Bob's birthday, and Bob about Alice's. + {name="Bob", date="2000-01-01", to_notify=["alice@localhost"]}, + {name="Alice", date="2000-01-23", to_notify=["bob@localhost"]} + +] + +actions = [ + + # Predefined, trusted scripts to run. + # Scripts are going to be searched for inside of the "scripts" directory. + {id=0, name="print information", name_of_script="sample_script.py", startup_function="print_v1_data"}, + # These are not implemented! + {id=1, name="send XMPP message", name_of_script="dummy.py", startup_function="send_message"}, + {id=2, name="send SMS", name_of_script="dummy.py", startup_function="send_sms"} + +] \ No newline at end of file diff --git a/notify.py b/notify.py new file mode 100644 index 0000000..a2da3dd --- /dev/null +++ b/notify.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# stolat, cron-based birthday notifier +# Please run once per day. +# License: GPLv3 or later +import toml, os, importlib +from datetime import datetime, timedelta + +scripts = [] + +def get_current_path() -> str: + return os.path.dirname(os.path.abspath(__file__)) + +def load_config() -> dict: + + # Try to get configuration from current_path/list.toml + try: + path = get_current_path() + config = toml.load(path + "/list.toml") + except: + print("Sorry! It seems like I can't access list.toml. Is it in the script's working directory?") + quit(-1) + + return config + +def calc_age(birth_date: str) -> int: + date_as_datetime = datetime.strptime(birth_date, "%Y-%m-%d") + delta = datetime.today() - date_as_datetime + return round(delta.days / 365.25) + +def notify_user(birthday: dict, user: dict, actions: dict, days_till_birthday: int): + # print(f"Got {birthday}, {user} and actions {actions}.") + for channel in user['channels']: + + for notify_action in actions: + + if notify_action['id'] == channel: + + # User wants to receive a notification through current notify_action + try: + module = importlib.import_module("scripts." + notify_action['name_of_script'][:notify_action['name_of_script'].rfind(".py")]) + func = getattr(module, notify_action['startup_function']) + func("v1", user['user'], birthday['name'], calc_age(birthday['date']), days_till_birthday, birthday['date']) # v1-formatted function call + except Exception as e: + print(f"Error: failed executing {notify_action['startup_function']}() for {user['user']} ({birthday['name']}'s {calc_age(birthday['date'])}th birthday) :(\n" + f"Exception: {e}.") + +def iterate_birthdays(config: dict): + + badly_formatted_birthdays = 0 + today = datetime.today() + for birthday in config['birthdays']: + + try: + name = birthday['name'] + birthdate_as_str = birthday['date'] + birthdate = datetime.strptime(birthdate_as_str, "%Y-%m-%d") + to_notify = birthday['to_notify'] + except Exception as e: + badly_formatted_birthdays += 1 + continue + + for user in config['preferences']: + + birthdate_this_year = birthdate.replace(year=today.year) + + # Safeguard against no additional reminders + if not user['additional_reminders']: + user['additional_reminders'] = [] + + # Consider the birthday as additional reminder + # This way we check the list once. + if (birthdate_this_year - today).days + 1 in user['additional_reminders']: + notify_user(birthday, user, config['actions'], (birthdate_this_year - today).days + 1) + + if badly_formatted_birthdays > 0: + print(f"Warning: found {badly_formatted_birthdays} incorrectly formatted birth dates.\n" + f" They haven't been checked for possible birthdays. Please use ISO 8601.") + + + +def main(): + config = load_config() + iterate_birthdays(config) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/sample_script.py b/scripts/sample_script.py new file mode 100644 index 0000000..e564d76 --- /dev/null +++ b/scripts/sample_script.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +# Implement your functions here... + +def print_v1_data(*args): + + if args[0] == "v1": + string = (f"Called with API version {args[0]}\n" + f"to send to user {args[1]}\n" + f"about {args[2]}\n" + f"who turns {args[3]}\n" + f"in {args[4]} days\n" + f"with birthday being {args[5]}.\n") + print(string) + else: + print("Called with a wrong API version!") + +if __name__ == "__main__": + print("This script isn't meant be ran interactively!") \ No newline at end of file