*quietly adds a file that should have been in the tree since 9839a*
[halbot.git] / reminders.py
bloba516b01f8f75c1df97a671aae0bfa013bad69427
1 from __future__ import generators
2 import re, time, os
3 from sets import Set
4 from dateutil.parser import parse as raw_time_parser
6 import whois, perms
7 from common import get_timestamp, resize, extract_my_db, real_where, safeint, \
8 extract
9 from basic_commands import reply
10 from connection import hal
11 from globals import reminder_dbs, subscription_dbs, scheduler, private, \
12 commands, unlower, poke_via_msg, yes, no
13 from safety import safe
14 from ircbot import IRCDict, irc_lower
16 os.environ['TZ'] = "GMT"
17 time.tzset()
19 def parse_time(time_string):
20 return time.mktime(raw_time_parser(time_string).timetuple())
22 def get_reminder_db(who, where):
23 return extract_my_db(reminder_dbs, who, where, lambda: [])
25 def get_subscription_db(who, where):
26 return extract_my_db(subscription_dbs, who, where, IRCDict)
28 def describe_reminder(reminder):
29 (when, message, groups, repeat) = reminder
30 description = '"%s" at %s.' % (message, get_timestamp(when))
31 if groups:
32 groupstr = ",".join(groups)
33 description += " Groups: " + groupstr
34 if repeat:
35 hours = repeat / 3600
36 minutes = str(repeat / 60 % 60).zfill(2)
37 description += " Repeating every %d:%s." % (hours, minutes)
38 return description
40 reminder_res = {"add": re.compile(r'^"((?:[^"]|\")*)" +at +([^"]*)$',
41 re.IGNORECASE),
42 "set": re.compile(r'^(\d+) +(\w+) +(.*)$', re.IGNORECASE),
43 "repeat h+:mm": re.compile(r'^(\d+) +every +(\d+):(\d\d)$',
44 re.IGNORECASE),
45 "repeat x units": re.compile(r'^(\d+) +every +(\d+) +' +
46 r'(week|day|hour|minute)s?$',
47 re.IGNORECASE),
48 "repeat off": re.compile(r'^(\d+) +(off|disable|stop|never|none'
49 +r'|remove)$',
50 re.IGNORECASE)
53 time_units = {"minute": 60,
54 "hour": 60*60,
55 "day": 60*60*24,
56 "week": 60*60*24*7}
58 def reminder_index(who, where, reminders, whichstr):
59 which = safeint(whichstr)
60 if which < 1:
61 reply(who, where, "Which reminder?")
62 return -1
63 if which > len(reminders):
64 reply(who, where, "I don't have that many reminders.")
65 return -1
66 return which - 1
68 def fire_reminder(where, reminders, reminder):
69 (when, message, groups, repeat) = reminder
70 curtime = time.time()
71 if when < (curtime - 60):
72 delay = curtime - when
73 reply('', where, "Reminder delayed by %d minutes: %s" % (delay // 60,
74 message), all=True)
75 else:
76 delay = 0
77 reply('', where, message, all=True)
78 if where in hal.channels:
79 subscriptions = get_subscription_db("", where)
80 please_inform_us = Set()
81 for group in groups:
82 if group in subscriptions:
83 please_inform_us = please_inform_us.union(subscriptions[group])
84 informable = whois.voice_or_better(where)
85 inform_us = informable.intersection(please_inform_us)
86 poke_list = [unlower.get(who, who) for who in inform_us
87 if not poke_via_msg.get(who, False)]
88 if poke_list:
89 poke_str = ", ".join(poke_list)
90 reply("", where, poke_str + ": I'm in yer reminders, informin' you.",
91 all=True)
92 for who in inform_us:
93 if poke_via_msg.get(who, False):
94 reply(who, private, "(%s) %s" % (where, message))
95 if repeat:
96 if repeat < delay:
97 skip = delay // repeat
98 reply('', where, "(skipping %d delayed repititions)" % skip, all=True)
99 real_repeat = (skip + 1) * repeat
100 else:
101 real_repeat = repeat
102 reminder[0] += real_repeat
103 schedule_reminder(where, reminders, reminder)
104 else:
105 reminders.remove(reminder)
106 fire_reminder = safe(fire_reminder)
108 def schedule_reminder(where, reminders, reminder):
109 scheduler.enterabs(reminder[0], 0, fire_reminder, (where, reminders, reminder))
111 def add_reminder(who, where, reminders, reminder):
112 if len(reminders) < 10 or where == "#casualgameplay":
113 reminders.append(reminder)
114 schedule_reminder(real_where(who, where), reminders, reminder)
115 reply(who, where, "Reminder %d added." % len(reminders))
116 else:
117 reply(who, where,
118 "I'm sorry, %s, I'm afraid I can't do that. You're limited to 10."
119 % who)
121 def cancel_reminder(where, reminders, reminder):
122 try:
123 scheduler.cancel((reminder[0], 0, fire_reminder, (where, reminders, reminder)))
124 except ValueError:
125 pass
127 def reschedule(who, where, reminders, reminder, when):
128 rwhere = real_where(who, where)
129 cancel_reminder(rwhere, reminders, reminder)
130 reminder[0] = when
131 schedule_reminder(rwhere, reminders, reminder)
133 def reminder(who, where, reminder_args):
134 "$reminder is so complex it has its own help system: $reminder help"
135 (command, args) = resize(reminder_args.split(" ", 1), 2)
136 reminders = get_reminder_db(who, where)
137 if not command:
138 reply(who, where, "Available reminder commands: add del list set repeat help")
139 elif command in ("add", "new", "create", "make"):
140 parsed = reminder_res["add"].search(args)
141 if not parsed:
142 reply(who, where, "I don't understand that. Try $reminder help add")
143 return
144 (msg, whenstr) = parsed.groups()
145 try:
146 when = parse_time(whenstr)
147 except ValueError:
148 reply(who, where, "I don't understand that time.")
149 return
150 reminder = [when, msg, [], 0]
151 add_reminder(who, where, reminders, reminder)
152 elif command in ("del", "delete", "remove", "rm"):
153 which = reminder_index(who, where, reminders, args)
154 if which == -1:
155 return
156 reminder = reminders[which]
157 del reminders[which]
158 cancel_reminder(real_where(who, where), reminders, reminder)
159 reply(who, where, "Deleted reminder %d: %s" %
160 (which+1, describe_reminder(reminder)))
161 elif command in ("list", "show"):
162 if len(reminders) > 5:
163 reply(who, where, "Listing %d reminders in private " % len(reminders)
164 + "to avoid channel clutter")
165 where = private
166 if not reminders:
167 reply(who, where, "No reminders.")
168 index = 1
169 for reminder in reminders:
170 reply(who, where, "%2d. %s" % (index, describe_reminder(reminder)))
171 index += 1
172 elif command in ("change", "alter", "set"):
173 parsed = reminder_res["set"].search(args)
174 if not parsed:
175 reply(who, where, "I don't understand that. Try $reminder help set")
176 return
177 (whichstr, property, value) = parsed.groups()
178 which = reminder_index(who, where, reminders, whichstr)
179 if which == -1:
180 return
181 reminder = reminders[which]
182 lproperty = property.lower()
183 if lproperty in ("message", "msg"):
184 reminder[1] = value
185 elif lproperty == "time":
186 try:
187 when = parse_time(value)
188 reschedule(who, where, reminders, reminder, when)
189 except ValueError:
190 reply(who, where, "I don't understand that time.")
191 return
192 elif lproperty in ("group", "groups"):
193 groups = value.split(",")
194 groups = [group.strip() for group in groups]
195 reminder[2] = groups
196 reply(who, where, "Done.")
197 elif command == "repeat":
198 parse_hm = reminder_res["repeat h+:mm"].search(args)
199 parse_units = reminder_res["repeat x units"].search(args)
200 parse_off = reminder_res["repeat off"].search(args)
201 parsed = parse_hm or parse_units or parse_off
202 if parsed:
203 which = reminder_index(who, where, reminders, parsed.groups()[0])
204 if which == -1:
205 return
206 reminder = reminders[which]
207 else:
208 reply(who, where, "I don't understand that. Try $reminder help repeat")
209 return
210 if parse_hm:
211 (whichstr, hourstr, minutestr) = parsed.groups()
212 hours = safeint(hourstr)
213 if hours < 0:
214 reply(who, where, "Bad number of hours.")
215 return
216 minutes = safeint(minutestr)
217 if not (0 <= minutes <= 59):
218 reply(who, where, "Bad number of minutes.")
219 return
220 if hours == minutes == 0:
221 reply(who, where, "Repeating continuously sounds like a bad idea.")
222 return
223 reminder[3] = 60 * (minutes + 60 * hours)
224 reply(who, where,
225 "Reminder number %d now repeating every %d hours and %d minutes."
226 % (which+1, hours, minutes))
227 elif parse_units:
228 (whichstr, numstr, unit) = parsed.groups()
229 unit = unit.lower()
230 num = safeint(numstr)
231 if numstr < 1:
232 reply(who, where, "Bad number of %ss." % unit)
233 return
234 if unit not in time_units:
235 reply(who, where, "I don't know that unit.")
236 return
237 reminder[3] = num * time_units[unit]
238 reply(who, where, "Reminder number %d now repeating every %d %ss."
239 % (which+1, num, unit))
240 else: # parse_off
241 reminder[3] = 0
242 reply(who, where, "Repeating disabled on reminder %d." % which+1)
244 elif command == "help":
245 if not args:
246 reply(who, where, "To get help on a specific command, type $reminder " \
247 + "help <command> (e.g. $reminder help add). " \
248 + "Available reminder commands: add del list set "\
249 + "repeat help")
250 elif args in ("add", "new", "create", "make"):
251 reply(who, where, '$reminder add "<msg>" at <time>: Adds a new ' \
252 + "reminder. When <time> comes, I will say <msg>. A" \
253 + " variety of time formats are supported. Use $time" \
254 + " to get my current time for comparison.")
255 elif args in ("del", "delete", "remove", "rm"):
256 reply(who, where, "$reminder del <reminder>: Delete reminder number " \
257 + "<reminder>.");
258 elif args in ("change", "alter", "set"):
259 reply(who, where, "$reminder set <reminder> <property> <value>: Change" \
260 + " <property> of reminder number <reminder> to " \
261 + "<value>. Availale properties: message, time, group")
262 elif args in ("list", "show"):
263 reply(who, where, "$reminder list: Print a list of reminders. If there" \
264 + " are more than 5, I will reply in private.")
265 elif args == "repeat":
266 reply(who, where, "$reminder repeat <reminder> every <interval>: " \
267 + "Reminder number <reminder> will be repeated every " \
268 + "<interval>. Use $reminder repeat <reminder> off to"\
269 + " stop repeating a reminder.")
270 elif args == "help":
271 reply(who, where, "$reminder help <command>: get help on <command>. " \
272 + "You know, like you just did.")
273 else:
274 reply(who, where, "I don't understand that.")
275 commands['reminder'] = (perms.admin, reminder)
276 commands['reminders'] = (perms.admin, reminder)
278 def reminders_list(who, where, args):
279 "$reminders-list: Asks Hal for the current list of reminders."
280 reminder(who, where, "list")
281 commands['reminders-list'] = (perms.voice, reminders_list)
283 def subscribe(who, where, args):
284 "$subscribe <groups>: Subscribes you to the (comma-separated) list of reminder groups. Hal will try to get your attention when a reminder in one of those groups is triggered. Reminder groups are listed in $reminders-list"
285 if where == private:
286 reply(who, where, "Don't you think subscribing to reminders that are" +
287 " already in PM is a bit... pointless?")
288 return
290 unlower[irc_lower(who)] = who
291 subscriptions = get_subscription_db(who, where)
292 targets = [target.strip() for target in args.split(",")]
293 new_subscription = False
294 for target in targets:
295 subscribers = extract(subscriptions, target, Set)
296 user = irc_lower(who)
297 if user not in subscribers:
298 subscribers.add(user)
299 new_subscription = True
300 if new_subscription:
301 reply(who, where, "Done.")
302 else:
303 reply(who, where, "You're already subscribed.")
304 commands['subscribe'] = (perms.voice, subscribe)
306 def unsubscribe(who, where, args):
307 "$unsubscribe <groups>: Unsubscribes you from the (comma-separated) list of reminder groups."
308 subscriptions = get_subscription_db(who, where)
309 targets = [target.strip() for target in args.split(",")]
310 lost_subscription = False
311 for target in targets:
312 subscribers = extract(subscriptions, target, Set)
313 user = irc_lower(who)
314 if user in subscribers:
315 subscribers.discard(user)
316 lost_subscription = True
317 if lost_subscription:
318 reply(who, where, "Done.")
319 else:
320 reply(who, where, "You weren't subscribed.")
321 commands['unsubscribe'] = (perms.voice, unsubscribe)
323 def poke_msg(who, where, args):
324 "$poke-msg (on/off): Normally, Hal pokes you in the channel. If you turn $poke-msg on, he'll send you a /msg instead. Note that unlike most commands, this is global and will affect Hal's behavior in all channels."
325 args = args.lower()
326 state = poke_via_msg.get(who, False)
327 if args in yes:
328 if state:
329 reply(who, where, "Already sending pokes via /msg.")
330 else:
331 poke_via_msg[who] = True
332 reply(who, where, "Done.")
333 elif args in no:
334 if not state:
335 reply(who, where, "I wasn't sending pokes via /msg.")
336 else:
337 poke_via_msg[who] = False
338 reply(who, where, "Done.")
339 else:
340 reply(who, where, poke_msg.__doc__)
341 commands['poke-msg'] = (perms.voice, poke_msg)
343 def run_reminders():
344 for where in reminder_dbs.keys():
345 db = reminder_dbs[where]
346 for reminder in db:
347 schedule_reminder(where, db, reminder)
348 while True: # Looping in case of errors.
349 safe(scheduler.run_forever)()