diff options
author | Sean B. Palmer <http://inamidst.com/sbp/> | 2008-02-21 12:06:33 +0000 |
---|---|---|
committer | Sean B. Palmer <http://inamidst.com/sbp/> | 2008-02-21 12:06:33 +0000 |
commit | 7931fab14599b739c18c8f1ebcc24b75688dbc09 (patch) | |
tree | bf4df9757f10c155e3b6f78aed48f15884ebbbe6 /modules | |
download | bot-7931fab14599b739c18c8f1ebcc24b75688dbc09.tar.gz bot-7931fab14599b739c18c8f1ebcc24b75688dbc09.tar.bz2 bot-7931fab14599b739c18c8f1ebcc24b75688dbc09.zip |
Phenny2, now being tested on Freenode as the main phenny.
Diffstat (limited to 'modules')
-rw-r--r-- | modules/__init__.py | 0 | ||||
-rw-r--r-- | modules/admin.py | 53 | ||||
-rwxr-xr-x | modules/clock.py | 266 | ||||
-rw-r--r-- | modules/codepoints.py | 89 | ||||
-rwxr-xr-x | modules/etymology.py | 102 | ||||
-rwxr-xr-x | modules/head.py | 126 | ||||
-rw-r--r-- | modules/info.py | 44 | ||||
-rwxr-xr-x | modules/ping.py | 23 | ||||
-rwxr-xr-x | modules/reload.py | 34 | ||||
-rwxr-xr-x | modules/search.py | 82 | ||||
-rwxr-xr-x | modules/seen.py | 49 | ||||
-rw-r--r-- | modules/startup.py | 23 | ||||
-rwxr-xr-x | modules/tell.py | 164 | ||||
-rw-r--r-- | modules/translate.py | 102 | ||||
-rwxr-xr-x | modules/weather.py | 422 | ||||
-rw-r--r-- | modules/wikipedia.py | 146 |
16 files changed, 1725 insertions, 0 deletions
diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/__init__.py diff --git a/modules/admin.py b/modules/admin.py new file mode 100644 index 0000000..de2a7a7 --- /dev/null +++ b/modules/admin.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +""" +admin.py - Phenny Admin Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +def join(phenny, input): + # Can only be done in privmsg by an admin + if input.sender.startswith('#'): return + if input.admin: + phenny.write(['JOIN'], input.group(2)) +join.commands = ['join'] +join.priority = 'low' + +def part(phenny, input): + # Can only be done in privmsg by an admin + if input.sender.startswith('#'): return + if input.admin: + phenny.write(['PART'], input.group(2)) +part.commands = ['part'] +part.priority = 'low' + +def quit(phenny, input): + # Can only be done in privmsg by the owner + if input.sender.startswith('#'): return + if input.owner: + phenny.write(['QUIT']) + __import__('os')._exit(0) +quit.commands = ['quit'] +quit.priority = 'low' + +def msg(phenny, input): + # Can only be done in privmsg by an admin + if input.sender.startswith('#'): return + if input.admin: + phenny.msg(input.group(2), input.group(3)) +msg.rule = (['msg'], r'(#\S+) (.*)') +msg.priority = 'low' + +def me(phenny, input): + # Can only be done in privmsg by an admin + if input.sender.startswith('#'): return + if input.admin: + msg = '\x01ACTION %s\x01' % input.group(3) + phenny.msg(input.group(2), msg) +me.rule = (['me'], r'(#\S+) (.*)') +me.priority = 'low' + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/clock.py b/modules/clock.py new file mode 100755 index 0000000..210f8fb --- /dev/null +++ b/modules/clock.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +""" +clock.py - Phenny Clock Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import math, time, urllib +from tools import deprecated + +TimeZones = {'KST': 9, 'CADT': 10.5, 'EETDST': 3, 'MESZ': 2, 'WADT': 9, + 'EET': 2, 'MST': -7, 'WAST': 8, 'IST': 5.5, 'B': 2, + 'MSK': 3, 'X': -11, 'MSD': 4, 'CETDST': 2, 'AST': -4, + 'HKT': 8, 'JST': 9, 'CAST': 9.5, 'CET': 1, 'CEST': 2, + 'EEST': 3, 'EAST': 10, 'METDST': 2, 'MDT': -6, 'A': 1, + 'UTC': 0, 'ADT': -3, 'EST': -5, 'E': 5, 'D': 4, 'G': 7, + 'F': 6, 'I': 9, 'H': 8, 'K': 10, 'PDT': -7, 'M': 12, + 'L': 11, 'O': -2, 'MEST': 2, 'Q': -4, 'P': -3, 'S': -6, + 'R': -5, 'U': -8, 'T': -7, 'W': -10, 'WET': 0, 'Y': -12, + 'CST': -6, 'EADT': 11, 'Z': 0, 'GMT': 0, 'WETDST': 1, + 'C': 3, 'WEST': 1, 'CDT': -5, 'MET': 1, 'N': -1, 'V': -9, + 'EDT': -4, 'UT': 0, 'PST': -8, 'MEZ': 1, 'BST': 1, + 'ACS': 9.5, 'ATL': -4, 'ALA': -9, 'HAW': -10, 'AKDT': -8, + 'AKST': -9, + 'BDST': 2} + +TZ1 = { + 'NDT': -2.5, + 'BRST': -2, + 'ADT': -3, + 'EDT': -4, + 'CDT': -5, + 'MDT': -6, + 'PDT': -7, + 'YDT': -8, + 'HDT': -9, + 'BST': 1, + 'MEST': 2, + 'SST': 2, + 'FST': 2, + 'CEST': 2, + 'EEST': 3, + 'WADT': 8, + 'KDT': 10, + 'EADT': 13, + 'NZD': 13, + 'NZDT': 13, + 'GMT': 0, + 'UT': 0, + 'UTC': 0, + 'WET': 0, + 'WAT': -1, + 'AT': -2, + 'FNT': -2, + 'BRT': -3, + 'MNT': -4, + 'EWT': -4, + 'AST': -4, + 'EST': -5, + 'ACT': -5, + 'CST': -6, + 'MST': -7, + 'PST': -8, + 'YST': -9, + 'HST': -10, + 'CAT': -10, + 'AHST': -10, + 'NT': -11, + 'IDLW': -12, + 'CET': 1, + 'MEZ': 1, + 'ECT': 1, + 'MET': 1, + 'MEWT': 1, + 'SWT': 1, + 'SET': 1, + 'FWT': 1, + 'EET': 2, + 'UKR': 2, + 'BT': 3, + 'ZP4': 4, + 'ZP5': 5, + 'ZP6': 6, + 'WST': 8, + 'HKT': 8, + 'CCT': 8, + 'JST': 9, + 'KST': 9, + 'EAST': 10, + 'GST': 10, + 'NZT': 12, + 'NZST': 12, + 'IDLE': 12 +} + +TZ2 = { + 'ACDT': -10.5, + 'ACST': -9.5, + 'ADT': 3, + 'AEDT': 11, # hmm + 'AEST': 10, # hmm + 'AHDT': 9, + 'AHST': 10, + 'AST': 4, + 'AT': 2, + 'AWDT': -9, + 'AWST': -8, + 'BAT': -3, + 'BDST': -2, + 'BET': 11, + 'BST': -1, + 'BT': -3, + 'BZT2': 3, + 'CADT': -10.5, + 'CAST': -9.5, + 'CAT': 10, + 'CCT': -8, + # 'CDT': 5, + 'CED': -2, + 'CET': -1, + 'CST': 6, + 'EAST': -10, + # 'EDT': 4, + 'EED': -3, + 'EET': -2, + 'EEST': -3, + 'EST': 5, + 'FST': -2, + 'FWT': -1, + 'GMT': 0, + 'GST': -10, + 'HDT': 9, + 'HST': 10, + 'IDLE': -12, + 'IDLW': 12, + 'IST': -5.5, + 'IT': -3.5, + 'JST': -9, + 'JT': -7, + 'KST': -9, + 'MDT': 6, + 'MED': -2, + 'MET': -1, + 'MEST': -2, + 'MEWT': -1, + 'MST': 7, + 'MT': -8, + 'NDT': 2.5, + 'NFT': 3.5, + 'NT': 11, + 'NST': -6.5, + 'NZ': -11, + 'NZST': -12, + 'NZDT': -13, + 'NZT': -12, + # 'PDT': 7, + 'PST': 8, + 'ROK': -9, + 'SAD': -10, + 'SAST': -9, + 'SAT': -9, + 'SDT': -10, + 'SST': -2, + 'SWT': -1, + 'USZ3': -4, + 'USZ4': -5, + 'USZ5': -6, + 'USZ6': -7, + 'UT': 0, + 'UTC': 0, + 'UZ10': -11, + 'WAT': 1, + 'WET': 0, + 'WST': -8, + 'YDT': 8, + 'YST': 9, + 'ZP4': -4, + 'ZP5': -5, + 'ZP6': -6 +} + +TimeZones.update(TZ2) +TimeZones.update(TZ1) + +@deprecated +def f_time(self, origin, match, args): + """.t [ <timezone> ] - Returns the current time""" + tz = match.group(1) or 'GMT' + + # Personal time zones, because they're rad + if hasattr(self.config, 'timezones'): + People = self.config.timezones + else: People = {} + + if People.has_key(tz): + tz = People[tz] + elif (not match.group(1)) and People.has_key(origin.nick): + tz = People[origin.nick] + + TZ = tz.upper() + if len(tz) > 30: return + + if (TZ == 'UTC') or (TZ == 'Z'): + msg = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + self.msg(origin.sender, msg) + elif TimeZones.has_key(TZ): + offset = TimeZones[TZ] * 3600 + timenow = time.gmtime(time.time() + offset) + msg = time.strftime("%a, %d %b %Y %H:%M:%S " + str(TZ), timenow) + self.msg(origin.sender, msg) + elif tz and tz[0] in ('+', '-') and 4 <= len(tz) <= 6: + timenow = time.gmtime(time.time() + (int(tz[:3]) * 3600)) + msg = time.strftime("%a, %d %b %Y %H:%M:%S " + str(tz), timenow) + self.msg(origin.sender, msg) + else: + try: t = float(tz) + except ValueError: + import os, re, subprocess + r_tz = re.compile(r'^[A-Za-z]+(?:/[A-Za-z_]+)*$') + if r_tz.match(tz) and os.path.isfile('/usr/share/zoneinfo/' + tz): + cmd, PIPE = 'TZ=%s date' % tz, subprocess.PIPE + proc = subprocess.Popen(cmd, shell=True, stdout=PIPE) + self.msg(origin.sender, proc.communicate()[0]) + else: + error = "Sorry, I don't know about the '%s' timezone." % tz + self.msg(origin.sender, origin.nick + ': ' + error) + else: + timenow = time.gmtime(time.time() + (t * 3600)) + msg = time.strftime("%a, %d %b %Y %H:%M:%S " + str(tz), timenow) + self.msg(origin.sender, msg) +f_time.commands = ['t'] + +def beats(phenny, input): + beats = ((time.time() + 3600) % 86400) / 86.4 + beats = int(math.floor(beats)) + phenny.say('@%03i' % beats) +beats.commands = ['beats'] +beats.priority = 'low' + +def divide(input, by): + return (input / by), (input % by) + +def yi(phenny, input): + quadraels, remainder = divide(int(time.time()), 1753200) + raels = quadraels * 4 + extraraels, remainder = divide(remainder, 432000) + if extraraels == 4: + return phenny.say('Yes!') + else: phenny.say('Not yet...') +yi.commands = ['yi'] +yi.priority = 'low' + +# d8uv d8uv d8uv d8uv d8uv d8uv d8uv + +def tock(phenny, input): + u = urllib.urlopen('http://tycho.usno.navy.mil/cgi-bin/timer.pl') + info = u.info() + u.close() + phenny.say('"' + info['Date'] + '" - tycho.usno.navy.mil') +tock.commands = ['tock'] +tock.priority = 'high' + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/codepoints.py b/modules/codepoints.py new file mode 100644 index 0000000..83425c5 --- /dev/null +++ b/modules/codepoints.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +""" +codepoints.py - Phenny Codepoints Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re, unicodedata +from itertools import islice + +def about(u, cp=None, name=None): + if cp is None: cp = ord(u) + if name is None: name = unicodedata.name(u) + + if not unicodedata.combining(u): + template = 'U+%04X %s (%s)' + else: template = 'U+%04X %s (\xe2\x97\x8c%s)' + return template % (cp, name, u.encode('utf-8')) + +def codepoint_simple(arg): + arg = arg.upper() + r_label = re.compile('\\b' + arg.replace(' ', '.*\\b')) + + results = [] + for cp in xrange(0xFFFF): + u = unichr(cp) + try: name = unicodedata.name(u) + except ValueError: continue + + if r_label.search(name): + results.append((len(name), u, cp, name)) + if not results: + return None + + length, u, cp, name = sorted(results)[0] + return about(u, cp, name) + +def codepoint_extended(arg): + arg = arg.upper() + try: r_search = re.compile(arg) + except: raise ValueError('Broken regexp: %r' % arg) + + for cp in xrange(1, 0x10FFFF): + u = unichr(cp) + name = unicodedata.name(u, '-') + + if r_search.search(name): + yield about(u, cp, name) + +def u(phenny, input): + arg = input.bytes[3:] + + ascii = True + for c in arg: + if ord(c) >= 0x80: + ascii = False + + if ascii: + if set(arg.upper()) - set('ABCDEFGHIJKLMNOPQRSTUVWXYZ '): + extended = True + else: extended = False + + if extended: + # look up a codepoint with regexp + results = list(islice(codepoint_extended(arg), 4)) + for i, result in enumerate(results): + if (i < 2) or ((i == 2) and (len(results) < 4)): + phenny.say(result) + elif (i == 2) and (len(results) > 3): + phenny.say(result + ' [...]') + else: + # look up a codepoint freely + result = codepoint_simple(arg) + if result is not None: + phenny.say(result) + else: phenny.reply("Sorry, no results for %r." % arg) + else: + text = arg.decode('utf-8') + # look up less than three podecoints + if len(text) <= 3: + for u in text: + phenny.say(about(u)) + # look up more than three podecoints + elif len(text) <= 8: + phenny.reply(' '.join('U+%04X' % ord(c) for c in text)) + else: phenny.reply('Sorry, your input is too long!') +u.commands = ['u'] diff --git a/modules/etymology.py b/modules/etymology.py new file mode 100755 index 0000000..ecdbb7b --- /dev/null +++ b/modules/etymology.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +""" +etymology.py - Phenny Etymology Module +Copyright 2007, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re +import web +from tools import deprecated + +etyuri = 'http://etymonline.com/?term=%s' +etysearch = 'http://etymonline.com/?search=%s' + +r_definition = re.compile(r'(?ims)<dd[^>]*>.*?</dd>') +r_tag = re.compile(r'<(?!!)[^>]+>') +r_whitespace = re.compile(r'[\t\r\n ]+') + +abbrs = [ + 'cf', 'lit', 'etc', 'Ger', 'Du', 'Skt', 'Rus', 'Eng', 'Amer.Eng', 'Sp', + 'Fr', 'N', 'E', 'S', 'W', 'L', 'Gen', 'J.C', 'dial', 'Gk', + '19c', '18c', '17c', '16c', 'St', 'Capt' +] +t_sentence = r'^.*?(?<!%s)(?:\.(?= [A-Z0-9]|\Z)|\Z)' +r_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs)) + +def unescape(s): + s = s.replace('>', '>') + s = s.replace('<', '<') + s = s.replace('&', '&') + return s + +def text(html): + html = r_tag.sub('', html) + html = r_whitespace.sub(' ', html) + return unescape(html).strip() + +def etymology(word): + # @@ <nsh> sbp, would it be possible to have a flag for .ety to get 2nd/etc + # entries? - http://swhack.com/logs/2006-07-19#T15-05-29 + + if len(word) > 25: + raise ValueError("Word too long: %s[...]" % word[:10]) + word = {'axe': 'ax/axe'}.get(word, word) + + bytes = web.get(etyuri % word) + definitions = r_definition.findall(bytes) + + if not definitions: + return None + + defn = text(definitions[0]) + m = r_sentence.match(defn) + if not m: + return None + sentence = m.group(0) + + try: + sentence = unicode(sentence, 'iso-8859-1') + sentence = sentence.encode('utf-8') + except: pass + + maxlength = 275 + if len(sentence) > maxlength: + sentence = sentence[:maxlength] + words = sentence[:-5].split(' ') + words.pop() + sentence = ' '.join(words) + ' [...]' + + sentence = '"' + sentence.replace('"', "'") + '"' + return sentence + ' - ' + (etyuri % word) + +@deprecated +def f_etymology(self, origin, match, args): + word = match.group(2) + + try: result = etymology(word) + except IOError: + msg = "Can't connect to etymonline.com (%s)" % (etyuri % word) + self.msg(origin.sender, msg) + return + + if result is not None: + if (origin.sender == '#esp') and (origin.nick == 'nsh'): + self.msg(origin.nick, result) + note = 'nsh: see privmsg (yes, this only happens for you)' + self.msg(origin.sender, note) + else: self.msg(origin.sender, result) + else: + uri = etysearch % word + msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri) + self.msg(origin.sender, msg) +# @@ Cf. http://swhack.com/logs/2006-01-04#T01-50-22 +f_etymology.rule = (['ety'], r"([A-Za-z0-9' -]+)") +f_etymology.thread = True +f_etymology.priority = 'high' + +if __name__=="__main__": + import sys + print etymology(sys.argv[1]) diff --git a/modules/head.py b/modules/head.py new file mode 100755 index 0000000..4b75cb4 --- /dev/null +++ b/modules/head.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +""" +head.py - Phenny HTTP Metadata Utilities +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re, urllib +from htmlentitydefs import name2codepoint +import web +from tools import deprecated + +@deprecated +def f_httphead(self, origin, match, args): + """.head <URI> <FieldName>? - Perform an HTTP HEAD on URI.""" + if origin.sender == '#talis': return + uri = match.group(2) + header = match.group(3) + + try: info = web.head(uri) + except IOError: + self.msg(origin.sender, "Can't connect to %s" % uri) + return + + if not isinstance(info, list): + info = dict(info) + info['Status'] = '200' + else: + newInfo = dict(info[0]) + newInfo['Status'] = str(info[1]) + info = newInfo + + if header is None: + msg = 'Status: %s (for more, try ".head uri header")' % info['Status'] + self.msg(origin.sender, msg) + else: + headerlower = header.lower() + if info.has_key(headerlower): + self.msg(origin.sender, header + ': ' + info.get(headerlower)) + else: + msg = 'There was no %s header in the response.' % header + self.msg(origin.sender, msg) +f_httphead.rule = (['head'], r'(\S+)(?: +(\S+))?') +f_httphead.thread = True + +r_title = re.compile(r'(?ims)<title[^>]*>(.*?)</title\s*>') +r_entity = re.compile(r'&[A-Za-z0-9#]+;') + +@deprecated +def f_title(self, origin, match, args): + """.title <URI> - Return the title of URI.""" + uri = match.group(2) + if not ':' in uri: + uri = 'http://' + uri + + try: + redirects = 0 + while True: + info = web.head(uri) + + if not isinstance(info, list): + status = '200' + else: + status = str(info[1]) + info = info[0] + if status.startswith('3'): + uri = info['Location'] + else: break + + redirects += 1 + if redirects >= 25: + self.msg(origin.sender, origin.nick + ": Too many redirects") + return + + try: mtype = info['Content-Type'] + except: + self.msg(origin.sender, origin.nick + ": Document isn't HTML") + return + if not (('/html' in mtype) or ('/xhtml' in mtype)): + self.msg(origin.sender, origin.nick + ": Document isn't HTML") + return + + u = urllib.urlopen(uri) + bytes = u.read(32768) + u.close() + + except IOError: + self.msg(origin.sender, "Can't connect to %s" % uri) + return + + m = r_title.search(bytes) + if m: + title = m.group(1) + title = title.strip() + title = title.replace('\t', ' ') + title = title.replace('\r', ' ') + title = title.replace('\n', ' ') + while ' ' in title: + title = title.replace(' ', ' ') + if len(title) > 200: + title = title[:200] + '[...]' + + def e(m): + entity = m.group(0) + if entity.startswith('&#x'): + cp = int(entity[3:-1], 16) + return unichr(cp).encode('utf-8') + elif entity.startswith('&#'): + cp = int(entity[2:-1]) + return unichr(cp).encode('utf-8') + else: + char = name2codepoint[entity[1:-1]] + return unichr(char).encode('utf-8') + title = r_entity.sub(e, title) + + if not title: + title = '[Title is the empty document, "".]' + self.msg(origin.sender, origin.nick + ': ' + title) + else: self.msg(origin.sender, origin.nick + ': No title found') +f_title.rule = (['title'], r'(\S+)') +f_title.thread = True + +if __name__ == '__main__': + print __doc__ diff --git a/modules/info.py b/modules/info.py new file mode 100644 index 0000000..a70c823 --- /dev/null +++ b/modules/info.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +""" +info.py - Phenny Information Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +def doc(phenny, input): + """Shows a command's documentation, and possibly an example.""" + name = input.group(1) + name = name.lower() + + if phenny.doc.has_key(name): + phenny.reply(phenny.doc[name][0]) + if phenny.doc[name][1]: + phenny.say('e.g. ' + phenny.doc[name][1]) +doc.rule = ('$nick', '(?i)help +([A-Za-z]+)(?:\?+)?$') +doc.example = '$nickname: help tell?' +doc.priority = 'low' + +def commands(phenny, input): + # This function only works in private message + if input.startswith('#'): return + names = ', '.join(sorted(phenny.doc.iterkeys())) + phenny.say('Commands I recognise: ' + names + '.') + phenny.say(("For help, do '%s: help example?' where example is the " + + "name of the command you want help for.") % phenny.nick) +commands.commands = ['commands'] +commands.priority = 'low' + +def help(phenny, input): + response = ( + 'Hi, I\'m a bot. Say ".commands" to me in private for a list ' + + 'of my commands, or see http://inamidst.com/phenny/ for more ' + + 'general details. My owner is %s.' + ) % phenny.config.owner + phenny.reply(response) +help.rule = ('$nick', r'(?i)help(?:[?!]+)?$') +help.priority = 'low' + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/ping.py b/modules/ping.py new file mode 100755 index 0000000..97e41e1 --- /dev/null +++ b/modules/ping.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +""" +ping.py - Phenny Ping Module +Author: Sean B. Palmer, inamidst.com +About: http://inamidst.com/phenny/ +""" + +import random + +def hello(phenny, input): + greeting = random.choice(('Hi', 'Hey', 'Hello')) + punctuation = random.choice(('', '!')) + phenny.say(greeting + ' ' + input.nick + punctuation) +hello.rule = r'(?i)(hi|hello|hey) $nickname\b' + +def interjection(phenny, input): + phenny.say(input.nick + '!') +interjection.rule = r'$nickname!' +interjection.priority = 'high' +interjection.thread = False + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/reload.py b/modules/reload.py new file mode 100755 index 0000000..257eaf7 --- /dev/null +++ b/modules/reload.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +""" +reload.py - Phenny Module Reloader Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import irc + +def f_reload(phenny, input): + """Reloads a module, for use by admins only.""" + if not input.admin: return + + name = match.group(2) + module = getattr(__import__('modules.' + name), name) + reload(module) + + if hasattr(module, '__file__'): + import os.path, time + mtime = os.path.getmtime(module.__file__) + modified = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(mtime)) + else: modified = 'unknown' + + self.register(vars(module)) + self.bind_commands() + + phenny.reply('%r (version: %s)' % (module, modified)) +f_reload.name = 'reload' +f_reload.rule = ('$nick', ['reload'], r'(\S+)') + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/search.py b/modules/search.py new file mode 100755 index 0000000..9ad1a04 --- /dev/null +++ b/modules/search.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +""" +search.py - Phenny Web Search Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re +import web + +r_string = re.compile(r'("(\\.|[^"\\])*")') +r_json = re.compile(r'^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$') +env = {'__builtins__': None, 'null': None, 'true': True, 'false': False} + +def json(text): + """Evaluate JSON text safely (we hope).""" + if r_json.match(r_string.sub('', text)): + text = r_string.sub(lambda m: 'u' + m.group(1), text) + return eval(text.strip(' \t\r\n'), env, {}) + raise ValueError('Input must be serialised JSON.') + +def search(query, n=1): + """Search using SearchMash, return its JSON.""" + q = web.urllib.quote(query.encode('utf-8')) + uri = 'http://www.searchmash.com/results/' + q + '?n=' + str(n) + bytes = web.get(uri) + return json(bytes) + +def result(query): + results = search(query) + return results['results'][0]['url'] + +def count(query): + results = search(query) + return results['estimatedCount'] + +def formatnumber(n): + """Format a number with beautiful commas.""" + parts = list(str(n)) + for i in range((len(parts) - 3), 0, -3): + parts.insert(i, ',') + return ''.join(parts) + +def g(phenny, input): + uri = result(input.group(2)) + phenny.reply(uri) +g.commands = ['g'] +g.priority = 'high' + +def gc(phenny, input): + query = input.group(2) + num = count(query) + phenny.say(query + ': ' + num) +gc.commands = ['gc'] +gc.priority = 'high' + +r_query = re.compile( + r'\+?"[^"\\]*(?:\\.[^"\\]*)*"|\[[^]\\]*(?:\\.[^]\\]*)*\]|\S+' +) + +def compare(phenny, input): + queries = r_query.findall(input.group(2)) + if len(queries) > 6: + return phenny.reply('Sorry, can only compare up to six things.') + + results = [] + for i, query in enumerate(queries): + query = query.strip('[]') + n = int((count(query) or '0').replace(',', '')) + results.append((n, query)) + if i >= 2: __import__('time').sleep(0.25) + if i >= 4: __import__('time').sleep(0.25) + + results = [(term, n) for (n, term) in reversed(sorted(results))] + reply = ', '.join('%s (%s)' % (t, formatnumber(n)) for (t, n) in results) + phenny.say(reply) +compare.commands = ['gco', 'comp'] + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/seen.py b/modules/seen.py new file mode 100755 index 0000000..189be61 --- /dev/null +++ b/modules/seen.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +""" +seen.py - Phenny Seen Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import time +from tools import deprecated + +@deprecated +def f_seen(self, origin, match, args): + """.seen <nick> - Reports when <nick> was last seen.""" + if origin.sender == '#talis': return + nick = match.group(2).lower() + if not hasattr(self, 'seen'): + return self.msg(origin.sender, '?') + if self.seen.has_key(nick): + channel, t = self.seen[nick] + t = time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(t)) + + msg = "I last saw %s at %s on %s" % (nick, t, channel) + self.msg(origin.sender, str(origin.nick) + ': ' + msg) + else: self.msg(origin.sender, "Sorry, I haven't seen %s around." % nick) +f_seen.rule = (['seen'], r'(\S+)') + +@deprecated +def f_note(self, origin, match, args): + def note(self, origin, match, args): + if not hasattr(self.bot, 'seen'): + self.bot.seen = {} + if origin.sender.startswith('#'): + # if origin.sender == '#inamidst': return + self.seen[origin.nick.lower()] = (origin.sender, time.time()) + + # if not hasattr(self, 'chanspeak'): + # self.chanspeak = {} + # if (len(args) > 2) and args[2].startswith('#'): + # self.chanspeak[args[2]] = args[0] + + try: note(self, origin, match, args) + except Exception, e: print e +f_note.rule = r'(.*)' +f_note.priority = 'low' + +if __name__ == '__main__': + print __doc__ diff --git a/modules/startup.py b/modules/startup.py new file mode 100644 index 0000000..1fd7348 --- /dev/null +++ b/modules/startup.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +""" +startup.py - Phenny Startup Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +def startup(phenny, input): + if hasattr(phenny.config, 'password'): + phenny.msg('NickServ', 'IDENTIFY %s' % phenny.config.password) + __import__('time').sleep(5) + + # Cf. http://swhack.com/logs/2005-12-05#T19-32-36 + for channel in phenny.channels: + phenny.write(('JOIN', channel)) +startup.rule = r'(.*)' +startup.event = '251' +startup.priority = 'low' + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/tell.py b/modules/tell.py new file mode 100755 index 0000000..3b487c8 --- /dev/null +++ b/modules/tell.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +""" +tell.py - Phenny Tell and Ask Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import os, re, time, random +import web + +maximum = 4 +lispchannels = frozenset([ '#lisp', '#scheme', '#opendarwin', '#macdev', +'#fink', '#jedit', '#dylan', '#emacs', '#xemacs', '#colloquy', '#adium', +'#growl', '#chicken', '#quicksilver', '#svn', '#slate', '#squeak', '#wiki', +'#nebula', '#myko', '#lisppaste', '#pearpc', '#fpc', '#hprog', +'#concatenative', '#slate-users', '#swhack', '#ud', '#t', '#compilers', +'#erights', '#esp', '#scsh', '#sisc', '#haskell', '#rhype', '#sicp', '#darcs', +'#hardcider', '#lisp-it', '#webkit', '#launchd', '#mudwalker', '#darwinports', +'#muse', '#chatkit', '#kowaleba', '#vectorprogramming', '#opensolaris', +'#oscar-cluster', '#ledger', '#cairo', '#idevgames', '#hug-bunny', '##parsers', +'#perl6', '#sdlperl', '#ksvg', '#rcirc', '#code4lib', '#linux-quebec', +'#programmering', '#maxima', '#robin', '##concurrency', '#paredit' ]) + +def loadReminders(fn): + result = {} + f = open(fn) + for line in f: + line = line.strip() + if line: + tellee, teller, verb, timenow, msg = line.split('\t', 4) + result.setdefault(tellee, []).append((teller, verb, timenow, msg)) + f.close() + return result + +def dumpReminders(fn, data): + f = open(fn, 'w') + for tellee in data.iterkeys(): + for remindon in data[tellee]: + line = '\t'.join((tellee,) + remindon) + f.write(line + '\n') + f.close() + return True + +def setup(self): + fn = self.nick + '-' + self.config.host + '.tell.db' + self.tell_filename = os.path.join(os.path.expanduser('~/.phenny'), fn) + if not os.path.exists(self.tell_filename): + try: f = open(self.tell_filename, 'w') + except OSError: pass + else: + f.write('') + f.close() + self.reminders = loadReminders(self.tell_filename) # @@ tell + +def f_remind(phenny, input): + teller = input.nick + + # @@ Multiple comma-separated tellees? Cf. Terje, #swhack, 2006-04-15 + verb, tellee, msg = input.groups() + tellee_original = tellee.rstrip(',:;') + tellee = tellee.lower() + + if not os.path.exists(phenny.tell_filename): + return + + if len(tellee) > 20: + return phenny.reply('That nickname is too long.') + + timenow = time.strftime('%d %b %H:%MZ', time.gmtime()) + if not tellee in (teller.lower(), phenny.nick, 'me'): # @@ + # @@ <deltab> and year, if necessary + warn = False + if not phenny.reminders.has_key(tellee): + phenny.reminders[tellee] = [(teller, verb, timenow, msg)] + else: + if len(phenny.reminders[tellee]) >= maximum: + warn = True + phenny.reminders[tellee].append((teller, verb, timenow, msg)) + # @@ Stephanie's augmentation + response = "I'll pass that on when %s is around." % tellee_original + if warn: response += (" I'll have to use a pastebin, though, so " + + "your message may get lost.") + + rand = random.random() + if rand > 0.9999: response = "yeah, yeah" + elif rand > 0.999: response = "%s: yeah, sure, whatever" % teller + + phenny.reply(response) + elif teller.lower() == tellee: + phenny.say('You can %s yourself that.' % verb) + else: phenny.say("Hey, I'm not as stupid as Monty you know!") + + dumpReminders(phenny.tell_filename, phenny.reminders) # @@ tell +f_remind.rule = ('$nick', ['tell', 'ask'], r'(\S+) (.*)') + +def getReminders(phenny, channel, key, tellee): + lines = [] + template = "%s: %s <%s> %s %s %s" + today = time.strftime('%d %b', time.gmtime()) + + for (teller, verb, datetime, msg) in phenny.reminders[key]: + if datetime.startswith(today): + datetime = datetime[len(today)+1:] + lines.append(template % (tellee, datetime, teller, verb, tellee, msg)) + + try: del phenny.reminders[key] + except KeyError: phenny.msg(channel, 'Er...') + return lines + +def message(phenny, input): + if not input.sender.startswith('#'): return + + tellee = input.nick + channel = input.sender + + if not os.path.exists(phenny.tell_filename): + return + + reminders = [] + remkeys = list(reversed(sorted(phenny.reminders.keys()))) + for remkey in remkeys: + if not remkey.endswith('*'): + if tellee.lower() == remkey: + reminders.extend(getReminders(phenny, channel, remkey, tellee)) + elif tellee.lower().startswith(remkey.rstrip('*')): + reminders.extend(getReminders(phenny, channel, remkey, tellee)) + + for line in reminders[:maximum]: + phenny.say(line) + + if reminders[maximum:]: + try: + if origin.sender in lispchannels: + chan = origin.sender + else: chan = 'None' + + result = web.post('http://paste.lisp.org/submit', + {'channel': chan, + 'username': phenny.nick, + 'title': 'Further Messages for %s' % tellee, + 'colorize': 'None', + 'text': '\n'.join(reminders[maximum:]) + '\n', + 'captcha': 'lisp', + 'captchaid': 'bdf447484f62a3e8b23816f9acee79d9' + } + ) + uris = re.findall('http://paste.lisp.org/display/\d+', result) + uri = list(reversed(uris)).pop() + if not origin.sender in lispchannels: + message = '%s: see %s for further messages' % (tellee, uri) + phenny.say(message) + except: + error = '[Sorry, some messages were elided and lost...]' + phenny.say(error) + + if len(phenny.reminders.keys()) != remkeys: + dumpReminders(phenny.tell_filename, phenny.reminders) # @@ tell +message.rule = r'(.*)' +message.priority = 'low' + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/translate.py b/modules/translate.py new file mode 100644 index 0000000..ed3589f --- /dev/null +++ b/modules/translate.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +translate.py - Phenny Translation Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re +import web + +r_translation = re.compile(r'<div style=padding:10px;>([^<]+)</div>') + +def guess_language(phrase): + languages = { + 'english': 'en', + 'french': 'fr', + 'spanish': 'es', + 'portuguese': 'pt', + 'german': 'de', + 'italian': 'it', + 'korean': 'ko', + 'japanese': 'ja', + 'chinese': 'zh', + 'dutch': 'nl', + 'greek': 'el', + 'russian': 'ru' + } + + uri = 'http://www.xrce.xerox.com/cgi-bin/mltt/LanguageGuesser' + form = {'Text': phrase} + bytes = web.post(uri, form) + for line in bytes.splitlines(): + if '<listing><font size=+1>' in line: + i = line.find('<listing><font size=+1>') + lang = line[i+len('<listing><font size=+1>'):].strip() + lang = lang.lower() + if '_' in lang: + j = lang.find('_') + lang = lang[:j] + try: return languages[lang] + except KeyError: + return lang + return 'unknown' + +def translate(phrase, lang, target='en'): + babelfish = 'http://world.altavista.com/tr' + form = { + 'doit': 'done', + 'intl': '1', + 'tt': 'urltext', + 'trtext': phrase, + 'lp': lang + '_' + target + } + + bytes = web.post(babelfish, form) + m = r_translation.search(bytes) + if m: + translation = m.group(1) + translation = translation.replace('\r', ' ') + translation = translation.replace('\n', ' ') + while ' ' in translation: + translation = translation.replace(' ', ' ') + return translation + return None + +def tr(phenny, input): + lang, phrase = input.groups() + + if (len(phrase) > 350) and (not phenny.admin(input.nick)): + return phenny.reply('Phrase must be under 350 characters.') + + language = guess_language(phrase) + if language is None: + return phenny.reply('Unable to guess the language, sorry.') + + if language != 'en': + translation = translate(phrase, language) + if translation is not None: + return phenny.reply(u'"%s" (%s)' % (translation, language)) + + error = "I think it's %s, but I can't translate that language." + return phenny.reply(error % language.title()) + + # Otherwise, it's English, so mangle it for fun + for other in ['de', 'ja']: + phrase = translate(phrase, 'en', other) + phrase = translate(phrase, other, 'en') + + if phrase is not None: + return phenny.reply(u'"%s" (en-unmangled)' % phrase) + return phenny.reply("I think it's English already.") + # @@ or 'Why but that be English, sire.' +tr.doc = ('phenny: "<phrase>"? or phenny: <lang> "<phrase>"?', + 'Translate <phrase>, optionally forcing the <lang> interpretation.') +tr.rule = ('$nick', ur'(?:([a-z]{2}) +)?["“](.+?)["”]\? *$') +tr.priority = 'low' + +if __name__ == '__main__': + print __doc__.strip() diff --git a/modules/weather.py b/modules/weather.py new file mode 100755 index 0000000..9e03bf4 --- /dev/null +++ b/modules/weather.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python +""" +weather.py - Phenny Weather Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re, urllib +import web +from tools import deprecated + +r_from = re.compile(r'(?i)([+-]\d+):00 from') + +r_json = re.compile(r'^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$') +r_string = re.compile(r'("(\\.|[^"\\])*")') +env = {'__builtins__': None, 'null': None, + 'true': True, 'false': False} + +def json(text): + """Evaluate JSON text safely (we hope).""" + if r_json.match(r_string.sub('', text)): + text = r_string.sub(lambda m: 'u' + m.group(1), text) + return eval(text.strip(' \t\r\n'), env, {}) + raise ValueError('Input must be serialised JSON.') + +def location(name): + name = urllib.quote(name) + uri = 'http://ws.geonames.org/searchJSON?q=%s&maxRows=1' % name + for i in xrange(10): + u = urllib.urlopen(uri) + if u is not None: break + bytes = u.read() + u.close() + + results = json(bytes) + try: name = results['geonames'][0]['name'] + except IndexError: + return '?', '?', '0', '0' + countryName = results['geonames'][0]['countryName'] + lat = results['geonames'][0]['lat'] + lng = results['geonames'][0]['lng'] + return name, countryName, lat, lng + +class GrumbleError(object): + pass + +def local(icao, hour, minute): + uri = ('http://www.flightstats.com/' + + 'go/Airport/airportDetails.do?airportCode=%s') + try: bytes = web.get(uri % icao) + except AttributeError: + raise GrumbleError('A WEBSITE HAS GONE DOWN WTF STUPID WEB') + m = r_from.search(bytes) + if m: + offset = m.group(1) + lhour = int(hour) + int(offset) + lhour = lhour % 24 + return (str(lhour) + ':' + str(minute) + ', ' + str(hour) + + str(minute) + 'Z') + # return (str(lhour) + ':' + str(minute) + ' (' + str(hour) + + # ':' + str(minute) + 'Z)') + return str(hour) + ':' + str(minute) + 'Z' + +def code(phenny, search): + name, country, latitude, longitude = location(search) + if name == '?': return False + + sumOfSquares = (99999999999999999999999999999, 'ICAO') + from icao import data + for icao_code, lat, lon in data: + latDiff = abs(latitude - lat) + lonDiff = abs(longitude - lon) + diff = (latDiff * latDiff) + (lonDiff * lonDiff) + if diff < sumOfSquares[0]: + sumOfSquares = (diff, icao_code) + return sumOfSquares[1] + +@deprecated +def f_weather(self, origin, match, args): + """.weather <ICAO> - Show the weather at airport with the code <ICAO>.""" + if origin.sender == '#talis': + if args[0].startswith('.weather '): return + + icao_code = match.group(2) + if (not len(icao_code) == 4) or \ + (len(icao_code) > 1 and icao_code[0] in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' and + icao_code[1] in 'abcdefghijklmnopqrstuvwxyz'): + icao_code = code(self, icao_code) + else: icao_code = icao_code.upper() + + if not icao_code: + self.msg(origin.sender, 'No ICAO code found, sorry') + return + + uri = 'http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT' + try: bytes = web.get(uri % icao_code) + except AttributeError: + raise GrumbleError('OH CRAP NOAA HAS GONE DOWN THE WEB IS BROKEN') + if 'Not Found' in bytes: + self.msg(origin.sender, icao_code+': no such ICAO code, or no NOAA data') + return + + metar = bytes.splitlines().pop() + metar = metar.split(' ') + + if len(metar[0]) == 4: + metar = metar[1:] + + if metar[0].endswith('Z'): + time = metar[0] + metar = metar[1:] + else: time = None + + if metar[0] == 'AUTO': + metar = metar[1:] + if metar[0] == 'VCU': + self.msg(origin.sender, icao_code + ': no data provided') + return + + if metar[0].endswith('KT'): + wind = metar[0] + metar = metar[1:] + else: wind = None + + if ('V' in metar[0]) and (metar[0] != 'CAVOK'): + vari = metar[0] + metar = metar[1:] + else: vari = None + + if ((len(metar[0]) == 4) or + metar[0].endswith('SM')): + visibility = metar[0] + metar = metar[1:] + else: visibility = None + + while metar[0].startswith('R') and (metar[0].endswith('L') + or 'L/' in metar[0]): + metar = metar[1:] + + if len(metar[0]) == 6 and (metar[0].endswith('N') or + metar[0].endswith('E') or + metar[0].endswith('S') or + metar[0].endswith('W')): + metar = metar[1:] # 7000SE? + + cond = [] + while (((len(metar[0]) < 5) or + metar[0].startswith('+') or + metar[0].startswith('-')) and (not (metar[0].startswith('VV') or + metar[0].startswith('SKC') or metar[0].startswith('CLR') or + metar[0].startswith('FEW') or metar[0].startswith('SCT') or + metar[0].startswith('BKN') or metar[0].startswith('OVC')))): + cond.append(metar[0]) + metar = metar[1:] + + while '/P' in metar[0]: + metar = metar[1:] + + if not metar: + self.msg(origin.sender, icao_code + ': no data provided') + return + + cover = [] + while (metar[0].startswith('VV') or metar[0].startswith('SKC') or + metar[0].startswith('CLR') or metar[0].startswith('FEW') or + metar[0].startswith('SCT') or metar[0].startswith('BKN') or + metar[0].startswith('OVC')): + cover.append(metar[0]) + metar = metar[1:] + if not metar: + self.msg(origin.sender, icao_code + ': no data provided') + return + + if metar[0] == 'CAVOK': + cover.append('CLR') + metar = metar[1:] + + if metar[0] == 'PRFG': + cover.append('CLR') # @@? + metar = metar[1:] + + if metar[0] == 'NSC': + cover.append('CLR') + metar = metar[1:] + + if ('/' in metar[0]) or (len(metar[0]) == 5 and metar[0][2] == '.'): + temp = metar[0] + metar = metar[1:] + else: temp = None + + if metar[0].startswith('QFE'): + metar = metar[1:] + + if metar[0].startswith('Q') or metar[0].startswith('A'): + pressure = metar[0] + metar = metar[1:] + else: pressure = None + + if time: + hour = time[2:4] + minute = time[4:6] + time = local(icao_code, hour, minute) + else: time = '(time unknown)' + + if wind: + speed = int(wind[3:5]) + if speed < 1: + description = 'Calm' + elif speed < 4: + description = 'Light air' + elif speed < 7: + description = 'Light breeze' + elif speed < 11: + description = 'Gentle breeze' + elif speed < 16: + description = 'Moderate breeze' + elif speed < 22: + description = 'Fresh breeze' + elif speed < 28: + description = 'Strong breeze' + elif speed < 34: + description = 'Near gale' + elif speed < 41: + description = 'Gale' + elif speed < 48: + description = 'Strong gale' + elif speed < 56: + description = 'Storm' + elif speed < 64: + description = 'Violent storm' + else: description = 'Hurricane' + + degrees = wind[0:3] + if degrees == 'VRB': + degrees = u'\u21BB'.encode('utf-8') + elif (degrees <= 22.5) or (degrees > 337.5): + degrees = u'\u2191'.encode('utf-8') + elif (degrees > 22.5) and (degrees <= 67.5): + degrees = u'\u2197'.encode('utf-8') + elif (degrees > 67.5) and (degrees <= 112.5): + degrees = u'\u2192'.encode('utf-8') + elif (degrees > 112.5) and (degrees <= 157.5): + degrees = u'\u2198'.encode('utf-8') + elif (degrees > 157.5) and (degrees <= 202.5): + degrees = u'\u2193'.encode('utf-8') + elif (degrees > 202.5) and (degrees <= 247.5): + degrees = u'\u2199'.encode('utf-8') + elif (degrees > 247.5) and (degrees <= 292.5): + degrees = u'\u2190'.encode('utf-8') + elif (degrees > 292.5) and (degrees <= 337.5): + degrees = u'\u2196'.encode('utf-8') + + if not icao_code.startswith('EN') and not icao_code.startswith('ED'): + wind = '%s %skt (%s)' % (description, speed, degrees) + elif icao_code.startswith('ED'): + kmh = int(round(speed * 1.852, 0)) + wind = '%s %skm/h (%skt) (%s)' % (description, kmh, speed, degrees) + elif icao_code.startswith('EN'): + ms = int(round(speed * 0.514444444, 0)) + wind = '%s %sm/s (%skt) (%s)' % (description, ms, speed, degrees) + else: wind = '(wind unknown)' + + if visibility: + visibility = visibility + 'm' + else: visibility = '(visibility unknown)' + + if cover: + level = None + for c in cover: + if c.startswith('OVC') or c.startswith('VV'): + if (level is None) or (level < 8): + level = 8 + elif c.startswith('BKN'): + if (level is None) or (level < 5): + level = 5 + elif c.startswith('SCT'): + if (level is None) or (level < 3): + level = 3 + elif c.startswith('FEW'): + if (level is None) or (level < 1): + level = 1 + elif c.startswith('SKC') or c.startswith('CLR'): + if level is None: + level = 0 + + if level == 8: + cover = u'Overcast \u2601'.encode('utf-8') + elif level == 5: + cover = 'Cloudy' + elif level == 3: + cover = 'Scattered' + elif (level == 1) or (level == 0): + cover = u'Clear \u263C'.encode('utf-8') + else: cover = 'Cover Unknown' + else: cover = 'Cover Unknown' + + if temp: + if '/' in temp: + temp = temp.split('/')[0] + else: temp = temp.split('.')[0] + if temp.startswith('M'): + temp = '-' + temp[1:] + try: temp = int(temp) + except ValueError: temp = '?' + else: temp = '?' + + if pressure: + if pressure.startswith('Q'): + pressure = pressure.lstrip('Q') + if pressure != 'NIL': + pressure = str(int(pressure)) + 'mb' + else: pressure = '?mb' + elif pressure.startswith('A'): + pressure = pressure.lstrip('A') + if pressure != 'NIL': + inches = pressure[:2] + '.' + pressure[2:] + mb = int(float(inches) * 33.7685) + pressure = '%sin (%smb)' % (inches, mb) + else: pressure = '?mb' + + if isinstance(temp, int): + f = round((temp * 1.8) + 32, 2) + temp = u'%s\u2109 (%s\u2103)'.encode('utf-8') % (f, temp) + else: pressure = '?mb' + if isinstance(temp, int): + temp = u'%s\u2103'.encode('utf-8') % temp + + if cond: + conds = cond + cond = '' + + intensities = { + '-': 'Light', + '+': 'Heavy' + } + + descriptors = { + 'MI': 'Shallow', + 'PR': 'Partial', + 'BC': 'Patches', + 'DR': 'Drifting', + 'BL': 'Blowing', + 'SH': 'Showers of', + 'TS': 'Thundery', + 'FZ': 'Freezing', + 'VC': 'In the vicinity:' + } + + phenomena = { + 'DZ': 'Drizzle', + 'RA': 'Rain', + 'SN': 'Snow', + 'SG': 'Snow Grains', + 'IC': 'Ice Crystals', + 'PL': 'Ice Pellets', + 'GR': 'Hail', + 'GS': 'Small Hail', + 'UP': 'Unknown Precipitation', + 'BR': 'Mist', + 'FG': 'Fog', + 'FU': 'Smoke', + 'VA': 'Volcanic Ash', + 'DU': 'Dust', + 'SA': 'Sand', + 'HZ': 'Haze', + 'PY': 'Spray', + 'PO': 'Whirls', + 'SQ': 'Squalls', + 'FC': 'Tornado', + 'SS': 'Sandstorm', + 'DS': 'Duststorm', + # ? Cf. http://swhack.com/logs/2007-10-05#T07-58-56 + 'TS': 'Thunderstorm', + 'SH': 'Showers' + } + + for c in conds: + if c.endswith('//'): + if cond: cond += ', ' + cond += 'Some Precipitation' + elif len(c) == 5: + intensity = intensities[c[0]] + descriptor = descriptors[c[1:3]] + phenomenon = phenomena.get(c[3:], c[3:]) + if cond: cond += ', ' + cond += intensity + ' ' + descriptor + ' ' + phenomenon + elif len(c) == 4: + descriptor = descriptors.get(c[:2], c[:2]) + phenomenon = phenomena.get(c[2:], c[2:]) + if cond: cond += ', ' + cond += descriptor + ' ' + phenomenon + elif len(c) == 3: + intensity = intensities.get(c[0], c[0]) + phenomenon = phenomena.get(c[1:], c[1:]) + if cond: cond += ', ' + cond += intensity + ' ' + phenomenon + elif len(c) == 2: + phenomenon = phenomena.get(c, c) + if cond: cond += ', ' + cond += phenomenon + + # if not cond: + # format = u'%s at %s: %s, %s, %s, %s' + # args = (icao, time, cover, temp, pressure, wind) + # else: + # format = u'%s at %s: %s, %s, %s, %s, %s' + # args = (icao, time, cover, temp, pressure, cond, wind) + + if not cond: + format = u'%s, %s, %s, %s - %s %s' + args = (cover, temp, pressure, wind, str(icao_code), time) + else: + format = u'%s, %s, %s, %s, %s - %s, %s' + args = (cover, temp, pressure, cond, wind, str(icao_code), time) + + self.msg(origin.sender, format.encode('utf-8') % args) +f_weather.rule = (['weather'], r'(.*)') + +if __name__ == '__main__': + print __doc__ diff --git a/modules/wikipedia.py b/modules/wikipedia.py new file mode 100644 index 0000000..893ecab --- /dev/null +++ b/modules/wikipedia.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +""" +wikipedia.py - Phenny Wikipedia Module +Copyright 2008, Sean B. Palmer, inamidst.com +Licensed under the Eiffel Forum License 2. + +http://inamidst.com/phenny/ +""" + +import re, urllib +import web + +wikiuri = 'http://en.wikipedia.org/wiki/%s' +wikisearch = 'http://en.wikipedia.org/wiki/Special:Search?' \ + + 'search=%s&fulltext=Search' + +r_tr = re.compile(r'(?ims)<tr[^>]*>.*?</tr>') +r_paragraph = re.compile(r'(?ims)<p[^>]*>.*?</p>|<li(?!n)[^>]*>.*?</li>') +r_tag = re.compile(r'<(?!!)[^>]+>') +r_whitespace = re.compile(r'[\t\r\n ]+') +r_redirect = re.compile( + r'(?ims)class=.redirectText.>\s*<a\s*href=./wiki/([^"/]+)' +) + +abbrs = ['etc', 'ca', 'cf', 'Co', 'Ltd', 'Inc', 'Mt', 'Mr', 'Mrs', + 'Dr', 'Ms', 'Rev', 'Fr', 'St', 'Sgt', 'pron', 'approx', 'lit', + 'syn'] \ + + list('ABCDEFGHIJKLMNOPQRSTUVWXYZ') \ + + list('abcdefghijklmnopqrstuvwxyz') +t_sentence = r'^.{5,}?(?<!\b%s)(?:\.(?= [A-Z0-9]|\Z)|\Z)' +r_sentence = re.compile(t_sentence % r')(?<!\b'.join(abbrs)) + +def unescape(s): + s = s.replace('>', '>') + s = s.replace('<', '<') + s = s.replace('&', '&') + s = s.replace(' ', ' ') + return s + +def text(html): + html = r_tag.sub('', html) + html = r_whitespace.sub(' ', html) + return unescape(html).strip() + +def search(term): + try: import google + except ImportError, e: + print e + return term + + term = term.replace('_', ' ') + uri = google.google('site:en.wikipedia.org %s' % term) + if uri: + return uri[len('http://en.wikipedia.org/wiki/'):] + else: return term + +def wikipedia(term, last=False): + bytes = web.get(wikiuri % urllib.quote(term)) + bytes = r_tr.sub('', bytes) + + if not last: + r = r_redirect.search(bytes[:4096]) + if r: + term = urllib.unquote(r.group(1)) + return wikipedia(term, last=True) + + paragraphs = r_paragraph.findall(bytes) + + if not paragraphs: + if not last: + term = search(term) + return wikipedia(term, last=True) + return None + + # Pre-process + paragraphs = [para for para in paragraphs + if (para and 'technical limitations' not in para + and 'window.showTocToggle' not in para + and 'Deletion_policy' not in para + and 'Template:AfD_footer' not in para + and not (para.startswith('<p><i>') and + para.endswith('</i></p>')) + and not 'disambiguation)"' in para) + and not '(images and media)' in para + and not 'This article contains a' in para + and not 'id="coordinates"' in para] + + for i, para in enumerate(paragraphs): + para = para.replace('<sup>', '|') + para = para.replace('</sup>', '|') + paragraphs[i] = text(para).strip() + + # Post-process + paragraphs = [para for para in paragraphs if + (para and not (para.endswith(':') and len(para) < 150))] + + para = text(paragraphs[0]) + m = r_sentence.match(para) + + if not m: + if not last: + term = search(term) + return wikipedia(term, last=True) + return None + sentence = m.group(0) + + maxlength = 275 + if len(sentence) > maxlength: + sentence = sentence[:maxlength] + words = sentence[:-5].split(' ') + words.pop() + sentence = ' '.join(words) + ' [...]' + + if ((sentence == 'Wikipedia does not have an article with this exact name.') + or (sentence == 'Wikipedia does not have a page with this exact name.')): + if not last: + term = search(term) + return wikipedia(term, last=True) + return None + + sentence = '"' + sentence.replace('"', "'") + '"' + return sentence + ' - ' + (wikiuri % term) + +def wik(phenny, input): + origterm = input.groups()[1] + term = urllib.unquote(origterm) + if not term: + return phenny.say(origin.sender, 'Maybe you meant ".wik Zen"?') + + term = term[0].upper() + term[1:] + term = term.replace(' ', '_') + + try: result = wikipedia(term) + except IOError: + error = "Can't connect to en.wikipedia.org (%s)" % (wikiuri % term) + return phenny.say(error) + + if result is not None: + phenny.say(result) + else: phenny.say('Can\'t find anything in Wikipedia for "%s".' % origterm) + +wik.commands = ['wik'] +wik.priority = 'high' + +if __name__ == '__main__': + print __doc__.strip() |