summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorSean B. Palmer <http://inamidst.com/sbp/>2008-02-21 12:06:33 +0000
committerSean B. Palmer <http://inamidst.com/sbp/>2008-02-21 12:06:33 +0000
commit7931fab14599b739c18c8f1ebcc24b75688dbc09 (patch)
treebf4df9757f10c155e3b6f78aed48f15884ebbbe6 /modules
downloadbot-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__.py0
-rw-r--r--modules/admin.py53
-rwxr-xr-xmodules/clock.py266
-rw-r--r--modules/codepoints.py89
-rwxr-xr-xmodules/etymology.py102
-rwxr-xr-xmodules/head.py126
-rw-r--r--modules/info.py44
-rwxr-xr-xmodules/ping.py23
-rwxr-xr-xmodules/reload.py34
-rwxr-xr-xmodules/search.py82
-rwxr-xr-xmodules/seen.py49
-rw-r--r--modules/startup.py23
-rwxr-xr-xmodules/tell.py164
-rw-r--r--modules/translate.py102
-rwxr-xr-xmodules/weather.py422
-rw-r--r--modules/wikipedia.py146
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('&gt;', '>')
+ s = s.replace('&lt;', '<')
+ s = s.replace('&amp;', '&')
+ 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('&gt;', '>')
+ s = s.replace('&lt;', '<')
+ s = s.replace('&amp;', '&')
+ s = s.replace('&#160;', ' ')
+ 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()