2 import html
, re
, irclib
, ircbot
, thread
, sys
, atexit
, bz2
, logging
, traceback
, time
, sched
, os
3 from dateutil
.parser
import parse
as raw_time_parser
4 from cPickle
import dumps
as pickle
, loads
as depickle
9 hal
= ircbot
.SingleServerIRCBot([("localhost", 6667)], nick
, "Halbot")
10 hal
.connect("localhost", 6667, nick
)
12 hal
.connection
.join("#casualgameplay")
13 hal
.connection
.privmsg("NickServ", "identify Orqv)>y!")
14 is_re
= re
.compile(" (is|are|was|were)( |$)", re
.IGNORECASE
)
15 nick_re
= re
.compile(nick
+ "[:,] ", re
.IGNORECASE
)
17 scheduler
= sched
.scheduler(time
.time
, time
.sleep
)
18 os
.environ
['TZ'] = "GMT"
22 my_users
= ircbot
.IRCDict()
23 factoid_dbs
= ircbot
.IRCDict()
24 locked_dbs
= ircbot
.IRCDict()
25 reminder_dbs
= ircbot
.IRCDict()
29 pickled
= bz2
.decompress(zipped
)
30 (version
, data
) = depickle(pickled
)
31 if version
== SAVE_VERSION
:
32 (my_users
, factoid_dbs
, locked_dbs
, reminder_dbs
) = data
34 (my_users
, factoid_dbs
, locked_dbs
) = data
36 sys
.exit("Unrecognized database version.")
38 del db
, zipped
, pickled
42 logging
.getLogger().addHandler(logging
.FileHandler("error.log"))
45 def __init__(self
, prefix
=""):
47 def write(self
, unbuffered
):
48 self
.data
+= unbuffered
50 def get_timestamp(when
=None):
53 return time
.ctime(when
) + " " + time
.tzname
[time
.daylight
]
55 def parse_time(time_string
):
56 return time
.mktime(raw_time_parser(time_string
).timetuple())
58 def safe_call(func
, args
):
62 if isinstance(e
, SystemExit):
64 buffer = Buffer("Exception in function %s at %s:\n"
65 % (func
.__name
__, get_timestamp()))
66 traceback
.print_exc(file=buffer)
67 logging
.getLogger().error(buffer.data
)
68 # Try to report the error interactively.
69 if len(args
) >= 2 and type(args
[0]) == type(args
[1]) == str:
71 reply(args
[0], args
[1], "Ow! Ohh, man, that didn't feel good. " \
72 +"Somebody get FunnyMan, quick!")
75 elif len(args
) and type(args
[0]) == str:
77 reply('', args
[0], "Ow! Ohh, man, that didn't feel good. " \
78 +"Somebody get FunnyMan, quick!", all
=True)
83 return lambda *args
: safe_call(func
, args
)
85 def resize(list_or_tuple
, length
):
86 '''Creates a new list or tuple of length 'length', using as many elements from
87 list_or_tuple as possible and right-padding with "". Does not modify
89 if len(list_or_tuple
) < length
:
90 if type(list_or_tuple
) == list:
91 return list_or_tuple
+ [""] * (length
- len(list_or_tuple
))
93 return list_or_tuple
+ ("",) * (length
- len(list_or_tuple
))
95 return list_or_tuple
[:length
]
103 self
.channel_perms
= {}
107 def __init__(self
, level
, symbol
=None):
111 Perms
.symbols
[symbol
] = self
112 def __cmp__(self
, other
):
113 return self
.level
- other
.level
118 return str(self
.level
)
120 def from_symbol(symbol
):
121 return Perms
.symbols
.get(symbol
, Perms
.present
)
122 from_symbol
= staticmethod(from_symbol
)
123 Perms
.ban
= Perms(-2)
124 Perms
.none
= Perms(-1)
125 Perms
.present
= Perms(0)
126 Perms
.voice
= Perms(1, "+")
127 Perms
.hop
= Perms(2, "%")
128 Perms
.op
= Perms(3, "@")
129 Perms
.admin
= Perms(4, "&")
130 Perms
.owner
= Perms(5, "~")
131 Perms
.ircop
= Perms(6)
133 waiting_commands
= {}
136 def got_command(who
, where
, command
, args
=""):
137 if not command
in commands
:
138 command
= 'raise error'
139 args
= "I don't understand that."
140 waiting
= waiting_commands
.get(who
, [])
142 (required_perms
, do_it
) = commands
[command
]
143 if where
== private
and required_perms
<= Perms
.owner
:
144 safe_call(do_it
, (who
, where
, args
))
145 elif required_perms
<= Perms
.voice
and where
in hal
.channels
:
146 chan
= hal
.channels
[where
]
147 if who
in chan
.voiceddict
or who
in chan
.hopdict
or who
in chan
.operdict \
148 or who
in chan
.admindict
or who
in chan
.ownerdict
:
149 safe_call(do_it
, (who
, where
, args
))
153 waiting_commands
[who
] = [(where
, command
, args
)]
154 hal
.connection
.whois((who
,))
156 waiting
.append((where
, command
, args
))
158 def reply(who
, where
, what
, all
=False):
160 hal
.connection
.privmsg(who
, what
)
162 hal
.connection
.privmsg(where
, what
)
164 hal
.connection
.privmsg(where
, "%s: %s" % (who
, what
))
166 def get_perms(who
, where
):
167 user
= whois_info
[who
]
171 server_perm
= Perms
.owner
173 server_perm
= whois_info
[who
].channel_perms
.get(where
, Perms
.none
)
175 my_perm
= my_users
.get(who
, Perms
.none
)
178 return max(server_perm
, my_perm
)
182 commands
['raise error'] = (Perms
.voice
, reply
)
184 def ping(who
, where
, args
):
185 reply(who
, where
, "Pong!")
186 commands
['ping'] = (Perms
.voice
, ping
)
188 class TestException(Exception): pass
190 def error(who
, where
, args
):
191 raise TestException
, str(args
)
192 commands
['error'] = (Perms
.ircop
, error
)
194 def join(who
, where
, args
):
195 hal
.connection
.join(where
)
196 commands
['do join'] = (Perms
.op
, join
)
198 def save(who
=None, where
=None, args
=None):
201 (my_users
, factoid_dbs
, locked_dbs
, reminder_dbs
)
203 zipped
= bz2
.compress(pickled
)
204 db
= open("hal.db", "w")
208 reply(who
, where
, "Database saved.")
209 commands
['save'] = (Perms
.ircop
, save
)
210 atexit
.register(save
)
212 def shutdown(who
, where
, args
):
213 hal
.connection
.quit("Daisy, daaaisy...")
215 commands
['shutdown'] = (Perms
.ircop
, shutdown
)
217 def title(who
, where
, args
, all
=False):
219 reply(who
, where
, "[%s]" % (html
.get_title(args
)), all
)
222 reply(who
, where
, "Error retrieving URL '%s'." % args
, all
)
223 commands
['title'] = (Perms
.voice
, title
)
225 def title_implicit(who
, where
, args
):
226 title(who
, where
, args
, all
=True)
227 commands
['title implicit'] = (Perms
.voice
, title_implicit
)
229 def google(who
, where
, args
):
231 url
= html
.first_google(args
)
232 reply(who
, where
, "Google says: %s [%s]." % (url
, html
.get_title(url
)), all
=True)
235 reply(who
, where
, "Ewwww... Google barfed on me.", all
=True)
236 commands
['google'] = (Perms
.voice
, google
)
238 def jig(who
, where
, args
):
239 google(who
, where
, args
+ " site:jayisgames.com")
240 commands
['jig'] = (Perms
.voice
, jig
)
242 def wp(who
, where
, args
):
243 google(who
, where
, args
+ " site:en.wikipedia.org")
244 commands
['wp'] = (Perms
.voice
, wp
)
245 commands
['wikipedia'] = (Perms
.voice
, wp
)
247 def extract_my_db(dbs
, who
, where
, default_maker
):
253 default
= default_maker()
257 def get_fact_dbs(who
, where
):
258 factoids
= extract_my_db(factoid_dbs
, who
, where
, ircbot
.IRCDict
)
259 locked
= extract_my_db(locked_dbs
, who
, where
, ircbot
.IRCDict
)
260 return (factoids
, locked
)
262 def do_is(who
, where
, args
):
263 (key
, to_be
, garbage
, fact
) = args
264 (factoids
, locked
) = get_fact_dbs(who
, where
)
265 if key
.endswith('?'):
268 reply(who
, where
, "I'm sorry, %s, I'm afraid I can't do that." % who
, all
=True)
271 factoids
[key
] = (to_be
, fact
)
272 elif key
in factoids
:
274 commands
['do is'] = (Perms
.voice
, do_is
)
276 def forget(who
, where
, args
):
277 (factoids
, locked
) = get_fact_dbs(who
, where
)
280 reply(who
, where
, "I'm sorry, %s, I'm afraid I can't do that." % who
, all
=True)
281 elif key
in factoids
:
283 reply(who
, where
, "I forgot %s." % key
)
285 reply(who
, where
, "I don't have anything matching '%s'." % key
)
286 commands
['forget'] = (Perms
.voice
, forget
)
288 def force(who
, where
, args
):
289 (factoids
, locked
) = get_fact_dbs(who
, where
)
290 if not is_re
.search(args
):
291 reply(who
, where
, "Syntax: $force a (is/are/was/were) b")
293 (key
, to_be
, garbage
, fact
) = resize(is_re
.split(args
, 1),4)
294 if key
.endswith('?'):
297 factoids
[key
] = (to_be
, fact
)
298 elif key
in factoids
:
300 commands
['force'] = (Perms
.op
, force
)
302 def lock(who
, where
, args
):
303 (factoids
, locked
) = get_fact_dbs(who
, where
)
304 if args
.endswith('?'):
306 if not locked
.get(args
):
308 reply(who
, where
, "Done.")
310 reply(who
, where
, "It's already locked.")
311 commands
['lock'] = (Perms
.op
, lock
)
313 def unlock(who
, where
, args
):
314 (factoids
, locked
) = get_fact_dbs(who
, where
)
315 if args
.endswith('?'):
319 reply(who
, where
, "Done.")
321 reply(who
, where
, "It's not locked.")
322 commands
['unlock'] = (Perms
.op
, unlock
)
324 def factoid(who
, where
, args
, implicit
=False):
325 (factoids
, locked
) = get_fact_dbs(who
, where
)
326 if args
.endswith('?'):
329 (to_be
, fact
) = factoids
[args
]
330 if not implicit
and fact
.count("$who"):
334 if lfact
.startswith("<"):
335 if lfact
.startswith("<raw>"):
336 response
= fact
[5:].lstrip()
337 elif lfact
.startswith("<reply>"):
338 response
= fact
[7:].lstrip()
339 elif lfact
.startswith("<action>"):
340 response
= "\x01ACTION %s\x01" % fact
[8:].lstrip()
342 response
= "I hear that %s %s %s" % (args
, to_be
, fact
)
343 response
= response
.replace("$who", who
)
344 reply(who
, where
, response
, all
=implicit
)
346 reply(who
, where
, "I don't have anything matching '%s'." % args
)
347 commands
['fact'] = (Perms
.voice
, factoid
)
348 commands
['factoid'] = (Perms
.voice
, factoid
)
350 def factoid_implicit(who
, where
, args
):
351 factoid(who
, where
, args
, implicit
=True)
352 commands
['factoid implicit'] = (Perms
.voice
, factoid_implicit
)
354 def get_time(who
, where
, args
):
355 reply(who
, where
, "Current time is: " + get_timestamp())
356 commands
['time'] = (Perms
.voice
, get_time
)
358 def get_reminder_db(who
, where
):
359 return extract_my_db(reminder_dbs
, who
, where
, lambda: [])
361 def describe_reminder(reminder
):
362 (when
, message
, groups
, repeat
) = reminder
363 description
= '"%s" at %s.' % (message
, get_timestamp(when
))
365 groupstr
= ",".join(groups
)
366 description
+= " Groups: " + groupstr
368 hours
= repeat
/ 3600
369 minutes
= str(repeat
/ 60 % 60).zfill(2)
370 description
+= " Repeating every %d:%s." % (hours
, minutes
)
373 reminder_res
= {"add": re
.compile(r
'^"((?:[^"]|\")*)" +at +([^"]*)$',
375 "set": re
.compile(r
'^(\d+) +(\w+) +(.*)$', re
.IGNORECASE
),
376 "repeat h+:mm": re
.compile(r
'^(\d+) +every +(\d+):(\d\d)$',
378 "repeat x units": re
.compile(r
'^(\d+) +every +(\d+) +' +
379 r
'(week|day|hour|minute)s?$',
381 "repeat off": re
.compile(r
'^(\d+) +(off|disable|stop|never|none'
386 time_units
= {"minute": 60,
397 def reminder_index(who
, where
, reminders
, whichstr
):
398 which
= safeint(whichstr
)
400 reply(who
, where
, "Which reminder?")
402 if which
> len(reminders
):
403 reply(who
, where
, "I don't have that many reminders.")
407 class workaround(object):
410 def real_where(who
, where
):
415 def fire_reminder(where
, reminders
, reminder
):
416 (when
, message
, groups
, repeat
) = reminder
417 curtime
= time
.time()
418 if when
< (curtime
- 60):
419 delay
= curtime
- when
420 reply('', where
, "Reminder delayed by %d minutes: %s" % (delay
// 60,
424 reply('', where
, message
, all
=True)
427 skip
= delay
// repeat
428 reply('', where
, "(skipping %d delayed repititions)" % skip
, all
=True)
429 real_repeat
= (skip
+ 1) * repeat
432 reminder
[0] += real_repeat
433 workaround
.schedule_reminder(where
, reminders
, reminder
)
435 reminders
.remove(reminder
)
436 fire_reminder
= safe(fire_reminder
)
438 def schedule_reminder(where
, reminders
, reminder
):
439 scheduler
.enterabs(reminder
[0], 0, fire_reminder
, (where
, reminders
, reminder
))
440 workaround
.schedule_reminder
= staticmethod(schedule_reminder
)
442 def add_reminder(who
, where
, reminders
, reminder
):
443 if len(reminders
) < 10 or where
== "#casualgameplay":
444 reminders
.append(reminder
)
445 schedule_reminder(real_where(who
, where
), reminders
, reminder
)
446 reply(who
, where
, "Done.")
449 "I'm sorry, %s, I'm afraid I can't do that. You're limited to 10."
452 def cancel_reminder(where
, reminders
, reminder
):
454 scheduler
.cancel((reminder
[0], 0, fire_reminder
, (where
, reminders
, reminder
)))
458 def reschedule(who
, where
, reminders
, reminder
, when
):
459 rwhere
= real_where(who
, where
)
460 cancel_reminder(rwhere
, reminders
, reminder
)
462 schedule_reminder(rwhere
, reminders
, reminder
)
464 def reminder(who
, where
, reminder_args
):
465 (command
, args
) = resize(reminder_args
.split(" ", 1), 2)
466 reminders
= get_reminder_db(who
, where
)
468 reply(who
, where
, "Available reminder commands: add del list set repeat help")
469 elif command
in ("add", "new", "create", "make"):
470 parsed
= reminder_res
["add"].search(args
)
472 reply(who
, where
, "I don't understand that. Try $reminder help add")
474 (msg
, whenstr
) = parsed
.groups()
476 when
= parse_time(whenstr
)
478 reply(who
, where
, "I don't understand that time.")
480 reminder
= [when
, msg
, [], 0]
481 add_reminder(who
, where
, reminders
, reminder
)
482 elif command
in ("del", "delete", "remove", "rm"):
483 which
= reminder_index(who
, where
, reminders
, args
)
486 reminder
= reminders
[which
]
488 cancel_reminder(real_where(who
, where
), reminders
, reminder
)
489 reply(who
, where
, "Deleted reminder %d: %s" %
490 (which
+1, describe_reminder(reminder
)))
491 elif command
in ("list", "show"):
492 if len(reminders
) > 5:
493 reply(who
, where
, "Listing %d reminders in private " % len(reminders
)
494 + "to avoid channel clutter")
497 reply(who
, where
, "No reminders.")
499 for reminder
in reminders
:
500 reply(who
, where
, "%2d. %s" % (index
, describe_reminder(reminder
)))
502 elif command
in ("change", "alter", "set"):
503 parsed
= reminder_res
["set"].search(args
)
505 reply(who
, where
, "I don't understand that. Try $reminder help set")
507 (whichstr
, property, value
) = parsed
.groups()
508 which
= reminder_index(who
, where
, reminders
, whichstr
)
511 reminder
= reminders
[which
]
512 lproperty
= property.lower()
513 if lproperty
in ("message", "msg"):
515 elif lproperty
== "time":
517 when
= parse_time(value
)
518 reschedule(who
, where
, reminders
, reminder
, when
)
520 reply(who
, where
, "I don't understand that time.")
522 elif lproperty
in ("group", "groups"):
523 groups
= value
.split(",")
524 groups
= [group
.strip() for group
in groups
]
526 reply(who
, where
, "Done.")
527 elif command
== "repeat":
528 parse_hm
= reminder_res
["repeat h+:mm"].search(args
)
529 parse_units
= reminder_res
["repeat x units"].search(args
)
530 parse_off
= reminder_res
["repeat off"].search(args
)
531 parsed
= parse_hm
or parse_units
or parse_off
533 which
= reminder_index(who
, where
, reminders
, parsed
.groups()[0])
536 reminder
= reminders
[which
]
538 reply(who
, where
, "I don't understand that. Try $reminder help repeat")
541 (whichstr
, hourstr
, minutestr
) = parsed
.groups()
542 hours
= safeint(hourstr
)
544 reply(who
, where
, "Bad number of hours.")
546 minutes
= safeint(minutestr
)
547 # Mathematicians, read at your own peril.
549 reply(who
, where
, "Bad number of minutes.")
551 if hours
== minutes
== 0:
552 reply(who
, where
, "Repeating continuously sounds like a bad idea.")
554 reminder
[3] = 60 * (minutes
+ 60 * hours
)
556 "Reminder number %d now repeating every %d hours and %d minutes."
557 % (which
+1, hours
, minutes
))
559 (whichstr
, numstr
, unit
) = parsed
.groups()
561 num
= safeint(numstr
)
563 reply(who
, where
, "Bad number of %ss." % unit
)
565 if unit
not in time_units
:
566 reply(who
, where
, "I don't know that unit.")
568 reminder
[3] = num
* time_units
[unit
]
569 reply(who
, where
, "Reminder number %d now repeating every %d %ss."
570 % (which
+1, num
, unit
))
573 reply(who
, where
, "Repeating disabled on reminder %d." % which
+1)
575 elif command
== "help":
577 reply(who
, where
, "To get help on a specific command, type $reminder " \
578 + "help <command> (e.g. $reminder help add). " \
579 + "Available reminder commands: add del list set "\
581 elif args
in ("add", "new", "create", "make"):
582 reply(who
, where
, '$reminder add "<msg>" at <time>: Adds a new ' \
583 + "reminder. When <time> comes, I will say <msg>. A" \
584 + " variety of time formats are supported. Use $time" \
585 + " to get my current time for comparison.")
586 elif args
in ("del", "delete", "remove", "rm"):
587 reply(who
, where
, "$reminder del <reminder>: Delete reminder number " \
589 elif args
in ("change", "alter", "set"):
590 reply(who
, where
, "$reminder set <reminder> <property> <value>: Change" \
591 + " <property> of reminder number <reminder> to " \
592 + "<value>. Availale properties: message, time, group")
593 elif args
in ("list", "show"):
594 reply(who
, where
, "$reminder list: Print a list of reminders. If there" \
595 + " are more than 5, I will reply in private.")
596 elif args
== "repeat":
597 reply(who
, where
, "$reminder repeat <reminder> every <interval>: " \
598 + "Reminder number <reminder> will be repeated every " \
599 + "<interval>. Use $reminder repeat <reminder> off to"\
600 + " stop repeating a reminder.")
602 reply(who
, where
, "$reminder help <command>: get help on <command>. " \
603 + "You know, like you just did.")
605 reply(who
, where
, "I don't understand that.")
606 commands
['reminder'] = (Perms
.admin
, reminder
)
607 commands
['reminders'] = (Perms
.admin
, reminder
)
609 def do_commands(who
):
610 user
= whois_info
[who
]
611 for where
, command
, args
in waiting_commands
[who
]:
612 if command
not in commands
:
613 command
= 'raise error'
614 args
= "I don't understand that."
615 (required_perms
, do_it
) = commands
[command
]
616 if required_perms
<= get_perms(who
, where
):
617 safe_call(do_it
, (who
, where
, args
))
618 waiting_commands
[who
] = []
621 def got_whois(conn
, event
):
622 user
= event
.arguments()[0]
623 whois_info
[user
] = Whois()
624 got_whois
= safe(got_whois
)
625 irc
.add_global_handler("whoisuser", got_whois
)
626 irc
.add_global_handler("nosuchnick", got_whois
)
629 def got_registered(conn
, event
):
630 args
= event
.arguments()
631 if len(args
) == 2 and args
[1] == "is a registered nick":
633 whois_info
[user
].registered
= True
634 got_registered
= safe(got_registered
)
635 irc
.add_global_handler("307", got_registered
)
638 def got_channels(conn
, event
):
639 args
= event
.arguments()
642 channels
= args
[1].split()
645 whois_info
[user
].channel_perms
[c
[1:]] = Perms
.from_symbol(c
[0])
647 whois_info
[user
].channel_perms
[c
] = Perms
.present
648 got_channels
= safe(got_channels
)
649 irc
.add_global_handler("whoischannels", got_channels
)
652 def got_servop(conn
, event
):
653 args
= event
.arguments()
656 whois_info
[user
].ircop
= True
657 got_servop
= safe(got_servop
)
658 irc
.add_global_handler("whoisoperator", got_servop
)
661 def got_whoend(conn
, event
):
662 who
= event
.arguments()[0]
665 user
= whois_info
[who
]
666 print "Who info for %s:" % who
667 print "Registered: %s" % user
.registered
668 print "IRCop: %s" % user
.ircop
669 p
= user
.channel_perms
670 perms
= "".join([("%s: %s, " % (k
, p
[k
])) for k
in p
])[:-2]
671 print "Channel info: %s" % perms
672 got_whoend
= safe(got_whoend
)
673 irc
.add_global_handler("endofwhois", got_whoend
)
676 def got_invite(conn
, event
):
677 who
= irclib
.nm_to_n(event
.source())
678 where
= event
.arguments()[0]
679 got_command(who
, where
, "do join")
680 got_invite
= safe(got_invite
)
681 irc
.add_global_handler("invite", got_invite
)
684 def got_msg(conn
, event
):
685 msg
= event
.arguments()[0]
686 who
= irclib
.nm_to_n(event
.source())
687 (command
, args
) = resize(msg
.split(" ", 1),2)
688 if command
and command
[0] == "$":
689 command
= command
[1:]
691 if event
.eventtype() == 'privmsg':
694 where
= event
.target()
695 if msg
.count("http://"):
696 got_command(who
, where
, "title implicit", html
.extract_url(msg
))
698 if nick_re
.match(msg
):
699 (me
, command
, args
) = resize(msg
.split(" ", 2),3)
700 if command
in commands
:
702 elif is_re
.search(msg
):
704 args
= is_re
.split(msg
.split(" ", 1)[1], 1)
706 command
= "factoid implicit"
707 args
= msg
.split(" ", 1)[1]
709 if command
in commands
and commands
[command
][0] <= Perms
.voice
:
711 elif is_re
.search(msg
):
713 args
= is_re
.split(msg
, 1)
715 command
= "factoid implicit"
717 got_command(who
, where
, command
, args
)
718 got_msg
= safe(got_msg
)
719 irc
.add_global_handler("privmsg", got_msg
)
720 irc
.add_global_handler("pubmsg", got_msg
)
725 cmd
= sys
.stdin
.readline().strip()
727 hal
.connection
.send_raw(cmd
)
728 thread
.start_new_thread(debug_console
, ())
731 for where
in reminder_dbs
.keys():
732 db
= reminder_dbs
[where
]
734 schedule_reminder(where
, db
, reminder
)
738 thread
.start_new_thread(run_reminders
, ())
740 hal
.ircobj
.process_forever(None)