-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfindappointment
executable file
·253 lines (228 loc) · 9.4 KB
/
findappointment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/usr/bin/env python3
# Copyright 2006-2024 Michael Cuffaro
#
# This file is part of mccal.
#
# mccal 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.
#
# mccal 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 mccal. If not, see <http://www.gnu.org/licenses/>.
import argparse
import os
import re
import subprocess
import signal
import sys
import time
from datetime import datetime
from pathlib import Path
def notify_admin(addr, subject, body):
print(f"{subject}: {body}", file=sys.stderr)
result = subprocess.run(
f'echo "{body}" | mail -s "{subject}" {addr}',
shell=True,
capture_output=True
)
if result.returncode != 0:
print(
"Unable to send an email to '{}': {}".format(addr, result.stderr.decode().strip('\n')),
file=sys.stderr
)
def main(adm_addr, calfilename, pausefile_name, sleep_secs, text_mode, mail_recipient, once_only,
dev_mode):
if not os.path.isfile(calfilename):
with open(calfilename, mode='w'):
notify_admin(adm_addr, "findappointment initialised a new calendar file",
"No action is required. This is for information only.")
# We begin by reading in each line of the calendar file, noting any that in particular
# are marked as PENDING or PROCESSED:
calfile_contents = []
pending_events = set()
processed_events = set()
directive_pattern = r"^((SNOOZE|PENDING|PROCESSED) )?ID:([0-9\.]+) "
try:
with open(calfilename) as calfile:
for i, next_line in enumerate(calfile):
next_line = next_line.strip('\n')
match = re.match(directive_pattern, next_line)
if not match:
print(f"Line {i} has invalid syntax: '{next_line}'", file=sys.stderr)
sys.exit(1)
match = match.groups()
directive, event_id = match[1], match[2]
calfile_contents.append(next_line)
if directive == "PENDING":
pending_events.add(event_id)
elif directive == "PROCESSED":
processed_events.add(event_id)
# If there are any pending events that have not been processed, tell the user about it via
# sys.stderr and by email:
incompletes = [
event_id for event_id in pending_events if event_id not in processed_events
]
if incompletes:
message = (
f"The following reminders were found to be incompletely processed:\n"
f"{', '.join(incompletes)}.\n"
"No action is required. These events will be reprocessed in due course."
)
print(message, file=sys.stderr)
notify_admin(adm_addr, "findappointment found incompletely processed events", message)
# Now write all the lines, other than the pending lines, back to the calendar file.
with open(calfilename, mode='w') as calfile:
for line in calfile_contents:
match = re.match(directive_pattern, line).groups()
directive, event_id = match[1], match[2]
if directive != "PENDING":
print(line, file=calfile)
except Exception as e:
notify_admin(adm_addr, "findappointment encountered an error", e)
sys.exit(1)
# Startup is complete. We now iterate every `sleep_secs` seconds:
iterated_once = False
while True:
try:
iterated_once = iterated_once or True
# If the pause file is present, then sleep and check again on the next iteration:
if os.path.isfile(pausefile_name):
time.sleep(sleep_secs)
continue
# Open the calendar file and get the current date:
calfile = open(calfilename)
curr_date = datetime.now()
reminder_queue = {}
for next_line in calfile:
next_line = next_line.strip('\n')
# If the line begins with a directive, extract it and then remove it from the line:
directive_pattern = r"^((SNOOZE|PENDING|PROCESSED) )?"
directive = re.match(directive_pattern, next_line)
directive = directive[2] if directive else None
next_line = re.sub(directive_pattern, "", next_line)
# Parse the rest of the line into four fields:
event_id, dom, tod, text = next_line.split(' ', maxsplit=3)
_, event_id = event_id.split(':')
# Possibly put the event into the reminder queue, depending on the current time:
year, month, day = dom.split('-')
hour, minute = tod.split(':')
appt_date = datetime(year=int(year), month=int(month), day=int(day), hour=int(hour),
minute=int(minute))
if curr_date >= appt_date:
# Mark it as a dev event if we are running in dev_mode:
if dev_mode and not text.endswith('[DEV]"'):
text = text.strip('"')
text = f'"{text} [DEV]"'
# If the directive indicates that this ID is pending or processed, remove it from the
# reminder queue:
if directive in ["PENDING", "PROCESSED"]:
reminder_queue.pop(f"{event_id}", None)
else:
# Otherwise add it to the reminder queue:
reminder_queue[f"{event_id}"] = f"{text}"
# Now that we have gone through the calendar and populated the reminder queue, it is time
# to notify the user:
for (event_id, message) in reminder_queue.items():
remind_prog = "remind -t" if text_mode else "remind"
remind_prog = f"{remind_prog} -m {mail_recipient}" if mail_recipient else remind_prog
remind_prog = f"./{remind_prog}" if dev_mode else f"{str(Path.home())}/bin/{remind_prog}"
command = f"{remind_prog} {calfile.name} {event_id} {message}"
if text_mode:
result = subprocess.run(command, shell=True, capture_output=True)
if result.returncode != 0:
raise Exception(result.stderr.decode().strip('\n'))
print(result.stdout.decode().strip('\n'))
else:
# If this is UI mode, run the remind command in the background:
command = f"{command} &"
result = subprocess.run(command, shell=True)
# This isn't the process's final exit code since it is now running in the background,
# but hopefully if there are problems even starting they will be reflected here:
if result.returncode != 0:
raise Exception(result.stderr.decode().strip('\n'))
# Close the calendar for this iteration:
calfile.close()
except Exception as e:
notify_admin(adm_addr, "findappointment encountered an error", e)
if once_only and iterated_once:
break
time.sleep(sleep_secs)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Simple reminder calendar -- findappointment")
default_adm_email = 'root'
parser.add_argument(
'--adm', metavar='ADDR',
help=(f"(defaults to '{default_adm_email}' if unspecified) the email address of the "
"administrator to send an email to in the case of an error. Note that, regardless of "
"this value, error messages will always also be written to STDERR")
)
default_calendar = f"{str(Path.home())}/.mycalendar.txt"
parser.add_argument(
'--calendar', metavar='FILE',
help=f"read from the calendar FILE instead of from '{default_calendar}'"
)
default_pause_on = f"{str(Path.home())}/.pause_mccal"
parser.add_argument(
'--pause_on', metavar='FILE',
help=(f"use the existence of FILE, instead of '{default_pause_on}', as an indication "
"to findappointment that it should postpone any user notifications until the next "
"iteration")
)
default_sleep = 300
parser.add_argument(
'--sleep', metavar='N',
help=(f"sleep for N seconds, instead of the default {default_sleep}, between "
"iterations"),
type=int
)
parser.add_argument(
'-t', '--text_mode',
help="write all reminder notifications to the console without any popups",
action='store_true'
)
parser.add_argument(
'-m', '--mail', metavar='ADDR',
help=("when a reminder becomes due, send an email using 'mail' to ADDR in addition "
"to the usual notification"),
)
parser.add_argument(
'--once',
help=('do not iterate; look once through the calendar file for reminders that are due, '
'generate notifications, and exit.'),
action='store_true'
)
parser.add_argument(
'--dev',
help='assume that all other mccal commands are located in the current working directory',
action='store_true'
)
args = parser.parse_args()
adm_email = args.adm or default_adm_email
# Set up a handler for various signals:
def notify_and_terminate(signum, _):
notify_admin(
adm_email,
"findappointment terminated",
f"findappointment received signal '{signal.strsignal(signum)}' and was terminated."
)
sys.exit(signum)
# Bind the handler to the desired signals:
signal.signal(signal.SIGHUP, notify_and_terminate)
signal.signal(signal.SIGINT, notify_and_terminate)
signal.signal(signal.SIGTERM, notify_and_terminate)
main(
adm_email,
args.calendar or default_calendar,
args.pause_on or default_pause_on,
args.sleep or default_sleep,
args.text_mode,
args.mail,
args.once,
args.dev
)