#!/usr/bin/env python3 # Copyright 2023 Florian Fischer # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . """Notify the responsible people about occuring events via signal messages""" import argparse import datetime import pathlib import os import sys import typing as t import subprocess import icalevents from icalevents.icalevents import events RESPONSIBILITIES_ENV_VAR_NAME = 'SEN_RESPONSIBILITIES' def load_events(uri: str, start: datetime.datetime, end: datetime.datetime) -> list[icalevents.icalparser.Event]: """Retrieve all events from the specified ics calendars""" evs = [] if uri.startswith("http"): evs += events(url=uri, start=start, end=end) elif pathlib.Path(uri).is_file(): evs += events(file=uri, start=start, end=end) return evs def load_responsibilities(start: datetime.date, resp_file_path='', offset=0) -> list[str]: """Load resposibility definitions If no file path containing responsibilities is passed se tyhe environment. The environment variable 'SEN_RESPONSIBILITIES' is used to pass a line separated list of responsibilities per calendar week.""" all_resp = '' if resp_file_path and pathlib.Path(resp_file_path).is_file(): with open(resp_file_path, 'r', encoding='utf-8') as resp_file: all_resp = resp_file.read() all_resp = all_resp or os.environ.get(RESPONSIBILITIES_ENV_VAR_NAME, '') if not all_resp: print(f'{RESPONSIBILITIES_ENV_VAR_NAME} not set', file=sys.stderr) sys.exit(1) resps_order = all_resp.splitlines() cal_week = start.isocalendar()[1] if len(resps_order) == 52: resp = resps_order[cal_week - 1] else: resp = resps_order[((cal_week + offset) % len(resps_order)) - 1] return resp.split(',') def fmt_mentions( responsibilities: t.Optional[list[str]] = None) -> t.Tuple[str, str]: """Return a string of mentions and the required cli arguments for signal-cli""" if not responsibilities: return '', '' mention_notification_prfx = '' mention_arg = '' for resp in responsibilities: tokens = resp.split(':') if len(tokens) > 1: name = tokens[0] num = tokens[1] else: name = num = tokens[0] mention_arg += f' {len(mention_notification_prfx)}:{len(name)}:{num}' mention_notification_prfx += f', {name}' return mention_notification_prfx[2:], f'--mention {mention_arg[1:]}' def notify(notification: str, signal_user: str, recipients: t.Optional[list[str]] = None, group: t.Optional[str] = None): '''notify a group or a list of recipients about events''' if not recipients and not group: return mentions, mentions_arg = fmt_mentions(responsibilities=recipients) if mentions: notification = f'{mentions} {notification}' cmd = f'signal-cli -u {signal_user} send --message-from-stdin {mentions_arg}' if group: cmd += f' -g {group}' subprocess.run(cmd.split(), input=notification.encode(), check=True) def main(): """Programm entry point loading data from webuntis and printing the remaining lessons""" parser = argparse.ArgumentParser() parser.add_argument('--date', help='The date to notify for') parser.add_argument('--calendar', help='The calendars URI') parser.add_argument('--responsibilities-file', help='A file specifying the responsibilities per line') parser.add_argument('--responsibilities-offset', default=0, type=int, help='Offset in the responsibilities file') parser.add_argument('--group', help='The group notifications should be send to') parser.add_argument('user', help='The signal user sending messages') args = parser.parse_args() if args.date: tomorrow = datetime.date.fromisoformat(args.date) else: tomorrow = datetime.date.today() + datetime.timedelta(days=1) end = tomorrow + datetime.timedelta(days=1) recipients = load_responsibilities( tomorrow, resp_file_path=args.responsibilities_file, offset=args.responsibilities_offset) # load ICS events evs = load_events(args.calendar, tomorrow, end) if not evs: return notification = '' for event in evs: if event.start.date() != tomorrow: continue notification += f' {event.summary}' if notification: notify(notification, args.user, recipients, args.group) if __name__ == '__main__': main()