├── opt ├── __init__.py └── freenode.py ├── modules ├── __init__.py ├── ping.py ├── validate.py ├── seen.py ├── reload.py ├── admin.py ├── startup.py ├── twitter.py ├── info.py ├── wiktionary.py ├── calc.py ├── etymology.py ├── oblique.py ├── codepoints.py ├── translate.py ├── remind.py ├── tell.py ├── wikipedia.py ├── head.py ├── clock.py ├── search.py └── weather.py ├── README.txt ├── project ├── tools.py ├── __init__.py ├── web.py ├── phenny ├── irc.py └── bot.py /opt/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Installation &c. 2 | 3 | 1) Run ./phenny - this creates a default config file 4 | 2) Edit ~/.phenny/default.py 5 | 3) Run ./phenny - this now runs phenny with your settings 6 | 7 | Enjoy! 8 | 9 | -- 10 | Sean B. Palmer, http://inamidst.com/sbp/ 11 | -------------------------------------------------------------------------------- /modules/ping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ping.py - Phenny Ping Module 4 | Author: Sean B. Palmer, inamidst.com 5 | About: http://inamidst.com/phenny/ 6 | """ 7 | 8 | import random 9 | 10 | def hello(phenny, input): 11 | greeting = random.choice(('Hi', 'Hey', 'Hello')) 12 | punctuation = random.choice(('', '!')) 13 | phenny.say(greeting + ' ' + input.nick + punctuation) 14 | hello.rule = r'(?i)(hi|hello|hey) $nickname[ \t]*$' 15 | 16 | def interjection(phenny, input): 17 | phenny.say(input.nick + '!') 18 | interjection.rule = r'$nickname!' 19 | interjection.priority = 'high' 20 | interjection.thread = False 21 | 22 | if __name__ == '__main__': 23 | print __doc__.strip() 24 | -------------------------------------------------------------------------------- /project: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # project 3 | # Copyright 2008, Sean B. Palmer, inamidst.com 4 | # Licensed under the Eiffel Forum License 2. 5 | 6 | # archive - Create phenny.tar.bz2 using git archive 7 | function archive() { 8 | git archive --format=tar --prefix=phenny/ HEAD | bzip2 > phenny.tar.bz2 9 | } 10 | 11 | # commit - Check the code into git and push to github 12 | function commit() { 13 | git commit -a && git push origin master 14 | } 15 | 16 | # history - Show a log of recent updates 17 | function history() { 18 | git log --pretty=oneline --no-merges -10 19 | } 20 | 21 | # help - Show functions in project script 22 | function help() { 23 | egrep '^# [a-z]+ - ' $0 | sed 's/# //' 24 | } 25 | 26 | eval "$1" 27 | -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | tools.py - Phenny Tools 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | def deprecated(old): 11 | def new(phenny, input, old=old): 12 | self = phenny 13 | origin = type('Origin', (object,), { 14 | 'sender': input.sender, 15 | 'nick': input.nick 16 | })() 17 | match = input.match 18 | args = [input.bytes, input.sender, '@@'] 19 | 20 | old(self, origin, match, args) 21 | new.__module__ = old.__module__ 22 | new.__name__ = old.__name__ 23 | return new 24 | 25 | if __name__ == '__main__': 26 | print __doc__.strip() 27 | -------------------------------------------------------------------------------- /modules/validate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | validate.py - Phenny Validation Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import web 11 | 12 | def val(phenny, input): 13 | """Check a webpage using the W3C Markup Validator.""" 14 | if not input.group(2): 15 | return phenny.reply("Nothing to validate.") 16 | uri = input.group(2) 17 | if not uri.startswith('http://'): 18 | uri = 'http://' + uri 19 | 20 | path = '/check?uri=%s;output=xml' % web.urllib.quote(uri) 21 | info = web.head('http://validator.w3.org' + path) 22 | 23 | result = uri + ' is ' 24 | 25 | if isinstance(info, list): 26 | return phenny.say('Got HTTP response %s' % info[1]) 27 | 28 | if info.has_key('X-W3C-Validator-Status'): 29 | result += str(info['X-W3C-Validator-Status']) 30 | if info['X-W3C-Validator-Status'] != 'Valid': 31 | if info.has_key('X-W3C-Validator-Errors'): 32 | n = int(info['X-W3C-Validator-Errors'].split(' ')[0]) 33 | if n != 1: 34 | result += ' (%s errors)' % n 35 | else: result += ' (%s error)' % n 36 | else: result += 'Unvalidatable: no X-W3C-Validator-Status' 37 | 38 | phenny.reply(result) 39 | val.rule = (['val'], r'(?i)(\S+)') 40 | val.example = '.val http://www.w3.org/' 41 | 42 | if __name__ == '__main__': 43 | print __doc__.strip() 44 | -------------------------------------------------------------------------------- /opt/freenode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | freenode.py - Freenode Specific Stuff 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | def replaced(phenny, input): 11 | command = input.group(1) 12 | responses = { 13 | 'cp': '.cp has been replaced by .u', 14 | 'pc': '.pc has been replaced by .u', 15 | 'unicode': '.unicode has been replaced by .u', 16 | 'compare': '.compare has been replaced by .gcs (googlecounts)', 17 | # 'map': 'the .map command has been removed; ask sbp for details', 18 | 'acronym': 'the .acronym command has been removed; ask sbp for details', 19 | # 'img': 'the .img command has been removed; ask sbp for details', 20 | 'v': '.v has been replaced by .val', 21 | 'validate': '.validate has been replaced by .validate', 22 | # 'rates': "moon wanter. moOOoon wanter!", 23 | 'web': 'the .web command has been removed; ask sbp for details', 24 | 'origin': ".origin hasn't been ported to my new codebase yet" 25 | # 'gs': 'sorry, .gs no longer works' 26 | } 27 | try: response = responses[command] 28 | except KeyError: return 29 | else: phenny.reply(response) 30 | replaced.commands = [ 31 | 'cp', 'pc', 'unicode', 'compare', 'map', 'acronym', 32 | 'v', 'validate', 'thesaurus', 'web', 'mangle', 'origin', 33 | 'swhack' 34 | ] 35 | replaced.priority = 'low' 36 | 37 | if __name__ == '__main__': 38 | print __doc__.strip() 39 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | __init__.py - Phenny Init Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import sys, os, time, threading, signal 11 | import bot 12 | 13 | class Watcher(object): 14 | # Cf. http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496735 15 | def __init__(self): 16 | self.child = os.fork() 17 | if self.child != 0: 18 | self.watch() 19 | 20 | def watch(self): 21 | try: os.wait() 22 | except KeyboardInterrupt: 23 | self.kill() 24 | sys.exit() 25 | 26 | def kill(self): 27 | try: os.kill(self.child, signal.SIGKILL) 28 | except OSError: pass 29 | 30 | def run_phenny(config): 31 | if hasattr(config, 'delay'): 32 | delay = config.delay 33 | else: delay = 20 34 | 35 | def connect(config): 36 | p = bot.Phenny(config) 37 | p.run(config.host, config.port) 38 | 39 | try: Watcher() 40 | except Exception, e: 41 | print >> sys.stderr, 'Warning:', e, '(in __init__.py)' 42 | 43 | while True: 44 | try: connect(config) 45 | except KeyboardInterrupt: 46 | sys.exit() 47 | 48 | if not isinstance(delay, int): 49 | break 50 | 51 | warning = 'Warning: Disconnected. Reconnecting in %s seconds...' % delay 52 | print >> sys.stderr, warning 53 | time.sleep(delay) 54 | 55 | def run(config): 56 | t = threading.Thread(target=run_phenny, args=(config,)) 57 | if hasattr(t, 'run'): 58 | t.run() 59 | else: t.start() 60 | 61 | if __name__ == '__main__': 62 | print __doc__ 63 | -------------------------------------------------------------------------------- /modules/seen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | seen.py - Phenny Seen Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import time 11 | from tools import deprecated 12 | 13 | def seen(phenny, input): 14 | """.seen - Reports when was last seen.""" 15 | nick = input.group(2) 16 | if not nick: 17 | return phenny.reply("Need a nickname to search for...") 18 | nick = nick.lower() 19 | 20 | if not hasattr(phenny, 'seen'): 21 | return phenny.reply("?") 22 | 23 | if phenny.seen.has_key(nick): 24 | channel, t = phenny.seen[nick] 25 | t = time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(t)) 26 | 27 | msg = "I last saw %s at %s on %s" % (nick, t, channel) 28 | phenny.reply(msg) 29 | else: phenny.reply("Sorry, I haven't seen %s around." % nick) 30 | seen.rule = (['seen'], r'(\S+)') 31 | 32 | @deprecated 33 | def f_note(self, origin, match, args): 34 | def note(self, origin, match, args): 35 | if not hasattr(self.bot, 'seen'): 36 | self.bot.seen = {} 37 | if origin.sender.startswith('#'): 38 | # if origin.sender == '#inamidst': return 39 | self.seen[origin.nick.lower()] = (origin.sender, time.time()) 40 | 41 | # if not hasattr(self, 'chanspeak'): 42 | # self.chanspeak = {} 43 | # if (len(args) > 2) and args[2].startswith('#'): 44 | # self.chanspeak[args[2]] = args[0] 45 | 46 | try: note(self, origin, match, args) 47 | except Exception, e: print e 48 | f_note.rule = r'(.*)' 49 | f_note.priority = 'low' 50 | 51 | if __name__ == '__main__': 52 | print __doc__.strip() 53 | -------------------------------------------------------------------------------- /modules/reload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | reload.py - Phenny Module Reloader Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import sys, os.path, time, imp 11 | import irc 12 | 13 | def f_reload(phenny, input): 14 | """Reloads a module, for use by admins only.""" 15 | if not input.admin: return 16 | 17 | name = input.group(2) 18 | if name == phenny.config.owner: 19 | return phenny.reply('What?') 20 | 21 | if (not name) or (name == '*'): 22 | phenny.variables = None 23 | phenny.commands = None 24 | phenny.setup() 25 | return phenny.reply('done') 26 | 27 | if not sys.modules.has_key(name): 28 | return phenny.reply('%s: no such module!' % name) 29 | 30 | # Thanks to moot for prodding me on this 31 | path = sys.modules[name].__file__ 32 | if path.endswith('.pyc') or path.endswith('.pyo'): 33 | path = path[:-1] 34 | if not os.path.isfile(path): 35 | return phenny.reply('Found %s, but not the source file' % name) 36 | 37 | module = imp.load_source(name, path) 38 | sys.modules[name] = module 39 | if hasattr(module, 'setup'): 40 | module.setup(phenny) 41 | 42 | mtime = os.path.getmtime(module.__file__) 43 | modified = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(mtime)) 44 | 45 | phenny.register(vars(module)) 46 | phenny.bind_commands() 47 | 48 | phenny.reply('%r (version: %s)' % (module, modified)) 49 | f_reload.name = 'reload' 50 | f_reload.rule = ('$nick', ['reload'], r'(\S+)?') 51 | f_reload.priority = 'low' 52 | f_reload.thread = False 53 | 54 | if __name__ == '__main__': 55 | print __doc__.strip() 56 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | web.py - Web Facilities 4 | Author: Sean B. Palmer, inamidst.com 5 | About: http://inamidst.com/phenny/ 6 | """ 7 | 8 | import re, urllib 9 | from htmlentitydefs import name2codepoint 10 | 11 | class Grab(urllib.URLopener): 12 | def __init__(self, *args): 13 | self.version = 'Mozilla/5.0 (Phenny)' 14 | urllib.URLopener.__init__(self, *args) 15 | def http_error_default(self, url, fp, errcode, errmsg, headers): 16 | return urllib.addinfourl(fp, [headers, errcode], "http:" + url) 17 | urllib._urlopener = Grab() 18 | 19 | def get(uri): 20 | if not uri.startswith('http'): 21 | return 22 | u = urllib.urlopen(uri) 23 | bytes = u.read() 24 | u.close() 25 | return bytes 26 | 27 | def head(uri): 28 | if not uri.startswith('http'): 29 | return 30 | u = urllib.urlopen(uri) 31 | info = u.info() 32 | u.close() 33 | return info 34 | 35 | def post(uri, query): 36 | if not uri.startswith('http'): 37 | return 38 | data = urllib.urlencode(query) 39 | u = urllib.urlopen(uri, data) 40 | bytes = u.read() 41 | u.close() 42 | return bytes 43 | 44 | r_entity = re.compile(r'&([^;\s]+);') 45 | 46 | def entity(match): 47 | value = match.group(1).lower() 48 | if value.startswith('#x'): 49 | return unichr(int(value[2:], 16)) 50 | elif value.startswith('#'): 51 | return unichr(int(value[1:])) 52 | elif name2codepoint.has_key(value): 53 | return unichr(name2codepoint[value]) 54 | return '[' + value + ']' 55 | 56 | def decode(html): 57 | return r_entity.sub(entity, html) 58 | 59 | r_string = re.compile(r'("(\\.|[^"\\])*")') 60 | r_json = re.compile(r'^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$') 61 | env = {'__builtins__': None, 'null': None, 'true': True, 'false': False} 62 | 63 | def json(text): 64 | """Evaluate JSON text safely (we hope).""" 65 | if r_json.match(r_string.sub('', text)): 66 | text = r_string.sub(lambda m: 'u' + m.group(1), text) 67 | return eval(text.strip(' \t\r\n'), env, {}) 68 | raise ValueError('Input must be serialised JSON.') 69 | 70 | if __name__=="__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /modules/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | admin.py - Phenny Admin Module 4 | Copyright 2008-9, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | def join(phenny, input): 11 | """Join the specified channel. This is an admin-only command.""" 12 | # Can only be done in privmsg by an admin 13 | if input.sender.startswith('#'): return 14 | if input.admin: 15 | channel, key = input.group(1), input.group(2) 16 | if not key: 17 | phenny.write(['JOIN'], channel) 18 | else: phenny.write(['JOIN', channel, key]) 19 | join.rule = r'\.join (#\S+)(?: *(\S+))?' 20 | join.priority = 'low' 21 | join.example = '.join #example or .join #example key' 22 | 23 | def part(phenny, input): 24 | """Part the specified channel. This is an admin-only command.""" 25 | # Can only be done in privmsg by an admin 26 | if input.sender.startswith('#'): return 27 | if input.admin: 28 | phenny.write(['PART'], input.group(2)) 29 | part.commands = ['part'] 30 | part.priority = 'low' 31 | part.example = '.part #example' 32 | 33 | def quit(phenny, input): 34 | """Quit from the server. This is an owner-only command.""" 35 | # Can only be done in privmsg by the owner 36 | if input.sender.startswith('#'): return 37 | if input.owner: 38 | phenny.write(['QUIT']) 39 | __import__('os')._exit(0) 40 | quit.commands = ['quit'] 41 | quit.priority = 'low' 42 | 43 | def msg(phenny, input): 44 | # Can only be done in privmsg by an admin 45 | if input.sender.startswith('#'): return 46 | a, b = input.group(2), input.group(3) 47 | if (not a) or (not b): return 48 | if input.admin: 49 | phenny.msg(a, b) 50 | msg.rule = (['msg'], r'(#?\S+) (.+)') 51 | msg.priority = 'low' 52 | 53 | def me(phenny, input): 54 | # Can only be done in privmsg by an admin 55 | if input.sender.startswith('#'): return 56 | if input.admin: 57 | msg = '\x01ACTION %s\x01' % input.group(3) 58 | phenny.msg(input.group(2) or input.sender, msg) 59 | me.rule = (['me'], r'(#?\S+) (.+)') 60 | me.priority = 'low' 61 | 62 | if __name__ == '__main__': 63 | print __doc__.strip() 64 | -------------------------------------------------------------------------------- /modules/startup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | startup.py - Phenny Startup Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import threading, time 11 | 12 | def setup(phenny): 13 | print("Setting up phenny") 14 | # by clsn 15 | phenny.data = {} 16 | refresh_delay = 300.0 17 | 18 | if hasattr(phenny.config, 'refresh_delay'): 19 | try: refresh_delay = float(phenny.config.refresh_delay) 20 | except: pass 21 | 22 | def close(): 23 | print "Nobody PONGed our PING, restarting" 24 | phenny.handle_close() 25 | 26 | def pingloop(): 27 | timer = threading.Timer(refresh_delay, close, ()) 28 | phenny.data['startup.setup.timer'] = timer 29 | phenny.data['startup.setup.timer'].start() 30 | # print "PING!" 31 | phenny.write(('PING', phenny.config.host)) 32 | phenny.data['startup.setup.pingloop'] = pingloop 33 | 34 | def pong(phenny, input): 35 | try: 36 | # print "PONG!" 37 | phenny.data['startup.setup.timer'].cancel() 38 | time.sleep(refresh_delay + 60.0) 39 | pingloop() 40 | except: pass 41 | pong.event = 'PONG' 42 | pong.thread = True 43 | pong.rule = r'.*' 44 | phenny.variables['pong'] = pong 45 | 46 | def startup(phenny, input): 47 | import time 48 | 49 | # Start the ping loop. Has to be done after USER on e.g. quakenet 50 | if phenny.data.get('startup.setup.pingloop'): 51 | phenny.data['startup.setup.pingloop']() 52 | 53 | if hasattr(phenny.config, 'serverpass'): 54 | phenny.write(('PASS', phenny.config.serverpass)) 55 | 56 | if hasattr(phenny.config, 'password'): 57 | phenny.msg('NickServ', 'IDENTIFY %s' % phenny.config.password) 58 | time.sleep(5) 59 | 60 | # Cf. http://swhack.com/logs/2005-12-05#T19-32-36 61 | for channel in phenny.channels: 62 | phenny.write(('JOIN', channel)) 63 | time.sleep(0.5) 64 | startup.rule = r'(.*)' 65 | startup.event = '251' 66 | startup.priority = 'low' 67 | 68 | if __name__ == '__main__': 69 | print __doc__.strip() 70 | -------------------------------------------------------------------------------- /modules/twitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | twitter.py - Phenny Twitter Module 4 | Copyright 2012, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, time 11 | import web 12 | 13 | r_username = re.compile(r'^[a-zA-Z0-9_]{1,15}$') 14 | r_link = re.compile(r'^https?://twitter.com/\S+$') 15 | r_p = re.compile(r'(?ims)(

]+>') 15 | r_ul = re.compile(r'(?ims)

') 16 | 17 | def text(html): 18 | text = r_tag.sub('', html).strip() 19 | text = text.replace('\n', ' ') 20 | text = text.replace('\r', '') 21 | text = text.replace('(intransitive', '(intr.') 22 | text = text.replace('(transitive', '(trans.') 23 | return text 24 | 25 | def wiktionary(word): 26 | bytes = web.get(uri % web.urllib.quote(word.encode('utf-8'))) 27 | bytes = r_ul.sub('', bytes) 28 | 29 | mode = None 30 | etymology = None 31 | definitions = {} 32 | for line in bytes.splitlines(): 33 | if 'id="Etymology"' in line: 34 | mode = 'etymology' 35 | elif 'id="Noun"' in line: 36 | mode = 'noun' 37 | elif 'id="Verb"' in line: 38 | mode = 'verb' 39 | elif 'id="Adjective"' in line: 40 | mode = 'adjective' 41 | elif 'id="Adverb"' in line: 42 | mode = 'adverb' 43 | elif 'id="Interjection"' in line: 44 | mode = 'interjection' 45 | elif 'id="Particle"' in line: 46 | mode = 'particle' 47 | elif 'id="Preposition"' in line: 48 | mode = 'preposition' 49 | elif 'id="' in line: 50 | mode = None 51 | 52 | elif (mode == 'etmyology') and ('

' in line): 53 | etymology = text(line) 54 | elif (mode is not None) and ('

  • ' in line): 55 | definitions.setdefault(mode, []).append(text(line)) 56 | 57 | if ' 300: 90 | result = result[:295] + '[...]' 91 | phenny.say(result) 92 | w.commands = ['w'] 93 | w.example = '.w bailiwick' 94 | 95 | def encarta(phenny, input): 96 | return phenny.reply('Microsoft removed Encarta, try .w instead!') 97 | encarta.commands = ['dict'] 98 | 99 | if __name__ == '__main__': 100 | print __doc__.strip() 101 | -------------------------------------------------------------------------------- /modules/calc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """ 4 | calc.py - Phenny Calculator Module 5 | Copyright 2008, Sean B. Palmer, inamidst.com 6 | Licensed under the Eiffel Forum License 2. 7 | 8 | http://inamidst.com/phenny/ 9 | """ 10 | 11 | import re 12 | import web 13 | 14 | r_result = re.compile(r'(?i)(.*?)') 15 | r_tag = re.compile(r'<\S+.*?>') 16 | 17 | subs = [ 18 | (' in ', ' -> '), 19 | (' over ', ' / '), 20 | (u'£', 'GBP '), 21 | (u'€', 'EUR '), 22 | ('\$', 'USD '), 23 | (r'\bKB\b', 'kilobytes'), 24 | (r'\bMB\b', 'megabytes'), 25 | (r'\bGB\b', 'kilobytes'), 26 | ('kbps', '(kilobits / second)'), 27 | ('mbps', '(megabits / second)') 28 | ] 29 | 30 | def calc(phenny, input): 31 | """Use the Frink online calculator.""" 32 | q = input.group(2) 33 | if not q: 34 | return phenny.say('0?') 35 | 36 | query = q[:] 37 | for a, b in subs: 38 | query = re.sub(a, b, query) 39 | query = query.rstrip(' \t') 40 | 41 | precision = 5 42 | if query[-3:] in ('GBP', 'USD', 'EUR', 'NOK'): 43 | precision = 2 44 | query = web.urllib.quote(query.encode('utf-8')) 45 | 46 | uri = 'http://futureboy.us/fsp/frink.fsp?fromVal=' 47 | bytes = web.get(uri + query) 48 | m = r_result.search(bytes) 49 | if m: 50 | result = m.group(1) 51 | result = r_tag.sub('', result) # strip span.warning tags 52 | result = result.replace('>', '>') 53 | result = result.replace('(undefined symbol)', '(?) ') 54 | 55 | if '.' in result: 56 | try: result = str(round(float(result), precision)) 57 | except ValueError: pass 58 | 59 | if not result.strip(): 60 | result = '?' 61 | elif ' in ' in q: 62 | result += ' ' + q.split(' in ', 1)[1] 63 | 64 | phenny.say(q + ' = ' + result[:350]) 65 | else: phenny.reply("Sorry, can't calculate that.") 66 | phenny.say('Note that .calc is deprecated, consider using .c') 67 | calc.commands = ['calc'] 68 | calc.example = '.calc 5 + 3' 69 | 70 | def c(phenny, input): 71 | """Google calculator.""" 72 | if not input.group(2): 73 | return phenny.reply("Nothing to calculate.") 74 | q = input.group(2).encode('utf-8') 75 | q = q.replace('\xcf\x95', 'phi') # utf-8 U+03D5 76 | q = q.replace('\xcf\x80', 'pi') # utf-8 U+03C0 77 | uri = 'http://www.google.com/ig/calculator?q=' 78 | bytes = web.get(uri + web.urllib.quote(q)) 79 | parts = bytes.split('",') 80 | answer = [p for p in parts if p.startswith('rhs: "')][0][6:] 81 | if answer: 82 | answer = answer.decode('unicode-escape') 83 | answer = ''.join(chr(ord(c)) for c in answer) 84 | answer = answer.decode('utf-8') 85 | answer = answer.replace(u'\xc2\xa0', ',') 86 | answer = answer.replace('', '^(') 87 | answer = answer.replace('', ')') 88 | answer = web.decode(answer) 89 | phenny.say(answer) 90 | else: phenny.say('Sorry, no result.') 91 | c.commands = ['c'] 92 | c.example = '.c 5 + 3' 93 | 94 | def py(phenny, input): 95 | query = input.group(2).encode('utf-8') 96 | uri = 'http://tumbolia.appspot.com/py/' 97 | answer = web.get(uri + web.urllib.quote(query)) 98 | if answer: 99 | phenny.say(answer) 100 | else: phenny.reply('Sorry, no result.') 101 | py.commands = ['py'] 102 | 103 | def wa(phenny, input): 104 | if not input.group(2): 105 | return phenny.reply("No search term.") 106 | query = input.group(2).encode('utf-8') 107 | uri = 'http://tumbolia.appspot.com/wa/' 108 | answer = web.get(uri + web.urllib.quote(query.replace('+', '%2B'))) 109 | if answer: 110 | phenny.say(answer) 111 | else: phenny.reply('Sorry, no result.') 112 | wa.commands = ['wa'] 113 | 114 | if __name__ == '__main__': 115 | print __doc__.strip() 116 | -------------------------------------------------------------------------------- /modules/etymology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | etymology.py - Phenny Etymology Module 4 | Copyright 2007-9, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, urllib 11 | import web 12 | from tools import deprecated 13 | 14 | etysite = 'http://www.etymonline.com/index.php?' 15 | etyuri = etysite + 'allowed_in_frame=0&term=%s' 16 | etysearch = etysite + 'allowed_in_frame=0&search=%s' 17 | 18 | r_definition = re.compile(r'(?ims)]*>.*?') 19 | r_tag = re.compile(r'<(?!!)[^>]+>') 20 | r_whitespace = re.compile(r'[\t\r\n ]+') 21 | 22 | class Grab(urllib.URLopener): 23 | def __init__(self, *args): 24 | self.version = 'Mozilla/5.0 (Phenny)' 25 | urllib.URLopener.__init__(self, *args) 26 | def http_error_default(self, url, fp, errcode, errmsg, headers): 27 | return urllib.addinfourl(fp, [headers, errcode], "http:" + url) 28 | 29 | abbrs = [ 30 | 'cf', 'lit', 'etc', 'Ger', 'Du', 'Skt', 'Rus', 'Eng', 'Amer.Eng', 'Sp', 31 | 'Fr', 'N', 'E', 'S', 'W', 'L', 'Gen', 'J.C', 'dial', 'Gk', 32 | '19c', '18c', '17c', '16c', 'St', 'Capt', 'obs', 'Jan', 'Feb', 'Mar', 33 | 'Apr', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'c', 'tr', 'e', 'g' 34 | ] 35 | t_sentence = r'^.*?(?') 40 | s = s.replace('<', '<') 41 | s = s.replace('&', '&') 42 | return s 43 | 44 | def text(html): 45 | html = r_tag.sub('', html) 46 | html = r_whitespace.sub(' ', html) 47 | return unescape(html).strip() 48 | 49 | def etymology(word): 50 | # @@ sbp, would it be possible to have a flag for .ety to get 2nd/etc 51 | # entries? - http://swhack.com/logs/2006-07-19#T15-05-29 52 | 53 | if len(word) > 25: 54 | raise ValueError("Word too long: %s[...]" % word[:10]) 55 | word = {'axe': 'ax/axe'}.get(word, word) 56 | 57 | grab = urllib._urlopener 58 | urllib._urlopener = Grab() 59 | urllib._urlopener.addheader("Referer", "http://www.etymonline.com/") 60 | bytes = web.get(etyuri % web.urllib.quote(word)) 61 | urllib._urlopener = grab 62 | definitions = r_definition.findall(bytes) 63 | 64 | if not definitions: 65 | return None 66 | 67 | defn = text(definitions[0]) 68 | m = r_sentence.match(defn) 69 | if not m: 70 | return None 71 | sentence = m.group(0) 72 | 73 | # try: 74 | # sentence = unicode(sentence, 'iso-8859-1') 75 | # sentence = sentence.encode('utf-8') 76 | # except: pass 77 | sentence = web.decode(sentence) 78 | 79 | maxlength = 275 80 | if len(sentence) > maxlength: 81 | sentence = sentence[:maxlength] 82 | words = sentence[:-5].split(' ') 83 | words.pop() 84 | sentence = ' '.join(words) + ' [...]' 85 | 86 | sentence = '"' + sentence.replace('"', "'") + '"' 87 | return sentence + ' - ' + ('http://etymonline.com/index.php?term=%s' % web.urllib.quote(word)) 88 | 89 | @deprecated 90 | def f_etymology(self, origin, match, args): 91 | word = match.group(2) 92 | 93 | try: result = etymology(word.encode('iso-8859-1')) 94 | except IOError: 95 | msg = "Can't connect to etymonline.com (%s)" % (etyuri % word) 96 | self.msg(origin.sender, msg) 97 | return 98 | except AttributeError: 99 | result = None 100 | 101 | if result is not None: 102 | self.msg(origin.sender, result) 103 | else: 104 | uri = etysearch % word 105 | msg = 'Can\'t find the etymology for "%s". Try %s' % (word, ('http://etymonline.com/index.php?term=%s' % web.urllib.quote(word))) 106 | self.msg(origin.sender, msg) 107 | # @@ Cf. http://swhack.com/logs/2006-01-04#T01-50-22 108 | f_etymology.rule = (['ety'], r"(.+?)$") 109 | f_etymology.thread = True 110 | f_etymology.priority = 'high' 111 | 112 | if __name__=="__main__": 113 | import sys 114 | print etymology(sys.argv[1]) 115 | -------------------------------------------------------------------------------- /modules/oblique.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | oblique.py - Web Services Interface 4 | Copyright 2008-9, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, urllib 11 | import web 12 | 13 | definitions = 'https://github.com/nslater/oblique/wiki' 14 | 15 | r_item = re.compile(r'(?i)
  • (.*?)
  • ') 16 | r_tag = re.compile(r'<[^>]+>') 17 | 18 | def mappings(uri): 19 | result = {} 20 | bytes = web.get(uri) 21 | for item in r_item.findall(bytes): 22 | item = r_tag.sub('', item).strip(' \t\r\n') 23 | if not ' ' in item: continue 24 | 25 | command, template = item.split(' ', 1) 26 | if not command.isalnum(): continue 27 | if not template.startswith('http://'): continue 28 | result[command] = template.replace('&', '&') 29 | return result 30 | 31 | def service(phenny, input, command, args): 32 | t = o.services[command] 33 | template = t.replace('${args}', urllib.quote(args.encode('utf-8'), '')) 34 | template = template.replace('${nick}', urllib.quote(input.nick, '')) 35 | uri = template.replace('${sender}', urllib.quote(input.sender, '')) 36 | 37 | info = web.head(uri) 38 | if isinstance(info, list): 39 | info = info[0] 40 | if not 'text/plain' in info.get('content-type', '').lower(): 41 | return phenny.reply("Sorry, the service didn't respond in plain text.") 42 | bytes = web.get(uri) 43 | lines = bytes.splitlines() 44 | if not lines: 45 | return phenny.reply("Sorry, the service didn't respond any output.") 46 | try: line = lines[0].encode('utf-8')[:350] 47 | except: line = lines[0][:250] 48 | phenny.say(line) 49 | 50 | def refresh(phenny): 51 | if hasattr(phenny.config, 'services'): 52 | services = phenny.config.services 53 | else: services = definitions 54 | 55 | old = o.services 56 | o.serviceURI = services 57 | o.services = mappings(o.serviceURI) 58 | return len(o.services), set(o.services) - set(old) 59 | 60 | def o(phenny, input): 61 | """Call a webservice.""" 62 | text = input.group(2) 63 | 64 | if (not o.services) or (text == 'refresh'): 65 | length, added = refresh(phenny) 66 | if text == 'refresh': 67 | msg = 'Okay, found %s services.' % length 68 | if added: 69 | msg += ' Added: ' + ', '.join(sorted(added)[:5]) 70 | if len(added) > 5: msg += ', &c.' 71 | return phenny.reply(msg) 72 | 73 | if not text: 74 | return phenny.reply('Try %s for details.' % o.serviceURI) 75 | 76 | if ' ' in text: 77 | command, args = text.split(' ', 1) 78 | else: command, args = text, '' 79 | command = command.lower() 80 | 81 | if command == 'service': 82 | msg = o.services.get(args, 'No such service!') 83 | return phenny.reply(msg) 84 | 85 | if not o.services.has_key(command): 86 | return phenny.reply('Service not found in %s' % o.serviceURI) 87 | 88 | if hasattr(phenny.config, 'external'): 89 | default = phenny.config.external.get('*') 90 | manifest = phenny.config.external.get(input.sender, default) 91 | if manifest: 92 | commands = set(manifest) 93 | if (command not in commands) and (manifest[0] != '!'): 94 | return phenny.reply('Sorry, %s is not whitelisted' % command) 95 | elif (command in commands) and (manifest[0] == '!'): 96 | return phenny.reply('Sorry, %s is blacklisted' % command) 97 | service(phenny, input, command, args) 98 | o.commands = ['o'] 99 | o.example = '.o servicename arg1 arg2 arg3' 100 | o.services = {} 101 | o.serviceURI = None 102 | 103 | def snippet(phenny, input): 104 | if not o.services: 105 | refresh(phenny) 106 | 107 | search = urllib.quote(input.group(2).encode('utf-8')) 108 | py = "BeautifulSoup.BeautifulSoup(re.sub('<.*?>|(?<= ) +', '', " + \ 109 | "''.join(chr(ord(c)) for c in " + \ 110 | "eval(urllib.urlopen('http://ajax.googleapis.com/ajax/serv" + \ 111 | "ices/search/web?v=1.0&q=" + search + "').read()" + \ 112 | ".replace('null', 'None'))['responseData']['resul" + \ 113 | "ts'][0]['content'].decode('unicode-escape')).replace(" + \ 114 | "'"', '\x22')), convertEntities=True)" 115 | service(phenny, input, 'py', py) 116 | snippet.commands = ['snippet'] 117 | 118 | if __name__ == '__main__': 119 | print __doc__.strip() 120 | -------------------------------------------------------------------------------- /modules/codepoints.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | codepoints.py - Phenny Codepoints Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, unicodedata 11 | from itertools import islice 12 | 13 | def about(u, cp=None, name=None): 14 | if cp is None: 15 | cp = ord(u) 16 | if name is None: 17 | try: name = unicodedata.name(u) 18 | except ValueError: 19 | return 'U+%04X (No name found)' % cp 20 | 21 | if not unicodedata.combining(u): 22 | template = 'U+%04X %s (%s)' 23 | else: template = 'U+%04X %s (\xe2\x97\x8c%s)' 24 | return template % (cp, name, u.encode('utf-8')) 25 | 26 | def codepoint_simple(arg): 27 | arg = arg.upper() 28 | 29 | r_label = re.compile('\\b' + arg.replace(' ', '.*\\b') + '\\b') 30 | 31 | results = [] 32 | for cp in xrange(0xFFFF): 33 | u = unichr(cp) 34 | try: name = unicodedata.name(u) 35 | except ValueError: continue 36 | 37 | if r_label.search(name): 38 | results.append((len(name), u, cp, name)) 39 | if not results: 40 | r_label = re.compile('\\b' + arg.replace(' ', '.*\\b')) 41 | for cp in xrange(0xFFFF): 42 | u = unichr(cp) 43 | try: name = unicodedata.name(u) 44 | except ValueError: continue 45 | 46 | if r_label.search(name): 47 | results.append((len(name), u, cp, name)) 48 | 49 | if not results: 50 | return None 51 | 52 | length, u, cp, name = sorted(results)[0] 53 | return about(u, cp, name) 54 | 55 | def codepoint_extended(arg): 56 | arg = arg.upper() 57 | try: r_search = re.compile(arg) 58 | except: raise ValueError('Broken regexp: %r' % arg) 59 | 60 | for cp in xrange(1, 0x10FFFF): 61 | u = unichr(cp) 62 | name = unicodedata.name(u, '-') 63 | 64 | if r_search.search(name): 65 | yield about(u, cp, name) 66 | 67 | def u(phenny, input): 68 | """Look up unicode information.""" 69 | arg = input.bytes[3:] 70 | # phenny.msg('#inamidst', '%r' % arg) 71 | if not arg: 72 | return phenny.reply('You gave me zero length input.') 73 | elif not arg.strip(' '): 74 | if len(arg) > 1: return phenny.reply('%s SPACEs (U+0020)' % len(arg)) 75 | return phenny.reply('1 SPACE (U+0020)') 76 | 77 | # @@ space 78 | if set(arg.upper()) - set( 79 | 'ABCDEFGHIJKLMNOPQRSTUVWYXYZ0123456789- .?+*{}[]\\/^$'): 80 | printable = False 81 | elif len(arg) > 1: 82 | printable = True 83 | else: printable = False 84 | 85 | if printable: 86 | extended = False 87 | for c in '.?+*{}[]\\/^$': 88 | if c in arg: 89 | extended = True 90 | break 91 | 92 | if len(arg) == 4: 93 | try: u = unichr(int(arg, 16)) 94 | except ValueError: pass 95 | else: return phenny.say(about(u)) 96 | 97 | if extended: 98 | # look up a codepoint with regexp 99 | results = list(islice(codepoint_extended(arg), 4)) 100 | for i, result in enumerate(results): 101 | if (i < 2) or ((i == 2) and (len(results) < 4)): 102 | phenny.say(result) 103 | elif (i == 2) and (len(results) > 3): 104 | phenny.say(result + ' [...]') 105 | if not results: 106 | phenny.reply('Sorry, no results') 107 | else: 108 | # look up a codepoint freely 109 | result = codepoint_simple(arg) 110 | if result is not None: 111 | phenny.say(result) 112 | else: phenny.reply("Sorry, no results for %r." % arg) 113 | else: 114 | text = arg.decode('utf-8') 115 | # look up less than three podecoints 116 | if len(text) <= 3: 117 | for u in text: 118 | phenny.say(about(u)) 119 | # look up more than three podecoints 120 | elif len(text) <= 10: 121 | phenny.reply(' '.join('U+%04X' % ord(c) for c in text)) 122 | else: phenny.reply('Sorry, your input is too long!') 123 | u.commands = ['u'] 124 | u.example = '.u 203D' 125 | 126 | def bytes(phenny, input): 127 | """Show the input as pretty printed bytes.""" 128 | b = input.bytes 129 | phenny.reply('%r' % b[b.find(' ') + 1:]) 130 | bytes.commands = ['bytes'] 131 | bytes.example = '.bytes \xe3\x8b\xa1' 132 | 133 | if __name__ == '__main__': 134 | print __doc__.strip() 135 | -------------------------------------------------------------------------------- /modules/translate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """ 4 | translate.py - Phenny Translation Module 5 | Copyright 2008, Sean B. Palmer, inamidst.com 6 | Licensed under the Eiffel Forum License 2. 7 | 8 | http://inamidst.com/phenny/ 9 | """ 10 | 11 | import re, urllib 12 | import web 13 | 14 | def translate(text, input='auto', output='en'): 15 | raw = False 16 | if output.endswith('-raw'): 17 | output = output[:-4] 18 | raw = True 19 | 20 | import urllib2, json 21 | opener = urllib2.build_opener() 22 | opener.addheaders = [( 23 | 'User-Agent', 'Mozilla/5.0' + 24 | '(X11; U; Linux i686)' + 25 | 'Gecko/20071127 Firefox/2.0.0.11' 26 | )] 27 | 28 | input, output = urllib.quote(input), urllib.quote(output) 29 | text = urllib.quote(text) 30 | 31 | result = opener.open('http://translate.google.com/translate_a/t?' + 32 | ('client=t&hl=en&sl=%s&tl=%s&multires=1' % (input, output)) + 33 | ('&otf=1&ssel=0&tsel=0&uptl=en&sc=1&text=%s' % text)).read() 34 | 35 | while ',,' in result: 36 | result = result.replace(',,', ',null,') 37 | data = json.loads(result) 38 | 39 | if raw: 40 | return str(data), 'en-raw' 41 | 42 | try: language = data[2] # -2][0][0] 43 | except: language = '?' 44 | 45 | return ''.join(x[0] for x in data[0]), language 46 | 47 | def tr(phenny, context): 48 | """Translates a phrase, with an optional language hint.""" 49 | input, output, phrase = context.groups() 50 | 51 | phrase = phrase.encode('utf-8') 52 | 53 | if (len(phrase) > 350) and (not context.admin): 54 | return phenny.reply('Phrase must be under 350 characters.') 55 | 56 | input = input or 'auto' 57 | input = input.encode('utf-8') 58 | output = (output or 'en').encode('utf-8') 59 | 60 | if input != output: 61 | msg, input = translate(phrase, input, output) 62 | if isinstance(msg, str): 63 | msg = msg.decode('utf-8') 64 | if msg: 65 | msg = web.decode(msg) # msg.replace(''', "'") 66 | msg = '"%s" (%s to %s, translate.google.com)' % (msg, input, output) 67 | else: msg = 'The %s to %s translation failed, sorry!' % (input, output) 68 | 69 | phenny.reply(msg) 70 | else: phenny.reply('Language guessing failed, so try suggesting one!') 71 | 72 | tr.rule = ('$nick', ur'(?:([a-z]{2}) +)?(?:([a-z]{2}|en-raw) +)?["“](.+?)["”]\? *$') 73 | tr.example = '$nickname: "mon chien"? or $nickname: fr "mon chien"?' 74 | tr.priority = 'low' 75 | 76 | def tr2(phenny, input): 77 | """Translates a phrase, with an optional language hint.""" 78 | command = input.group(2) 79 | if not command: 80 | return phenny.reply("Need something to translate!") 81 | command = command.encode('utf-8') 82 | 83 | def langcode(p): 84 | return p.startswith(':') and (2 < len(p) < 10) and p[1:].isalpha() 85 | 86 | args = ['auto', 'en'] 87 | 88 | for i in xrange(2): 89 | if not ' ' in command: break 90 | prefix, cmd = command.split(' ', 1) 91 | if langcode(prefix): 92 | args[i] = prefix[1:] 93 | command = cmd 94 | phrase = command 95 | 96 | # if (len(phrase) > 350) and (not input.admin): 97 | # return phenny.reply('Phrase must be under 350 characters.') 98 | 99 | src, dest = args 100 | if src != dest: 101 | msg, src = translate(phrase, src, dest) 102 | if isinstance(msg, str): 103 | msg = msg.decode('utf-8') 104 | if msg: 105 | msg = web.decode(msg) # msg.replace(''', "'") 106 | if len(msg) > 450: msg = msg[:450] + '[...]' 107 | msg = '"%s" (%s to %s, translate.google.com)' % (msg, src, dest) 108 | else: msg = 'The %s to %s translation failed, sorry!' % (src, dest) 109 | 110 | phenny.reply(msg) 111 | else: phenny.reply('Language guessing failed, so try suggesting one!') 112 | 113 | tr2.commands = ['tr'] 114 | tr2.priority = 'low' 115 | 116 | def mangle(phenny, input): 117 | import time 118 | 119 | phrase = input.group(2).encode('utf-8') 120 | for lang in ['fr', 'de', 'es', 'it', 'ja']: 121 | backup = phrase[:] 122 | phrase, _lang = translate(phrase, 'en', lang) 123 | phrase = phrase.encode("utf-8") 124 | 125 | if not phrase: 126 | phrase = backup[:] 127 | break 128 | time.sleep(0.25) 129 | 130 | backup = phrase[:] 131 | phrase, _lang = translate(phrase, lang, 'en') 132 | phrase = phrase.encode("utf-8") 133 | 134 | if not phrase: 135 | phrase = backup[:] 136 | break 137 | time.sleep(0.25) 138 | 139 | phrase = phrase.replace(' ,', ',').replace(' .', '.') 140 | phrase = phrase.strip(' ,') 141 | phenny.reply(phrase or 'ERRORS SRY') 142 | mangle.commands = ['mangle'] 143 | 144 | if __name__ == '__main__': 145 | print __doc__.strip() 146 | -------------------------------------------------------------------------------- /phenny: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | phenny - An IRC Bot 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | 9 | Note: DO NOT EDIT THIS FILE. 10 | Run ./phenny, then edit ~/.phenny/default.py 11 | Then run ./phenny again 12 | """ 13 | 14 | import sys, os, imp, optparse 15 | from textwrap import dedent as trim 16 | 17 | dotdir = os.path.expanduser('~/.phenny') 18 | 19 | def check_python_version(): 20 | if sys.version_info < (2, 4): 21 | error = 'Error: Requires Python 2.4 or later, from www.python.org' 22 | print >> sys.stderr, error 23 | sys.exit(1) 24 | 25 | def create_default_config(fn): 26 | f = open(fn, 'w') 27 | print >> f, trim("""\ 28 | nick = 'phenny' 29 | host = 'irc.example.net' 30 | channels = ['#example', '#test'] 31 | owner = 'yournickname' 32 | 33 | # password is the NickServ password, serverpass is the server password 34 | # password = 'example' 35 | # serverpass = 'serverpass' 36 | 37 | # These are people who will be able to use admin.py's functions... 38 | admins = [owner, 'someoneyoutrust'] 39 | # But admin.py is disabled by default, as follows: 40 | exclude = ['admin'] 41 | 42 | # If you want to enumerate a list of modules rather than disabling 43 | # some, use "enable = ['example']", which takes precedent over exclude 44 | # 45 | # enable = [] 46 | 47 | # Directories to load user modules from 48 | # e.g. /path/to/my/modules 49 | extra = [] 50 | 51 | # Services to load: maps channel names to white or black lists 52 | external = { 53 | '#liberal': ['!'], # allow all 54 | '#conservative': [], # allow none 55 | '*': ['!'] # default whitelist, allow all 56 | } 57 | 58 | # EOF 59 | """) 60 | f.close() 61 | 62 | def create_default_config_file(dotdir): 63 | print 'Creating a default config file at ~/.phenny/default.py...' 64 | default = os.path.join(dotdir, 'default.py') 65 | create_default_config(default) 66 | 67 | print 'Done; now you can edit default.py, and run phenny! Enjoy.' 68 | sys.exit(0) 69 | 70 | def create_dotdir(dotdir): 71 | print 'Creating a config directory at ~/.phenny...' 72 | try: os.mkdir(dotdir) 73 | except Exception, e: 74 | print >> sys.stderr, 'There was a problem creating %s:' % dotdir 75 | print >> sys.stderr, e.__class__, str(e) 76 | print >> sys.stderr, 'Please fix this and then run phenny again.' 77 | sys.exit(1) 78 | 79 | create_default_config_file(dotdir) 80 | 81 | def check_dotdir(): 82 | default = os.path.join(dotdir, 'default.py') 83 | 84 | if not os.path.isdir(dotdir): 85 | create_dotdir(dotdir) 86 | elif not os.path.isfile(default): 87 | create_default_config_file(dotdir) 88 | 89 | def config_names(config): 90 | config = config or 'default' 91 | 92 | def files(d): 93 | names = os.listdir(d) 94 | return list(os.path.join(d, fn) for fn in names if fn.endswith('.py')) 95 | 96 | here = os.path.join('.', config) 97 | if os.path.isfile(here): 98 | return [here] 99 | if os.path.isfile(here + '.py'): 100 | return [here + '.py'] 101 | if os.path.isdir(here): 102 | return files(here) 103 | 104 | there = os.path.join(dotdir, config) 105 | if os.path.isfile(there): 106 | return [there] 107 | if os.path.isfile(there + '.py'): 108 | return [there + '.py'] 109 | if os.path.isdir(there): 110 | return files(there) 111 | 112 | print >> sys.stderr, "Error: Couldn't find a config file!" 113 | print >> sys.stderr, 'What happened to ~/.phenny/default.py?' 114 | sys.exit(1) 115 | 116 | def main(argv=None): 117 | # Step One: Parse The Command Line 118 | 119 | parser = optparse.OptionParser('%prog [options]') 120 | parser.add_option('-c', '--config', metavar='fn', 121 | help='use this configuration file or directory') 122 | opts, args = parser.parse_args(argv) 123 | if args: print >> sys.stderr, 'Warning: ignoring spurious arguments' 124 | 125 | # Step Two: Check Dependencies 126 | 127 | check_python_version() # require python2.4 or later 128 | if not opts.config: 129 | check_dotdir() # require ~/.phenny, or make it and exit 130 | 131 | # Step Three: Load The Configurations 132 | 133 | config_modules = [] 134 | for config_name in config_names(opts.config): 135 | name = os.path.basename(config_name).split('.')[0] + '_config' 136 | module = imp.load_source(name, config_name) 137 | module.filename = config_name 138 | 139 | if not hasattr(module, 'prefix'): 140 | module.prefix = r'\.' 141 | 142 | if not hasattr(module, 'name'): 143 | module.name = 'Phenny Palmersbot, http://inamidst.com/phenny/' 144 | 145 | if not hasattr(module, 'port'): 146 | module.port = 6667 147 | 148 | if not hasattr(module, 'password'): 149 | module.password = None 150 | 151 | if module.host == 'irc.example.net': 152 | error = ('Error: you must edit the config file first!\n' + 153 | "You're currently using %s" % module.filename) 154 | print >> sys.stderr, error 155 | sys.exit(1) 156 | 157 | config_modules.append(module) 158 | 159 | # Step Four: Load Phenny 160 | 161 | try: from __init__ import run 162 | except ImportError: 163 | try: from phenny import run 164 | except ImportError: 165 | print >> sys.stderr, "Error: Couldn't find phenny to import" 166 | sys.exit(1) 167 | 168 | # Step Five: Initialise And Run The Phennies 169 | 170 | # @@ ignore SIGHUP 171 | for config_module in config_modules: 172 | run(config_module) # @@ thread this 173 | 174 | if __name__ == '__main__': 175 | main() 176 | -------------------------------------------------------------------------------- /modules/remind.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | remind.py - Phenny Reminder Module 4 | Copyright 2011, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import os, re, time, threading 11 | 12 | def filename(self): 13 | name = self.nick + '-' + self.config.host + '.reminders.db' 14 | return os.path.join(os.path.expanduser('~/.phenny'), name) 15 | 16 | def load_database(name): 17 | data = {} 18 | if os.path.isfile(name): 19 | f = open(name, 'rb') 20 | for line in f: 21 | unixtime, channel, nick, message = line.split('\t') 22 | message = message.rstrip('\n') 23 | t = int(unixtime) 24 | reminder = (channel, nick, message) 25 | try: data[t].append(reminder) 26 | except KeyError: data[t] = [reminder] 27 | f.close() 28 | return data 29 | 30 | def dump_database(name, data): 31 | f = open(name, 'wb') 32 | for unixtime, reminders in data.iteritems(): 33 | for channel, nick, message in reminders: 34 | f.write('%s\t%s\t%s\t%s\n' % (unixtime, channel, nick, message)) 35 | f.close() 36 | 37 | def setup(phenny): 38 | phenny.rfn = filename(phenny) 39 | 40 | # phenny.sending.acquire() 41 | phenny.rdb = load_database(phenny.rfn) 42 | # phenny.sending.release() 43 | 44 | def monitor(phenny): 45 | time.sleep(5) 46 | while True: 47 | now = int(time.time()) 48 | unixtimes = [int(key) for key in phenny.rdb] 49 | oldtimes = [t for t in unixtimes if t <= now] 50 | if oldtimes: 51 | for oldtime in oldtimes: 52 | for (channel, nick, message) in phenny.rdb[oldtime]: 53 | if message: 54 | phenny.msg(channel, nick + ': ' + message) 55 | else: phenny.msg(channel, nick + '!') 56 | del phenny.rdb[oldtime] 57 | 58 | # phenny.sending.acquire() 59 | dump_database(phenny.rfn, phenny.rdb) 60 | # phenny.sending.release() 61 | time.sleep(2.5) 62 | 63 | targs = (phenny,) 64 | t = threading.Thread(target=monitor, args=targs) 65 | t.start() 66 | 67 | scaling = { 68 | 'years': 365.25 * 24 * 3600, 69 | 'year': 365.25 * 24 * 3600, 70 | 'yrs': 365.25 * 24 * 3600, 71 | 'y': 365.25 * 24 * 3600, 72 | 73 | 'months': 29.53059 * 24 * 3600, 74 | 'month': 29.53059 * 24 * 3600, 75 | 'mo': 29.53059 * 24 * 3600, 76 | 77 | 'weeks': 7 * 24 * 3600, 78 | 'week': 7 * 24 * 3600, 79 | 'wks': 7 * 24 * 3600, 80 | 'wk': 7 * 24 * 3600, 81 | 'w': 7 * 24 * 3600, 82 | 83 | 'days': 24 * 3600, 84 | 'day': 24 * 3600, 85 | 'd': 24 * 3600, 86 | 87 | 'hours': 3600, 88 | 'hour': 3600, 89 | 'hrs': 3600, 90 | 'hr': 3600, 91 | 'h': 3600, 92 | 93 | 'minutes': 60, 94 | 'minute': 60, 95 | 'mins': 60, 96 | 'min': 60, 97 | 'm': 60, 98 | 99 | 'seconds': 1, 100 | 'second': 1, 101 | 'secs': 1, 102 | 'sec': 1, 103 | 's': 1 104 | } 105 | 106 | periods = '|'.join(scaling.keys()) 107 | p_command = r'\.in ([0-9]+(?:\.[0-9]+)?)\s?((?:%s)\b)?:?\s?(.*)' % periods 108 | r_command = re.compile(p_command) 109 | 110 | def remind(phenny, input): 111 | m = r_command.match(input.bytes) 112 | if not m: 113 | return phenny.reply("Sorry, didn't understand the input.") 114 | length, scale, message = m.groups() 115 | 116 | length = float(length) 117 | factor = scaling.get(scale, 60) 118 | duration = length * factor 119 | 120 | if duration % 1: 121 | duration = int(duration) + 1 122 | else: duration = int(duration) 123 | 124 | t = int(time.time()) + duration 125 | reminder = (input.sender, input.nick, message) 126 | 127 | try: phenny.rdb[t].append(reminder) 128 | except KeyError: phenny.rdb[t] = [reminder] 129 | 130 | dump_database(phenny.rfn, phenny.rdb) 131 | 132 | if duration >= 60: 133 | w = '' 134 | if duration >= 3600 * 12: 135 | w += time.strftime(' on %d %b %Y', time.gmtime(t)) 136 | w += time.strftime(' at %H:%MZ', time.gmtime(t)) 137 | phenny.reply('Okay, will remind%s' % w) 138 | else: phenny.reply('Okay, will remind in %s secs' % duration) 139 | remind.commands = ['in'] 140 | 141 | r_time = re.compile(r'^([0-9]{2}[:.][0-9]{2})') 142 | r_zone = re.compile(r'( ?([A-Za-z]+|[+-]\d\d?))') 143 | 144 | import calendar 145 | from clock import TimeZones 146 | 147 | def at(phenny, input): 148 | bytes = input[4:] 149 | 150 | m = r_time.match(bytes) 151 | if not m: 152 | return phenny.reply("Sorry, didn't understand the time spec.") 153 | t = m.group(1).replace('.', ':') 154 | bytes = bytes[len(t):] 155 | 156 | m = r_zone.match(bytes) 157 | if not m: 158 | return phenny.reply("Sorry, didn't understand the zone spec.") 159 | z = m.group(2) 160 | bytes = bytes[len(m.group(1)):].strip().encode("utf-8") 161 | 162 | if z.startswith('+') or z.startswith('-'): 163 | tz = int(z) 164 | 165 | if TimeZones.has_key(z): 166 | tz = TimeZones[z] 167 | else: return phenny.reply("Sorry, didn't understand the time zone.") 168 | 169 | d = time.strftime("%Y-%m-%d", time.gmtime()) 170 | d = time.strptime(("%s %s" % (d, t)).encode("utf-8"), "%Y-%m-%d %H:%M") 171 | 172 | d = int(calendar.timegm(d) - (3600 * tz)) 173 | duration = int((d - time.time()) / 60) 174 | 175 | if duration < 1: 176 | return phenny.reply("Sorry, that date is this minute or in the past. And only times in the same day are supported!") 177 | 178 | # phenny.say("%s %s %s" % (t, tz, d)) 179 | 180 | reminder = (input.sender, input.nick, bytes) 181 | # phenny.say(str((d, reminder))) 182 | try: phenny.rdb[d].append(reminder) 183 | except KeyError: phenny.rdb[d] = [reminder] 184 | 185 | phenny.sending.acquire() 186 | dump_database(phenny.rfn, phenny.rdb) 187 | phenny.sending.release() 188 | 189 | phenny.reply("Reminding at %s %s - in %s minute(s)" % (t, z, duration)) 190 | at.commands = ['at'] 191 | 192 | if __name__ == '__main__': 193 | print __doc__.strip() 194 | -------------------------------------------------------------------------------- /modules/tell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | tell.py - Phenny Tell and Ask Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import os, re, time, random 11 | import web 12 | 13 | maximum = 4 14 | lispchannels = frozenset([ '#lisp', '#scheme', '#opendarwin', '#macdev', 15 | '#fink', '#jedit', '#dylan', '#emacs', '#xemacs', '#colloquy', '#adium', 16 | '#growl', '#chicken', '#quicksilver', '#svn', '#slate', '#squeak', '#wiki', 17 | '#nebula', '#myko', '#lisppaste', '#pearpc', '#fpc', '#hprog', 18 | '#concatenative', '#slate-users', '#swhack', '#ud', '#t', '#compilers', 19 | '#erights', '#esp', '#scsh', '#sisc', '#haskell', '#rhype', '#sicp', '#darcs', 20 | '#hardcider', '#lisp-it', '#webkit', '#launchd', '#mudwalker', '#darwinports', 21 | '#muse', '#chatkit', '#kowaleba', '#vectorprogramming', '#opensolaris', 22 | '#oscar-cluster', '#ledger', '#cairo', '#idevgames', '#hug-bunny', '##parsers', 23 | '#perl6', '#sdlperl', '#ksvg', '#rcirc', '#code4lib', '#linux-quebec', 24 | '#programmering', '#maxima', '#robin', '##concurrency', '#paredit' ]) 25 | 26 | def loadReminders(fn): 27 | result = {} 28 | f = open(fn) 29 | for line in f: 30 | line = line.strip() 31 | if line: 32 | try: tellee, teller, verb, timenow, msg = line.split('\t', 4) 33 | except ValueError: continue # @@ hmm 34 | result.setdefault(tellee, []).append((teller, verb, timenow, msg)) 35 | f.close() 36 | return result 37 | 38 | def dumpReminders(fn, data): 39 | f = open(fn, 'w') 40 | for tellee in data.iterkeys(): 41 | for remindon in data[tellee]: 42 | line = '\t'.join((tellee,) + remindon) 43 | try: f.write(line + '\n') 44 | except IOError: break 45 | try: f.close() 46 | except IOError: pass 47 | return True 48 | 49 | def setup(self): 50 | fn = self.nick + '-' + self.config.host + '.tell.db' 51 | self.tell_filename = os.path.join(os.path.expanduser('~/.phenny'), fn) 52 | if not os.path.exists(self.tell_filename): 53 | try: f = open(self.tell_filename, 'w') 54 | except OSError: pass 55 | else: 56 | f.write('') 57 | f.close() 58 | self.reminders = loadReminders(self.tell_filename) # @@ tell 59 | 60 | def f_remind(phenny, input): 61 | teller = input.nick 62 | 63 | # @@ Multiple comma-separated tellees? Cf. Terje, #swhack, 2006-04-15 64 | verb, tellee, msg = input.groups() 65 | verb = verb.encode('utf-8') 66 | tellee = tellee.encode('utf-8') 67 | msg = msg.encode('utf-8') 68 | 69 | tellee_original = tellee.rstrip('.,:;') 70 | tellee = tellee_original.lower() 71 | 72 | if not os.path.exists(phenny.tell_filename): 73 | return 74 | 75 | if len(tellee) > 20: 76 | return phenny.reply('That nickname is too long.') 77 | 78 | timenow = time.strftime('%d %b %H:%MZ', time.gmtime()) 79 | if not tellee in (teller.lower(), phenny.nick, 'me'): # @@ 80 | # @@ and year, if necessary 81 | warn = False 82 | if not phenny.reminders.has_key(tellee): 83 | phenny.reminders[tellee] = [(teller, verb, timenow, msg)] 84 | else: 85 | # if len(phenny.reminders[tellee]) >= maximum: 86 | # warn = True 87 | phenny.reminders[tellee].append((teller, verb, timenow, msg)) 88 | # @@ Stephanie's augmentation 89 | response = "I'll pass that on when %s is around." % tellee_original 90 | # if warn: response += (" I'll have to use a pastebin, though, so " + 91 | # "your message may get lost.") 92 | 93 | rand = random.random() 94 | if rand > 0.9999: response = "yeah, yeah" 95 | elif rand > 0.999: response = "yeah, sure, whatever" 96 | 97 | phenny.reply(response) 98 | elif teller.lower() == tellee: 99 | phenny.say('You can %s yourself that.' % verb) 100 | else: phenny.say("Hey, I'm not as stupid as Monty you know!") 101 | 102 | dumpReminders(phenny.tell_filename, phenny.reminders) # @@ tell 103 | f_remind.rule = ('$nick', ['tell', 'ask'], r'(\S+) (.*)') 104 | 105 | def getReminders(phenny, channel, key, tellee): 106 | lines = [] 107 | template = "%s: %s <%s> %s %s %s" 108 | today = time.strftime('%d %b', time.gmtime()) 109 | 110 | for (teller, verb, datetime, msg) in phenny.reminders[key]: 111 | if datetime.startswith(today): 112 | datetime = datetime[len(today)+1:] 113 | lines.append(template % (tellee, datetime, teller, verb, tellee, msg)) 114 | 115 | try: del phenny.reminders[key] 116 | except KeyError: phenny.msg(channel, 'Er...') 117 | return lines 118 | 119 | def message(phenny, input): 120 | if not input.sender.startswith('#'): return 121 | 122 | tellee = input.nick 123 | channel = input.sender 124 | 125 | if not os: return 126 | if not os.path.exists(phenny.tell_filename): 127 | return 128 | 129 | reminders = [] 130 | remkeys = list(reversed(sorted(phenny.reminders.keys()))) 131 | for remkey in remkeys: 132 | if not remkey.endswith('*') or remkey.endswith(':'): 133 | if tellee.lower() == remkey: 134 | phenny.sending.acquire() 135 | reminders.extend(getReminders(phenny, channel, remkey, tellee)) 136 | phenny.sending.release() 137 | elif tellee.lower().strip("0123456789_-[]`") == remkey.rstrip('*:'): 138 | phenny.sending.acquire() 139 | reminders.extend(getReminders(phenny, channel, remkey, tellee)) 140 | phenny.sending.release() 141 | 142 | for line in reminders[:maximum]: 143 | phenny.say(line) 144 | 145 | if reminders[maximum:]: 146 | phenny.say('Further messages sent privately') 147 | for line in reminders[maximum:]: 148 | phenny.msg(tellee, line) 149 | 150 | if len(phenny.reminders.keys()) != remkeys: 151 | phenny.sending.acquire() 152 | dumpReminders(phenny.tell_filename, phenny.reminders) # @@ tell 153 | phenny.sending.release() 154 | message.rule = r'(.*)' 155 | message.priority = 'low' 156 | 157 | if __name__ == '__main__': 158 | print __doc__.strip() 159 | -------------------------------------------------------------------------------- /modules/wikipedia.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | wikipedia.py - Phenny Wikipedia Module 4 | Copyright 2008-9, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, urllib, gzip, StringIO 11 | import web 12 | 13 | wikiuri = 'http://%s.wikipedia.org/wiki/%s' 14 | # wikisearch = 'http://%s.wikipedia.org/wiki/Special:Search?' \ 15 | # + 'search=%s&fulltext=Search' 16 | 17 | r_tr = re.compile(r'(?ims)]*>.*?') 18 | r_paragraph = re.compile(r'(?ims)]*>.*?

    |]*>.*?') 19 | r_tag = re.compile(r'<(?!!)[^>]+>') 20 | r_whitespace = re.compile(r'[\t\r\n ]+') 21 | r_redirect = re.compile( 22 | r'(?ims)class=.redirectText.>\s*') 35 | s = s.replace('<', '<') 36 | s = s.replace('&', '&') 37 | s = s.replace(' ', ' ') 38 | return s 39 | 40 | def text(html): 41 | html = r_tag.sub('', html) 42 | html = r_whitespace.sub(' ', html) 43 | return unescape(html).strip() 44 | 45 | def search(term): 46 | try: import search 47 | except ImportError, e: 48 | print e 49 | return term 50 | 51 | if isinstance(term, unicode): 52 | term = term.encode('utf-8') 53 | else: term = term.decode('utf-8') 54 | 55 | term = term.replace('_', ' ') 56 | try: uri = search.google_search('site:en.wikipedia.org %s' % term) 57 | except IndexError: return term 58 | if uri: 59 | return uri[len('http://en.wikipedia.org/wiki/'):] 60 | else: return term 61 | 62 | def wikipedia(term, language='en', last=False): 63 | global wikiuri 64 | if not '%' in term: 65 | if isinstance(term, unicode): 66 | t = term.encode('utf-8') 67 | else: t = term 68 | q = urllib.quote(t) 69 | u = wikiuri % (language, q) 70 | bytes = web.get(u) 71 | else: bytes = web.get(wikiuri % (language, term)) 72 | 73 | if bytes.startswith('\x1f\x8b\x08\x00\x00\x00\x00\x00'): 74 | f = StringIO.StringIO(bytes) 75 | f.seek(0) 76 | gzip_file = gzip.GzipFile(fileobj=f) 77 | bytes = gzip_file.read() 78 | gzip_file.close() 79 | f.close() 80 | 81 | bytes = r_tr.sub('', bytes) 82 | 83 | if not last: 84 | r = r_redirect.search(bytes[:4096]) 85 | if r: 86 | term = urllib.unquote(r.group(1)) 87 | return wikipedia(term, language=language, last=True) 88 | 89 | paragraphs = r_paragraph.findall(bytes) 90 | 91 | if not paragraphs: 92 | if not last: 93 | term = search(term) 94 | return wikipedia(term, language=language, last=True) 95 | return None 96 | 97 | # Pre-process 98 | paragraphs = [para for para in paragraphs 99 | if (para and 'technical limitations' not in para 100 | and 'window.showTocToggle' not in para 101 | and 'Deletion_policy' not in para 102 | and 'Template:AfD_footer' not in para 103 | and not (para.startswith('

    ') and 104 | para.endswith('

    ')) 105 | and not 'disambiguation)"' in para) 106 | and not '(images and media)' in para 107 | and not 'This article contains a' in para 108 | and not 'id="coordinates"' in para 109 | and not 'class="thumb' in para] 110 | # and not 'style="display:none"' in para] 111 | 112 | for i, para in enumerate(paragraphs): 113 | para = para.replace('', '|') 114 | para = para.replace('', '|') 115 | paragraphs[i] = text(para).strip() 116 | 117 | # Post-process 118 | paragraphs = [para for para in paragraphs if 119 | (para and not (para.endswith(':') and len(para) < 150))] 120 | 121 | para = text(paragraphs[0]) 122 | m = r_sentence.match(para) 123 | 124 | if not m: 125 | if not last: 126 | term = search(term) 127 | return wikipedia(term, language=language, last=True) 128 | return None 129 | sentence = m.group(0) 130 | 131 | maxlength = 275 132 | if len(sentence) > maxlength: 133 | sentence = sentence[:maxlength] 134 | words = sentence[:-5].split(' ') 135 | words.pop() 136 | sentence = ' '.join(words) + ' [...]' 137 | 138 | if (('using the Article Wizard if you wish' in sentence) 139 | or ('or add a request for it' in sentence) 140 | or ('in existing articles' in sentence)): 141 | if not last: 142 | term = search(term) 143 | return wikipedia(term, language=language, last=True) 144 | return None 145 | 146 | sentence = '"' + sentence.replace('"', "'") + '"' 147 | sentence = sentence.decode('utf-8').encode('utf-8') 148 | wikiuri = wikiuri.decode('utf-8').encode('utf-8') 149 | term = term.decode('utf-8').encode('utf-8') 150 | return sentence + ' - ' + (wikiuri % (language, term)) 151 | 152 | def wik(phenny, input): 153 | origterm = input.groups()[1] 154 | if not origterm: 155 | return phenny.say('Perhaps you meant ".wik Zen"?') 156 | origterm = origterm.encode('utf-8') 157 | 158 | term = urllib.unquote(origterm) 159 | language = 'en' 160 | if term.startswith(':') and (' ' in term): 161 | a, b = term.split(' ', 1) 162 | a = a.lstrip(':') 163 | if a.isalpha(): 164 | language, term = a, b 165 | term = term[0].upper() + term[1:] 166 | term = term.replace(' ', '_') 167 | 168 | try: result = wikipedia(term, language) 169 | except IOError: 170 | args = (language, wikiuri % (language, term)) 171 | error = "Can't connect to %s.wikipedia.org (%s)" % args 172 | return phenny.say(error) 173 | 174 | if result is not None: 175 | phenny.say(result) 176 | else: phenny.say('Can\'t find anything in Wikipedia for "%s".' % origterm) 177 | 178 | wik.commands = ['wik'] 179 | wik.priority = 'high' 180 | 181 | if __name__ == '__main__': 182 | print __doc__.strip() 183 | -------------------------------------------------------------------------------- /modules/head.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | head.py - Phenny HTTP Metadata Utilities 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, urllib, urllib2, httplib, urlparse, time 11 | from htmlentitydefs import name2codepoint 12 | import web 13 | from tools import deprecated 14 | 15 | def head(phenny, input): 16 | """Provide HTTP HEAD information.""" 17 | uri = input.group(2) 18 | uri = (uri or '').encode('utf-8') 19 | if ' ' in uri: 20 | uri, header = uri.rsplit(' ', 1) 21 | else: uri, header = uri, None 22 | 23 | if not uri and hasattr(phenny, 'last_seen_uri'): 24 | try: uri = phenny.last_seen_uri[input.sender] 25 | except KeyError: return phenny.say('?') 26 | 27 | if not uri.startswith('htt'): 28 | uri = 'http://' + uri 29 | # uri = uri.replace('#!', '?_escaped_fragment_=') 30 | 31 | try: info = web.head(uri) 32 | except IOError: return phenny.say("Can't connect to %s" % uri) 33 | except httplib.InvalidURL: return phenny.say("Not a valid URI, sorry.") 34 | 35 | if not isinstance(info, list): 36 | try: info = dict(info) 37 | except TypeError: 38 | return phenny.reply('Try .head http://example.org/ [optional header]') 39 | info['Status'] = '200' 40 | else: 41 | newInfo = dict(info[0]) 42 | newInfo['Status'] = str(info[1]) 43 | info = newInfo 44 | 45 | if header is None: 46 | data = [] 47 | if info.has_key('Status'): 48 | data.append(info['Status']) 49 | if info.has_key('content-type'): 50 | data.append(info['content-type'].replace('; charset=', ', ')) 51 | if info.has_key('last-modified'): 52 | modified = info['last-modified'] 53 | modified = time.strptime(modified, '%a, %d %b %Y %H:%M:%S %Z') 54 | data.append(time.strftime('%Y-%m-%d %H:%M:%S UTC', modified)) 55 | if info.has_key('content-length'): 56 | data.append(info['content-length'] + ' bytes') 57 | phenny.reply(', '.join(data)) 58 | else: 59 | headerlower = header.lower() 60 | if info.has_key(headerlower): 61 | phenny.say(header + ': ' + info.get(headerlower)) 62 | else: 63 | msg = 'There was no %s header in the response.' % header 64 | phenny.say(msg) 65 | head.commands = ['head'] 66 | head.example = '.head http://www.w3.org/' 67 | 68 | r_title = re.compile(r'(?ims)]*>(.*?)') 69 | r_entity = re.compile(r'&[A-Za-z0-9#]+;') 70 | 71 | @deprecated 72 | def f_title(self, origin, match, args): 73 | """.title - Return the title of URI.""" 74 | uri = match.group(2) 75 | uri = (uri or '').encode('utf-8') 76 | 77 | if not uri and hasattr(self, 'last_seen_uri'): 78 | uri = self.last_seen_uri.get(origin.sender) 79 | if not uri: 80 | return self.msg(origin.sender, 'I need a URI to give the title of...') 81 | 82 | if not ':' in uri: 83 | uri = 'http://' + uri 84 | uri = uri.replace('#!', '?_escaped_fragment_=') 85 | 86 | localhost = [ 87 | 'http://localhost/', 'http://localhost:80/', 88 | 'http://localhost:8080/', 'http://127.0.0.1/', 89 | 'http://127.0.0.1:80/', 'http://127.0.0.1:8080/', 90 | 'https://localhost/', 'https://localhost:80/', 91 | 'https://localhost:8080/', 'https://127.0.0.1/', 92 | 'https://127.0.0.1:80/', 'https://127.0.0.1:8080/', 93 | ] 94 | for s in localhost: 95 | if uri.startswith(s): 96 | return phenny.reply('Sorry, access forbidden.') 97 | 98 | try: 99 | redirects = 0 100 | while True: 101 | headers = { 102 | 'Accept': 'text/html', 103 | 'User-Agent': 'Mozilla/5.0 (Phenny)' 104 | } 105 | req = urllib2.Request(uri, headers=headers) 106 | u = urllib2.urlopen(req) 107 | info = u.info() 108 | u.close() 109 | # info = web.head(uri) 110 | 111 | if not isinstance(info, list): 112 | status = '200' 113 | else: 114 | status = str(info[1]) 115 | info = info[0] 116 | if status.startswith('3'): 117 | uri = urlparse.urljoin(uri, info['Location']) 118 | else: break 119 | 120 | redirects += 1 121 | if redirects >= 25: 122 | self.msg(origin.sender, origin.nick + ": Too many redirects") 123 | return 124 | 125 | try: mtype = info['content-type'] 126 | except: 127 | err = ": Couldn't get the Content-Type, sorry" 128 | return self.msg(origin.sender, origin.nick + err) 129 | if not (('/html' in mtype) or ('/xhtml' in mtype)): 130 | self.msg(origin.sender, origin.nick + ": Document isn't HTML") 131 | return 132 | 133 | u = urllib2.urlopen(req) 134 | bytes = u.read(262144) 135 | u.close() 136 | 137 | except IOError: 138 | self.msg(origin.sender, "Can't connect to %s" % uri) 139 | return 140 | 141 | m = r_title.search(bytes) 142 | if m: 143 | title = m.group(1) 144 | title = title.strip() 145 | title = title.replace('\t', ' ') 146 | title = title.replace('\r', ' ') 147 | title = title.replace('\n', ' ') 148 | while ' ' in title: 149 | title = title.replace(' ', ' ') 150 | if len(title) > 200: 151 | title = title[:200] + '[...]' 152 | 153 | def e(m): 154 | entity = m.group(0) 155 | if entity.startswith('&#x'): 156 | cp = int(entity[3:-1], 16) 157 | return unichr(cp).encode('utf-8') 158 | elif entity.startswith('&#'): 159 | cp = int(entity[2:-1]) 160 | return unichr(cp).encode('utf-8') 161 | else: 162 | char = name2codepoint[entity[1:-1]] 163 | return unichr(char).encode('utf-8') 164 | title = r_entity.sub(e, title) 165 | 166 | if title: 167 | try: title.decode('utf-8') 168 | except: 169 | try: title = title.decode('iso-8859-1').encode('utf-8') 170 | except: title = title.decode('cp1252').encode('utf-8') 171 | else: pass 172 | else: title = '[The title is empty.]' 173 | 174 | title = title.replace('\n', '') 175 | title = title.replace('\r', '') 176 | self.msg(origin.sender, origin.nick + ': ' + title) 177 | else: self.msg(origin.sender, origin.nick + ': No title found') 178 | f_title.commands = ['title'] 179 | 180 | def noteuri(phenny, input): 181 | uri = input.group(1).encode('utf-8') 182 | if not hasattr(phenny.bot, 'last_seen_uri'): 183 | phenny.bot.last_seen_uri = {} 184 | phenny.bot.last_seen_uri[input.sender] = uri 185 | noteuri.rule = r'.*(http[s]?://[^<> "\x01]+)[,.]?' 186 | noteuri.priority = 'low' 187 | 188 | if __name__ == '__main__': 189 | print __doc__.strip() 190 | -------------------------------------------------------------------------------- /irc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | irc.py - A Utility IRC Bot 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import sys, re, time, traceback 11 | import socket, asyncore, asynchat 12 | 13 | class Origin(object): 14 | source = re.compile(r'([^!]*)!?([^@]*)@?(.*)') 15 | 16 | def __init__(self, bot, source, args): 17 | match = Origin.source.match(source or '') 18 | self.nick, self.user, self.host = match.groups() 19 | 20 | if len(args) > 1: 21 | target = args[1] 22 | else: target = None 23 | 24 | mappings = {bot.nick: self.nick, None: None} 25 | self.sender = mappings.get(target, target) 26 | 27 | class Bot(asynchat.async_chat): 28 | def __init__(self, nick, name, channels, password=None): 29 | asynchat.async_chat.__init__(self) 30 | self.set_terminator('\n') 31 | self.buffer = '' 32 | 33 | self.nick = nick 34 | self.user = nick 35 | self.name = name 36 | self.password = password 37 | 38 | self.verbose = True 39 | self.channels = channels or [] 40 | self.stack = [] 41 | 42 | import threading 43 | self.sending = threading.RLock() 44 | 45 | def initiate_send(self): 46 | self.sending.acquire() 47 | asynchat.async_chat.initiate_send(self) 48 | self.sending.release() 49 | 50 | # def push(self, *args, **kargs): 51 | # asynchat.async_chat.push(self, *args, **kargs) 52 | 53 | def __write(self, args, text=None): 54 | # print 'PUSH: %r %r %r' % (self, args, text) 55 | try: 56 | if text is not None: 57 | # 510 because CR and LF count too, as nyuszika7h points out 58 | self.push((' '.join(args) + ' :' + text)[:510] + '\r\n') 59 | else: self.push(' '.join(args)[:510] + '\r\n') 60 | except IndexError: 61 | pass 62 | 63 | def write(self, args, text=None): 64 | # This is a safe version of __write 65 | def safe(input): 66 | input = input.replace('\n', '') 67 | input = input.replace('\r', '') 68 | return input.encode('utf-8') 69 | try: 70 | args = [safe(arg) for arg in args] 71 | if text is not None: 72 | text = safe(text) 73 | self.__write(args, text) 74 | except Exception, e: pass 75 | 76 | def run(self, host, port=6667): 77 | self.initiate_connect(host, port) 78 | 79 | def initiate_connect(self, host, port): 80 | if self.verbose: 81 | message = 'Connecting to %s:%s...' % (host, port) 82 | print >> sys.stderr, message, 83 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 84 | self.connect((host, port)) 85 | try: asyncore.loop() 86 | except KeyboardInterrupt: 87 | sys.exit() 88 | 89 | def handle_connect(self): 90 | if self.verbose: 91 | print >> sys.stderr, 'connected!' 92 | if self.password: 93 | self.write(('PASS', self.password)) 94 | self.write(('NICK', self.nick)) 95 | self.write(('USER', self.user, '+iw', self.nick), self.name) 96 | 97 | def handle_close(self): 98 | self.close() 99 | print >> sys.stderr, 'Closed!' 100 | 101 | def collect_incoming_data(self, data): 102 | self.buffer += data 103 | 104 | def found_terminator(self): 105 | line = self.buffer 106 | if line.endswith('\r'): 107 | line = line[:-1] 108 | self.buffer = '' 109 | 110 | # print 'GOT:', repr(line) 111 | if line.startswith(':'): 112 | source, line = line[1:].split(' ', 1) 113 | else: source = None 114 | 115 | if ' :' in line: 116 | argstr, text = line.split(' :', 1) 117 | else: argstr, text = line, '' 118 | args = argstr.split() 119 | 120 | origin = Origin(self, source, args) 121 | self.dispatch(origin, tuple([text] + args)) 122 | 123 | if args[0] == 'PING': 124 | self.write(('PONG', text)) 125 | 126 | def dispatch(self, origin, args): 127 | pass 128 | 129 | def msg(self, recipient, text): 130 | self.sending.acquire() 131 | 132 | # Cf. http://swhack.com/logs/2006-03-01#T19-43-25 133 | if isinstance(text, unicode): 134 | try: text = text.encode('utf-8') 135 | except UnicodeEncodeError, e: 136 | text = e.__class__ + ': ' + str(e) 137 | if isinstance(recipient, unicode): 138 | try: recipient = recipient.encode('utf-8') 139 | except UnicodeEncodeError, e: 140 | return 141 | 142 | # No messages within the last 3 seconds? Go ahead! 143 | # Otherwise, wait so it's been at least 0.8 seconds + penalty 144 | if self.stack: 145 | elapsed = time.time() - self.stack[-1][0] 146 | if elapsed < 3: 147 | penalty = float(max(0, len(text) - 50)) / 70 148 | wait = 0.8 + penalty 149 | if elapsed < wait: 150 | time.sleep(wait - elapsed) 151 | 152 | # Loop detection 153 | messages = [m[1] for m in self.stack[-8:]] 154 | if messages.count(text) >= 5: 155 | text = '...' 156 | if messages.count('...') >= 3: 157 | self.sending.release() 158 | return 159 | 160 | def safe(input): 161 | input = input.replace('\n', '') 162 | return input.replace('\r', '') 163 | self.__write(('PRIVMSG', safe(recipient)), safe(text)) 164 | self.stack.append((time.time(), text)) 165 | self.stack = self.stack[-10:] 166 | 167 | self.sending.release() 168 | 169 | def notice(self, dest, text): 170 | self.write(('NOTICE', dest), text) 171 | 172 | def error(self, origin): 173 | try: 174 | import traceback 175 | trace = traceback.format_exc() 176 | print trace 177 | lines = list(reversed(trace.splitlines())) 178 | 179 | report = [lines[0].strip()] 180 | for line in lines: 181 | line = line.strip() 182 | if line.startswith('File "/'): 183 | report.append(line[0].lower() + line[1:]) 184 | break 185 | else: report.append('source unknown') 186 | 187 | self.msg(origin.sender, report[0] + ' (' + report[1] + ')') 188 | except: self.msg(origin.sender, "Got an error.") 189 | 190 | class TestBot(Bot): 191 | def f_ping(self, origin, match, args): 192 | delay = m.group(1) 193 | if delay is not None: 194 | import time 195 | time.sleep(int(delay)) 196 | self.msg(origin.sender, 'pong (%s)' % delay) 197 | else: self.msg(origin.sender, 'pong') 198 | f_ping.rule = r'^\.ping(?:[ \t]+(\d+))?$' 199 | 200 | def main(): 201 | # bot = TestBot('testbot', ['#d8uv.com']) 202 | # bot.run('irc.freenode.net') 203 | print __doc__ 204 | 205 | if __name__=="__main__": 206 | main() 207 | -------------------------------------------------------------------------------- /modules/clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | clock.py - Phenny Clock Module 4 | Copyright 2008-9, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, math, time, urllib, locale, socket, struct, datetime 11 | from decimal import Decimal as dec 12 | from tools import deprecated 13 | 14 | TimeZones = {'KST': 9, 'CADT': 10.5, 'EETDST': 3, 'MESZ': 2, 'WADT': 9, 15 | 'EET': 2, 'MST': -7, 'WAST': 8, 'IST': 5.5, 'B': 2, 16 | 'MSK': 3, 'X': -11, 'MSD': 4, 'CETDST': 2, 'AST': -4, 17 | 'HKT': 8, 'JST': 9, 'CAST': 9.5, 'CET': 1, 'CEST': 2, 18 | 'EEST': 3, 'EAST': 10, 'METDST': 2, 'MDT': -6, 'A': 1, 19 | 'UTC': 0, 'ADT': -3, 'EST': -5, 'E': 5, 'D': 4, 'G': 7, 20 | 'F': 6, 'I': 9, 'H': 8, 'K': 10, 'PDT': -7, 'M': 12, 21 | 'L': 11, 'O': -2, 'MEST': 2, 'Q': -4, 'P': -3, 'S': -6, 22 | 'R': -5, 'U': -8, 'T': -7, 'W': -10, 'WET': 0, 'Y': -12, 23 | 'CST': -6, 'EADT': 11, 'Z': 0, 'GMT': 0, 'WETDST': 1, 24 | 'C': 3, 'WEST': 1, 'CDT': -5, 'MET': 1, 'N': -1, 'V': -9, 25 | 'EDT': -4, 'UT': 0, 'PST': -8, 'MEZ': 1, 'BST': 1, 26 | 'ACS': 9.5, 'ATL': -4, 'ALA': -9, 'HAW': -10, 'AKDT': -8, 27 | 'AKST': -9, 28 | 'BDST': 2} 29 | 30 | TZ1 = { 31 | 'NDT': -2.5, 32 | 'BRST': -2, 33 | 'ADT': -3, 34 | 'EDT': -4, 35 | 'CDT': -5, 36 | 'MDT': -6, 37 | 'PDT': -7, 38 | 'YDT': -8, 39 | 'HDT': -9, 40 | 'BST': 1, 41 | 'MEST': 2, 42 | 'SST': 2, 43 | 'FST': 2, 44 | 'CEST': 2, 45 | 'EEST': 3, 46 | 'WADT': 8, 47 | 'KDT': 10, 48 | 'EADT': 13, 49 | 'NZD': 13, 50 | 'NZDT': 13, 51 | 'GMT': 0, 52 | 'UT': 0, 53 | 'UTC': 0, 54 | 'WET': 0, 55 | 'WAT': -1, 56 | 'AT': -2, 57 | 'FNT': -2, 58 | 'BRT': -3, 59 | 'MNT': -4, 60 | 'EWT': -4, 61 | 'AST': -4, 62 | 'EST': -5, 63 | 'ACT': -5, 64 | 'CST': -6, 65 | 'MST': -7, 66 | 'PST': -8, 67 | 'YST': -9, 68 | 'HST': -10, 69 | 'CAT': -10, 70 | 'AHST': -10, 71 | 'NT': -11, 72 | 'IDLW': -12, 73 | 'CET': 1, 74 | 'MEZ': 1, 75 | 'ECT': 1, 76 | 'MET': 1, 77 | 'MEWT': 1, 78 | 'SWT': 1, 79 | 'SET': 1, 80 | 'FWT': 1, 81 | 'EET': 2, 82 | 'UKR': 2, 83 | 'BT': 3, 84 | 'ZP4': 4, 85 | 'ZP5': 5, 86 | 'ZP6': 6, 87 | 'WST': 8, 88 | 'HKT': 8, 89 | 'CCT': 8, 90 | 'JST': 9, 91 | 'KST': 9, 92 | 'EAST': 10, 93 | 'GST': 10, 94 | 'NZT': 12, 95 | 'NZST': 12, 96 | 'IDLE': 12 97 | } 98 | 99 | TZ2 = { 100 | 'ACDT': 10.5, 101 | 'ACST': 9.5, 102 | 'ADT': 3, 103 | 'AEDT': 11, # hmm 104 | 'AEST': 10, # hmm 105 | 'AHDT': 9, 106 | 'AHST': 10, 107 | 'AST': 4, 108 | 'AT': 2, 109 | 'AWDT': -9, 110 | 'AWST': -8, 111 | 'BAT': -3, 112 | 'BDST': -2, 113 | 'BET': 11, 114 | 'BST': -1, 115 | 'BT': -3, 116 | 'BZT2': 3, 117 | 'CADT': -10.5, 118 | 'CAST': -9.5, 119 | 'CAT': 10, 120 | 'CCT': -8, 121 | # 'CDT': 5, 122 | 'CED': -2, 123 | 'CET': -1, 124 | 'CST': 6, 125 | 'EAST': -10, 126 | # 'EDT': 4, 127 | 'EED': -3, 128 | 'EET': -2, 129 | 'EEST': -3, 130 | 'EST': 5, 131 | 'FST': -2, 132 | 'FWT': -1, 133 | 'GMT': 0, 134 | 'GST': -10, 135 | 'HDT': 9, 136 | 'HST': 10, 137 | 'IDLE': -12, 138 | 'IDLW': 12, 139 | # 'IST': -5.5, 140 | 'IT': -3.5, 141 | 'JST': -9, 142 | 'JT': -7, 143 | 'KST': -9, 144 | 'MDT': 6, 145 | 'MED': -2, 146 | 'MET': -1, 147 | 'MEST': -2, 148 | 'MEWT': -1, 149 | 'MST': 7, 150 | 'MT': -8, 151 | 'NDT': 2.5, 152 | 'NFT': 3.5, 153 | 'NT': 11, 154 | 'NST': -6.5, 155 | 'NZ': -11, 156 | 'NZST': -12, 157 | 'NZDT': -13, 158 | 'NZT': -12, 159 | # 'PDT': 7, 160 | 'PST': 8, 161 | 'ROK': -9, 162 | 'SAD': -10, 163 | 'SAST': -9, 164 | 'SAT': -9, 165 | 'SDT': -10, 166 | 'SST': -2, 167 | 'SWT': -1, 168 | 'USZ3': -4, 169 | 'USZ4': -5, 170 | 'USZ5': -6, 171 | 'USZ6': -7, 172 | 'UT': 0, 173 | 'UTC': 0, 174 | 'UZ10': -11, 175 | 'WAT': 1, 176 | 'WET': 0, 177 | 'WST': -8, 178 | 'YDT': 8, 179 | 'YST': 9, 180 | 'ZP4': -4, 181 | 'ZP5': -5, 182 | 'ZP6': -6 183 | } 184 | 185 | TZ3 = { 186 | 'AEST': 10, 187 | 'AEDT': 11 188 | } 189 | 190 | # TimeZones.update(TZ2) # do these have to be negated? 191 | TimeZones.update(TZ1) 192 | TimeZones.update(TZ3) 193 | 194 | r_local = re.compile(r'\([a-z]+_[A-Z]+\)') 195 | 196 | @deprecated 197 | def f_time(self, origin, match, args): 198 | """Returns the current time.""" 199 | tz = match.group(2) or 'GMT' 200 | 201 | # Personal time zones, because they're rad 202 | if hasattr(self.config, 'timezones'): 203 | People = self.config.timezones 204 | else: People = {} 205 | 206 | if People.has_key(tz): 207 | tz = People[tz] 208 | elif (not match.group(2)) and People.has_key(origin.nick): 209 | tz = People[origin.nick] 210 | 211 | TZ = tz.upper() 212 | if len(tz) > 30: return 213 | 214 | if (TZ == 'UTC') or (TZ == 'Z'): 215 | msg = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) 216 | self.msg(origin.sender, msg) 217 | elif r_local.match(tz): # thanks to Mark Shoulsdon (clsn) 218 | locale.setlocale(locale.LC_TIME, (tz[1:-1], 'UTF-8')) 219 | msg = time.strftime("%A, %d %B %Y %H:%M:%SZ", time.gmtime()) 220 | self.msg(origin.sender, msg) 221 | elif TimeZones.has_key(TZ): 222 | offset = TimeZones[TZ] * 3600 223 | timenow = time.gmtime(time.time() + offset) 224 | msg = time.strftime("%a, %d %b %Y %H:%M:%S " + str(TZ), timenow) 225 | self.msg(origin.sender, msg) 226 | elif tz and tz[0] in ('+', '-') and 4 <= len(tz) <= 6: 227 | timenow = time.gmtime(time.time() + (int(tz[:3]) * 3600)) 228 | msg = time.strftime("%a, %d %b %Y %H:%M:%S " + str(tz), timenow) 229 | self.msg(origin.sender, msg) 230 | else: 231 | try: t = float(tz) 232 | except ValueError: 233 | import os, re, subprocess 234 | r_tz = re.compile(r'^[A-Za-z]+(?:/[A-Za-z_]+)*$') 235 | if r_tz.match(tz) and os.path.isfile('/usr/share/zoneinfo/' + tz): 236 | cmd, PIPE = 'TZ=%s date' % tz, subprocess.PIPE 237 | proc = subprocess.Popen(cmd, shell=True, stdout=PIPE) 238 | self.msg(origin.sender, proc.communicate()[0]) 239 | else: 240 | error = "Sorry, I don't know about the '%s' timezone." % tz 241 | self.msg(origin.sender, origin.nick + ': ' + error) 242 | else: 243 | timenow = time.gmtime(time.time() + (t * 3600)) 244 | msg = time.strftime("%a, %d %b %Y %H:%M:%S " + str(tz), timenow) 245 | self.msg(origin.sender, msg) 246 | f_time.commands = ['t'] 247 | f_time.name = 't' 248 | f_time.example = '.t UTC' 249 | 250 | def beats(phenny, input): 251 | """Shows the internet time in Swatch beats.""" 252 | beats = ((time.time() + 3600) % 86400) / 86.4 253 | beats = int(math.floor(beats)) 254 | phenny.say('@%03i' % beats) 255 | beats.commands = ['beats'] 256 | beats.priority = 'low' 257 | 258 | def divide(input, by): 259 | return (input / by), (input % by) 260 | 261 | def yi(phenny, input): 262 | """Shows whether it is currently yi or not.""" 263 | quadraels, remainder = divide(int(time.time()), 1753200) 264 | raels = quadraels * 4 265 | extraraels, remainder = divide(remainder, 432000) 266 | if extraraels == 4: 267 | return phenny.say('Yes! PARTAI!') 268 | else: phenny.say('Not yet...') 269 | yi.commands = ['yi'] 270 | yi.priority = 'low' 271 | 272 | def tock(phenny, input): 273 | """Shows the time from the USNO's atomic clock.""" 274 | u = urllib.urlopen('http://tycho.usno.navy.mil/cgi-bin/timer.pl') 275 | info = u.info() 276 | u.close() 277 | phenny.say('"' + info['Date'] + '" - tycho.usno.navy.mil') 278 | tock.commands = ['tock'] 279 | tock.priority = 'high' 280 | 281 | def npl(phenny, input): 282 | """Shows the time from NPL's SNTP server.""" 283 | # for server in ('ntp1.npl.co.uk', 'ntp2.npl.co.uk'): 284 | client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 285 | client.sendto('\x1b' + 47 * '\0', ('ntp1.npl.co.uk', 123)) 286 | data, address = client.recvfrom(1024) 287 | if data: 288 | buf = struct.unpack('B' * 48, data) 289 | d = dec('0.0') 290 | for i in range(8): 291 | d += dec(buf[32 + i]) * dec(str(math.pow(2, (3 - i) * 8))) 292 | d -= dec(2208988800L) 293 | a, b = str(d).split('.') 294 | f = '%Y-%m-%d %H:%M:%S' 295 | result = datetime.datetime.fromtimestamp(d).strftime(f) + '.' + b[:6] 296 | phenny.say(result + ' - ntp1.npl.co.uk') 297 | else: phenny.say('No data received, sorry') 298 | npl.commands = ['npl'] 299 | npl.priority = 'high' 300 | 301 | if __name__ == '__main__': 302 | print __doc__.strip() 303 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | bot.py - Phenny IRC Bot 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import sys, os, re, threading, imp 11 | import irc 12 | 13 | home = os.getcwd() 14 | 15 | def decode(bytes): 16 | try: text = bytes.decode('utf-8') 17 | except UnicodeDecodeError: 18 | try: text = bytes.decode('iso-8859-1') 19 | except UnicodeDecodeError: 20 | text = bytes.decode('cp1252') 21 | return text 22 | 23 | class Phenny(irc.Bot): 24 | def __init__(self, config): 25 | args = (config.nick, config.name, config.channels, config.password) 26 | irc.Bot.__init__(self, *args) 27 | self.config = config 28 | self.doc = {} 29 | self.stats = {} 30 | self.setup() 31 | 32 | def setup(self): 33 | self.variables = {} 34 | 35 | filenames = [] 36 | if not hasattr(self.config, 'enable'): 37 | for fn in os.listdir(os.path.join(home, 'modules')): 38 | if fn.endswith('.py') and not fn.startswith('_'): 39 | filenames.append(os.path.join(home, 'modules', fn)) 40 | else: 41 | for fn in self.config.enable: 42 | filenames.append(os.path.join(home, 'modules', fn + '.py')) 43 | 44 | if hasattr(self.config, 'extra'): 45 | for fn in self.config.extra: 46 | if os.path.isfile(fn): 47 | filenames.append(fn) 48 | elif os.path.isdir(fn): 49 | for n in os.listdir(fn): 50 | if n.endswith('.py') and not n.startswith('_'): 51 | filenames.append(os.path.join(fn, n)) 52 | 53 | modules = [] 54 | excluded_modules = getattr(self.config, 'exclude', []) 55 | for filename in filenames: 56 | name = os.path.basename(filename)[:-3] 57 | if name in excluded_modules: continue 58 | # if name in sys.modules: 59 | # del sys.modules[name] 60 | try: module = imp.load_source(name, filename) 61 | except Exception, e: 62 | print >> sys.stderr, "Error loading %s: %s (in bot.py)" % (name, e) 63 | else: 64 | if hasattr(module, 'setup'): 65 | module.setup(self) 66 | self.register(vars(module)) 67 | modules.append(name) 68 | 69 | if modules: 70 | print >> sys.stderr, 'Registered modules:', ', '.join(modules) 71 | else: print >> sys.stderr, "Warning: Couldn't find any modules" 72 | 73 | self.bind_commands() 74 | 75 | def register(self, variables): 76 | # This is used by reload.py, hence it being methodised 77 | for name, obj in variables.iteritems(): 78 | if hasattr(obj, 'commands') or hasattr(obj, 'rule'): 79 | self.variables[name] = obj 80 | 81 | def bind_commands(self): 82 | self.commands = {'high': {}, 'medium': {}, 'low': {}} 83 | 84 | def bind(self, priority, regexp, func): 85 | print priority, regexp.pattern.encode('utf-8'), func 86 | # register documentation 87 | if not hasattr(func, 'name'): 88 | func.name = func.__name__ 89 | if func.__doc__: 90 | if hasattr(func, 'example'): 91 | example = func.example 92 | example = example.replace('$nickname', self.nick) 93 | else: example = None 94 | self.doc[func.name] = (func.__doc__, example) 95 | self.commands[priority].setdefault(regexp, []).append(func) 96 | 97 | def sub(pattern, self=self): 98 | # These replacements have significant order 99 | pattern = pattern.replace('$nickname', re.escape(self.nick)) 100 | return pattern.replace('$nick', r'%s[,:] +' % re.escape(self.nick)) 101 | 102 | for name, func in self.variables.iteritems(): 103 | # print name, func 104 | if not hasattr(func, 'priority'): 105 | func.priority = 'medium' 106 | 107 | if not hasattr(func, 'thread'): 108 | func.thread = True 109 | 110 | if not hasattr(func, 'event'): 111 | func.event = 'PRIVMSG' 112 | else: func.event = func.event.upper() 113 | 114 | if hasattr(func, 'rule'): 115 | if isinstance(func.rule, str): 116 | pattern = sub(func.rule) 117 | regexp = re.compile(pattern) 118 | bind(self, func.priority, regexp, func) 119 | 120 | if isinstance(func.rule, tuple): 121 | # 1) e.g. ('$nick', '(.*)') 122 | if len(func.rule) == 2 and isinstance(func.rule[0], str): 123 | prefix, pattern = func.rule 124 | prefix = sub(prefix) 125 | regexp = re.compile(prefix + pattern) 126 | bind(self, func.priority, regexp, func) 127 | 128 | # 2) e.g. (['p', 'q'], '(.*)') 129 | elif len(func.rule) == 2 and isinstance(func.rule[0], list): 130 | prefix = self.config.prefix 131 | commands, pattern = func.rule 132 | for command in commands: 133 | command = r'(%s)\b(?: +(?:%s))?' % (command, pattern) 134 | regexp = re.compile(prefix + command) 135 | bind(self, func.priority, regexp, func) 136 | 137 | # 3) e.g. ('$nick', ['p', 'q'], '(.*)') 138 | elif len(func.rule) == 3: 139 | prefix, commands, pattern = func.rule 140 | prefix = sub(prefix) 141 | for command in commands: 142 | command = r'(%s) +' % command 143 | regexp = re.compile(prefix + command + pattern) 144 | bind(self, func.priority, regexp, func) 145 | 146 | if hasattr(func, 'commands'): 147 | for command in func.commands: 148 | template = r'^%s(%s)(?: +(.*))?$' 149 | pattern = template % (self.config.prefix, command) 150 | regexp = re.compile(pattern) 151 | bind(self, func.priority, regexp, func) 152 | 153 | def wrapped(self, origin, text, match): 154 | class PhennyWrapper(object): 155 | def __init__(self, phenny): 156 | self.bot = phenny 157 | 158 | def __getattr__(self, attr): 159 | sender = origin.sender or text 160 | if attr == 'reply': 161 | return (lambda msg: 162 | self.bot.msg(sender, origin.nick + ': ' + msg)) 163 | elif attr == 'say': 164 | return lambda msg: self.bot.msg(sender, msg) 165 | return getattr(self.bot, attr) 166 | 167 | return PhennyWrapper(self) 168 | 169 | def input(self, origin, text, bytes, match, event, args): 170 | class CommandInput(unicode): 171 | def __new__(cls, text, origin, bytes, match, event, args): 172 | s = unicode.__new__(cls, text) 173 | s.sender = origin.sender 174 | s.nick = origin.nick 175 | s.event = event 176 | s.bytes = bytes 177 | s.match = match 178 | s.group = match.group 179 | s.groups = match.groups 180 | s.args = args 181 | s.admin = origin.nick in self.config.admins 182 | s.owner = origin.nick == self.config.owner 183 | return s 184 | 185 | return CommandInput(text, origin, bytes, match, event, args) 186 | 187 | def call(self, func, origin, phenny, input): 188 | try: func(phenny, input) 189 | except Exception, e: 190 | self.error(origin) 191 | 192 | def limit(self, origin, func): 193 | if origin.sender and origin.sender.startswith('#'): 194 | if hasattr(self.config, 'limit'): 195 | limits = self.config.limit.get(origin.sender) 196 | if limits and (func.__module__ not in limits): 197 | return True 198 | return False 199 | 200 | def dispatch(self, origin, args): 201 | bytes, event, args = args[0], args[1], args[2:] 202 | text = decode(bytes) 203 | 204 | for priority in ('high', 'medium', 'low'): 205 | items = self.commands[priority].items() 206 | for regexp, funcs in items: 207 | for func in funcs: 208 | if event != func.event and func.event != '*': continue 209 | 210 | match = regexp.match(text) 211 | if match: 212 | if self.limit(origin, func): continue 213 | 214 | phenny = self.wrapped(origin, text, match) 215 | input = self.input(origin, text, bytes, match, event, args) 216 | 217 | if func.thread: 218 | targs = (func, origin, phenny, input) 219 | t = threading.Thread(target=self.call, args=targs) 220 | t.start() 221 | else: self.call(func, origin, phenny, input) 222 | 223 | for source in [origin.sender, origin.nick]: 224 | try: self.stats[(func.name, source)] += 1 225 | except KeyError: 226 | self.stats[(func.name, source)] = 1 227 | 228 | if __name__ == '__main__': 229 | print __doc__ 230 | -------------------------------------------------------------------------------- /modules/search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | search.py - Phenny Web Search Module 4 | Copyright 2008-9, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re 11 | import web 12 | 13 | class Grab(web.urllib.URLopener): 14 | def __init__(self, *args): 15 | self.version = 'Mozilla/5.0 (Phenny)' 16 | web.urllib.URLopener.__init__(self, *args) 17 | self.addheader('Referer', 'https://github.com/sbp/phenny') 18 | def http_error_default(self, url, fp, errcode, errmsg, headers): 19 | return web.urllib.addinfourl(fp, [headers, errcode], "http:" + url) 20 | 21 | def google_ajax(query): 22 | """Search using AjaxSearch, and return its JSON.""" 23 | if isinstance(query, unicode): 24 | query = query.encode('utf-8') 25 | uri = 'http://ajax.googleapis.com/ajax/services/search/web' 26 | args = '?v=1.0&safe=off&q=' + web.urllib.quote(query) 27 | handler = web.urllib._urlopener 28 | web.urllib._urlopener = Grab() 29 | bytes = web.get(uri + args) 30 | web.urllib._urlopener = handler 31 | return web.json(bytes) 32 | 33 | def google_search(query): 34 | results = google_ajax(query) 35 | try: return results['responseData']['results'][0]['unescapedUrl'] 36 | except IndexError: return None 37 | except TypeError: 38 | print results 39 | return False 40 | 41 | def google_count(query): 42 | results = google_ajax(query) 43 | if not results.has_key('responseData'): return '0' 44 | if not results['responseData'].has_key('cursor'): return '0' 45 | if not results['responseData']['cursor'].has_key('estimatedResultCount'): 46 | return '0' 47 | return results['responseData']['cursor']['estimatedResultCount'] 48 | 49 | def formatnumber(n): 50 | """Format a number with beautiful commas.""" 51 | parts = list(str(n)) 52 | for i in range((len(parts) - 3), 0, -3): 53 | parts.insert(i, ',') 54 | return ''.join(parts) 55 | 56 | def old_gc(query): 57 | return formatnumber(google_count(query)) 58 | 59 | def g(phenny, input): 60 | """Queries Google for the specified input.""" 61 | query = input.group(2) 62 | if not query: 63 | return phenny.reply('.g what?') 64 | query = query.encode('utf-8') 65 | uri = google_search(query) 66 | if uri: 67 | phenny.reply(uri) 68 | if not hasattr(phenny.bot, 'last_seen_uri'): 69 | phenny.bot.last_seen_uri = {} 70 | phenny.bot.last_seen_uri[input.sender] = uri 71 | elif uri is False: phenny.reply("Problem getting data from Google.") 72 | else: phenny.reply("No results found for '%s'." % query) 73 | g.commands = ['g'] 74 | g.priority = 'high' 75 | g.example = '.g swhack' 76 | 77 | def oldgc(phenny, input): 78 | """Returns the number of Google results for the specified input.""" 79 | query = input.group(2) 80 | if not query: 81 | return phenny.reply('.gc what?') 82 | query = query.encode('utf-8') 83 | num = formatnumber(google_count(query)) 84 | phenny.say(query + ': ' + num) 85 | oldgc.commands = ['ogc', 'oldgc'] 86 | oldgc.example = '.oldgc extrapolate' 87 | 88 | r_query = re.compile( 89 | r'\+?"[^"\\]*(?:\\.[^"\\]*)*"|\[[^]\\]*(?:\\.[^]\\]*)*\]|\S+' 90 | ) 91 | 92 | def gcs(phenny, input): 93 | if not input.group(2): 94 | return phenny.reply("Nothing to compare.") 95 | queries = r_query.findall(input.group(2)) 96 | if len(queries) > 6: 97 | return phenny.reply('Sorry, can only compare up to six things.') 98 | 99 | results = [] 100 | for i, query in enumerate(queries): 101 | query = query.strip('[]') 102 | query = query.encode('utf-8') 103 | n = int((formatnumber(google_count(query)) or '0').replace(',', '')) 104 | results.append((n, query)) 105 | if i >= 2: __import__('time').sleep(0.25) 106 | if i >= 4: __import__('time').sleep(0.25) 107 | 108 | results = [(term, n) for (n, term) in reversed(sorted(results))] 109 | reply = ', '.join('%s (%s)' % (t, formatnumber(n)) for (t, n) in results) 110 | phenny.say(reply) 111 | gcs.commands = ['gcs', 'comp'] 112 | 113 | r_bing = re.compile(r'

    ') 145 | 146 | def duck_search(query): 147 | query = query.replace('!', '') 148 | query = web.urllib.quote(query) 149 | uri = 'http://duckduckgo.com/html/?q=%s&kl=uk-en' % query 150 | bytes = web.get(uri) 151 | m = r_duck.search(bytes) 152 | if m: return web.decode(m.group(1)) 153 | 154 | def duck(phenny, input): 155 | query = input.group(2) 156 | if not query: return phenny.reply('.ddg what?') 157 | 158 | query = query.encode('utf-8') 159 | uri = duck_search(query) 160 | if uri: 161 | phenny.reply(uri) 162 | if not hasattr(phenny.bot, 'last_seen_uri'): 163 | phenny.bot.last_seen_uri = {} 164 | phenny.bot.last_seen_uri[input.sender] = uri 165 | else: phenny.reply("No results found for '%s'." % query) 166 | duck.commands = ['duck', 'ddg'] 167 | 168 | def search(phenny, input): 169 | if not input.group(2): 170 | return phenny.reply('.search for what?') 171 | query = input.group(2).encode('utf-8') 172 | gu = google_search(query) or '-' 173 | bu = bing_search(query) or '-' 174 | du = duck_search(query) or '-' 175 | 176 | if (gu == bu) and (bu == du): 177 | result = '%s (g, b, d)' % gu 178 | elif (gu == bu): 179 | result = '%s (g, b), %s (d)' % (gu, du) 180 | elif (bu == du): 181 | result = '%s (b, d), %s (g)' % (bu, gu) 182 | elif (gu == du): 183 | result = '%s (g, d), %s (b)' % (gu, bu) 184 | else: 185 | if len(gu) > 250: gu = '(extremely long link)' 186 | if len(bu) > 150: bu = '(extremely long link)' 187 | if len(du) > 150: du = '(extremely long link)' 188 | result = '%s (g), %s (b), %s (d)' % (gu, bu, du) 189 | 190 | phenny.reply(result) 191 | search.commands = ['search'] 192 | 193 | def suggest(phenny, input): 194 | if not input.group(2): 195 | return phenny.reply("No query term.") 196 | query = input.group(2).encode('utf-8') 197 | uri = 'http://websitedev.de/temp-bin/suggest.pl?q=' 198 | answer = web.get(uri + web.urllib.quote(query).replace('+', '%2B')) 199 | if answer: 200 | phenny.say(answer) 201 | else: phenny.reply('Sorry, no result.') 202 | suggest.commands = ['suggest'] 203 | 204 | def new_gc(query): 205 | uri = 'https://www.google.com/search?hl=en&q=' 206 | uri = uri + web.urllib.quote(query).replace('+', '%2B') 207 | # if '"' in query: uri += '&tbs=li:1' 208 | bytes = web.get(uri) 209 | if "did not match any documents" in bytes: 210 | return "0" 211 | for result in re.compile(r'(?ims)([0-9,]+) results?').findall(bytes): 212 | return result 213 | return None 214 | 215 | def newest_gc(query): 216 | uri = 'https://www.google.com/search?hl=en&q=' 217 | uri = uri + web.urllib.quote(query).replace('+', '%2B') 218 | bytes = web.get(uri + '&tbs=li:1') 219 | if "did not match any documents" in bytes: 220 | return "0" 221 | for result in re.compile(r'(?ims)([0-9,]+) results?').findall(bytes): 222 | return result 223 | return None 224 | 225 | def newerest_gc(query): 226 | uri = 'https://www.google.com/search?hl=en&q=' 227 | uri = uri + web.urllib.quote(query).replace('+', '%2B') 228 | bytes = web.get(uri + '&prmd=imvns&start=950') 229 | if "did not match any documents" in bytes: 230 | return "0" 231 | for result in re.compile(r'(?ims)([0-9,]+) results?').findall(bytes): 232 | return result 233 | return None 234 | 235 | def ngc(phenny, input): 236 | if not input.group(2): 237 | return phenny.reply("No query term.") 238 | query = input.group(2).encode('utf-8') 239 | result = new_gc(query) 240 | if result: 241 | phenny.say(query + ": " + result) 242 | else: phenny.reply("Sorry, couldn't get a result.") 243 | 244 | ngc.commands = ['ngc'] 245 | ngc.priority = 'high' 246 | ngc.example = '.ngc extrapolate' 247 | 248 | def gc(phenny, input): 249 | if not input.group(2): 250 | return phenny.reply("No query term.") 251 | query = input.group(2).encode('utf-8') 252 | result = query + ": " 253 | result += (old_gc(query) or "?") + " (api)" 254 | result += ", " + (newerest_gc(query) or "?") + " (end)" 255 | result += ", " + (new_gc(query) or "?") + " (site)" 256 | if '"' in query: 257 | result += ", " + (newest_gc(query) or "?") + " (verbatim)" 258 | phenny.say(result) 259 | 260 | gc.commands = ['gc'] 261 | gc.priority = 'high' 262 | gc.example = '.gc extrapolate' 263 | 264 | if __name__ == '__main__': 265 | print __doc__.strip() 266 | -------------------------------------------------------------------------------- /modules/weather.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | weather.py - Phenny Weather Module 4 | Copyright 2008, Sean B. Palmer, inamidst.com 5 | Licensed under the Eiffel Forum License 2. 6 | 7 | http://inamidst.com/phenny/ 8 | """ 9 | 10 | import re, urllib 11 | import web 12 | from tools import deprecated 13 | 14 | r_from = re.compile(r'(?i)([+-]\d+):00 from') 15 | 16 | def location(name): 17 | name = urllib.quote(name.encode('utf-8')) 18 | uri = 'http://ws.geonames.org/searchJSON?q=%s&maxRows=1' % name 19 | for i in xrange(10): 20 | u = urllib.urlopen(uri) 21 | if u is not None: break 22 | bytes = u.read() 23 | u.close() 24 | 25 | results = web.json(bytes) 26 | try: name = results['geonames'][0]['name'] 27 | except IndexError: 28 | return '?', '?', '0', '0' 29 | countryName = results['geonames'][0]['countryName'] 30 | lat = results['geonames'][0]['lat'] 31 | lng = results['geonames'][0]['lng'] 32 | return name, countryName, lat, lng 33 | 34 | class GrumbleError(object): 35 | pass 36 | 37 | def local(icao, hour, minute): 38 | uri = ('http://www.flightstats.com/' + 39 | 'go/Airport/airportDetails.do?airportCode=%s') 40 | try: bytes = web.get(uri % icao) 41 | except AttributeError: 42 | raise GrumbleError('A WEBSITE HAS GONE DOWN WTF STUPID WEB') 43 | m = r_from.search(bytes) 44 | if m: 45 | offset = m.group(1) 46 | lhour = int(hour) + int(offset) 47 | lhour = lhour % 24 48 | return (str(lhour) + ':' + str(minute) + ', ' + str(hour) + 49 | str(minute) + 'Z') 50 | # return (str(lhour) + ':' + str(minute) + ' (' + str(hour) + 51 | # ':' + str(minute) + 'Z)') 52 | return str(hour) + ':' + str(minute) + 'Z' 53 | 54 | def code(phenny, search): 55 | from icao import data 56 | 57 | if search.upper() in [loc[0] for loc in data]: 58 | return search.upper() 59 | else: 60 | name, country, latitude, longitude = location(search) 61 | if name == '?': return False 62 | sumOfSquares = (99999999999999999999999999999, 'ICAO') 63 | for icao_code, lat, lon in data: 64 | latDiff = abs(latitude - lat) 65 | lonDiff = abs(longitude - lon) 66 | diff = (latDiff * latDiff) + (lonDiff * lonDiff) 67 | if diff < sumOfSquares[0]: 68 | sumOfSquares = (diff, icao_code) 69 | return sumOfSquares[1] 70 | 71 | @deprecated 72 | def f_weather(self, origin, match, args): 73 | """.weather - Show the weather at airport with the code .""" 74 | if origin.sender == '#talis': 75 | if args[0].startswith('.weather '): return 76 | 77 | icao_code = match.group(2) 78 | if not icao_code: 79 | return self.msg(origin.sender, 'Try .weather London, for example?') 80 | 81 | icao_code = code(self, icao_code) 82 | 83 | if not icao_code: 84 | self.msg(origin.sender, 'No ICAO code found, sorry') 85 | return 86 | 87 | uri = 'http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT' 88 | try: bytes = web.get(uri % icao_code) 89 | except AttributeError: 90 | raise GrumbleError('OH CRAP NOAA HAS GONE DOWN THE WEB IS BROKEN') 91 | if 'Not Found' in bytes: 92 | self.msg(origin.sender, icao_code+': no such ICAO code, or no NOAA data') 93 | return 94 | 95 | metar = bytes.splitlines().pop() 96 | metar = metar.split(' ') 97 | 98 | if len(metar[0]) == 4: 99 | metar = metar[1:] 100 | 101 | if metar[0].endswith('Z'): 102 | time = metar[0] 103 | metar = metar[1:] 104 | else: time = None 105 | 106 | if metar[0] == 'AUTO': 107 | metar = metar[1:] 108 | if metar[0] == 'VCU': 109 | self.msg(origin.sender, icao_code + ': no data provided') 110 | return 111 | 112 | if metar[0].endswith('KT'): 113 | wind = metar[0] 114 | metar = metar[1:] 115 | else: wind = None 116 | 117 | if ('V' in metar[0]) and (metar[0] != 'CAVOK'): 118 | vari = metar[0] 119 | metar = metar[1:] 120 | else: vari = None 121 | 122 | if ((len(metar[0]) == 4) or 123 | metar[0].endswith('SM')): 124 | visibility = metar[0] 125 | metar = metar[1:] 126 | else: visibility = None 127 | 128 | while metar[0].startswith('R') and (metar[0].endswith('L') 129 | or 'L/' in metar[0]): 130 | metar = metar[1:] 131 | 132 | if len(metar[0]) == 6 and (metar[0].endswith('N') or 133 | metar[0].endswith('E') or 134 | metar[0].endswith('S') or 135 | metar[0].endswith('W')): 136 | metar = metar[1:] # 7000SE? 137 | 138 | cond = [] 139 | while (((len(metar[0]) < 5) or 140 | metar[0].startswith('+') or 141 | metar[0].startswith('-')) and (not (metar[0].startswith('VV') or 142 | metar[0].startswith('SKC') or metar[0].startswith('CLR') or 143 | metar[0].startswith('FEW') or metar[0].startswith('SCT') or 144 | metar[0].startswith('BKN') or metar[0].startswith('OVC')))): 145 | cond.append(metar[0]) 146 | metar = metar[1:] 147 | 148 | while '/P' in metar[0]: 149 | metar = metar[1:] 150 | 151 | if not metar: 152 | self.msg(origin.sender, icao_code + ': no data provided') 153 | return 154 | 155 | cover = [] 156 | while (metar[0].startswith('VV') or metar[0].startswith('SKC') or 157 | metar[0].startswith('CLR') or metar[0].startswith('FEW') or 158 | metar[0].startswith('SCT') or metar[0].startswith('BKN') or 159 | metar[0].startswith('OVC')): 160 | cover.append(metar[0]) 161 | metar = metar[1:] 162 | if not metar: 163 | self.msg(origin.sender, icao_code + ': no data provided') 164 | return 165 | 166 | if metar[0] == 'CAVOK': 167 | cover.append('CLR') 168 | metar = metar[1:] 169 | 170 | if metar[0] == 'PRFG': 171 | cover.append('CLR') # @@? 172 | metar = metar[1:] 173 | 174 | if metar[0] == 'NSC': 175 | cover.append('CLR') 176 | metar = metar[1:] 177 | 178 | if ('/' in metar[0]) or (len(metar[0]) == 5 and metar[0][2] == '.'): 179 | temp = metar[0] 180 | metar = metar[1:] 181 | else: temp = None 182 | 183 | if metar[0].startswith('QFE'): 184 | metar = metar[1:] 185 | 186 | if metar[0].startswith('Q') or metar[0].startswith('A'): 187 | pressure = metar[0] 188 | metar = metar[1:] 189 | else: pressure = None 190 | 191 | if time: 192 | hour = time[2:4] 193 | minute = time[4:6] 194 | time = local(icao_code, hour, minute) 195 | else: time = '(time unknown)' 196 | 197 | if wind: 198 | speed = int(wind[3:5]) 199 | if speed < 1: 200 | description = 'Calm' 201 | elif speed < 4: 202 | description = 'Light air' 203 | elif speed < 7: 204 | description = 'Light breeze' 205 | elif speed < 11: 206 | description = 'Gentle breeze' 207 | elif speed < 16: 208 | description = 'Moderate breeze' 209 | elif speed < 22: 210 | description = 'Fresh breeze' 211 | elif speed < 28: 212 | description = 'Strong breeze' 213 | elif speed < 34: 214 | description = 'Near gale' 215 | elif speed < 41: 216 | description = 'Gale' 217 | elif speed < 48: 218 | description = 'Strong gale' 219 | elif speed < 56: 220 | description = 'Storm' 221 | elif speed < 64: 222 | description = 'Violent storm' 223 | else: description = 'Hurricane' 224 | 225 | degrees = wind[0:3] 226 | if degrees == 'VRB': 227 | degrees = u'\u21BB'.encode('utf-8') 228 | elif (degrees <= 22.5) or (degrees > 337.5): 229 | degrees = u'\u2191'.encode('utf-8') 230 | elif (degrees > 22.5) and (degrees <= 67.5): 231 | degrees = u'\u2197'.encode('utf-8') 232 | elif (degrees > 67.5) and (degrees <= 112.5): 233 | degrees = u'\u2192'.encode('utf-8') 234 | elif (degrees > 112.5) and (degrees <= 157.5): 235 | degrees = u'\u2198'.encode('utf-8') 236 | elif (degrees > 157.5) and (degrees <= 202.5): 237 | degrees = u'\u2193'.encode('utf-8') 238 | elif (degrees > 202.5) and (degrees <= 247.5): 239 | degrees = u'\u2199'.encode('utf-8') 240 | elif (degrees > 247.5) and (degrees <= 292.5): 241 | degrees = u'\u2190'.encode('utf-8') 242 | elif (degrees > 292.5) and (degrees <= 337.5): 243 | degrees = u'\u2196'.encode('utf-8') 244 | 245 | if not icao_code.startswith('EN') and not icao_code.startswith('ED'): 246 | wind = '%s %skt (%s)' % (description, speed, degrees) 247 | elif icao_code.startswith('ED'): 248 | kmh = int(round(speed * 1.852, 0)) 249 | wind = '%s %skm/h (%skt) (%s)' % (description, kmh, speed, degrees) 250 | elif icao_code.startswith('EN'): 251 | ms = int(round(speed * 0.514444444, 0)) 252 | wind = '%s %sm/s (%skt) (%s)' % (description, ms, speed, degrees) 253 | else: wind = '(wind unknown)' 254 | 255 | if visibility: 256 | visibility = visibility + 'm' 257 | else: visibility = '(visibility unknown)' 258 | 259 | if cover: 260 | level = None 261 | for c in cover: 262 | if c.startswith('OVC') or c.startswith('VV'): 263 | if (level is None) or (level < 8): 264 | level = 8 265 | elif c.startswith('BKN'): 266 | if (level is None) or (level < 5): 267 | level = 5 268 | elif c.startswith('SCT'): 269 | if (level is None) or (level < 3): 270 | level = 3 271 | elif c.startswith('FEW'): 272 | if (level is None) or (level < 1): 273 | level = 1 274 | elif c.startswith('SKC') or c.startswith('CLR'): 275 | if level is None: 276 | level = 0 277 | 278 | if level == 8: 279 | cover = u'Overcast \u2601'.encode('utf-8') 280 | elif level == 5: 281 | cover = 'Cloudy' 282 | elif level == 3: 283 | cover = 'Scattered' 284 | elif (level == 1) or (level == 0): 285 | cover = u'Clear \u263C'.encode('utf-8') 286 | else: cover = 'Cover Unknown' 287 | else: cover = 'Cover Unknown' 288 | 289 | if temp: 290 | if '/' in temp: 291 | temp = temp.split('/')[0] 292 | else: temp = temp.split('.')[0] 293 | if temp.startswith('M'): 294 | temp = '-' + temp[1:] 295 | try: temp = int(temp) 296 | except ValueError: temp = '?' 297 | else: temp = '?' 298 | 299 | if pressure: 300 | if pressure.startswith('Q'): 301 | pressure = pressure.lstrip('Q') 302 | if pressure != 'NIL': 303 | pressure = str(int(pressure)) + 'mb' 304 | else: pressure = '?mb' 305 | elif pressure.startswith('A'): 306 | pressure = pressure.lstrip('A') 307 | if pressure != 'NIL': 308 | inches = pressure[:2] + '.' + pressure[2:] 309 | mb = int(float(inches) * 33.7685) 310 | pressure = '%sin (%smb)' % (inches, mb) 311 | else: pressure = '?mb' 312 | 313 | if isinstance(temp, int): 314 | f = round((temp * 1.8) + 32, 2) 315 | temp = u'%s\u2109 (%s\u2103)'.encode('utf-8') % (f, temp) 316 | else: pressure = '?mb' 317 | if isinstance(temp, int): 318 | temp = u'%s\u2103'.encode('utf-8') % temp 319 | 320 | if cond: 321 | conds = cond 322 | cond = '' 323 | 324 | intensities = { 325 | '-': 'Light', 326 | '+': 'Heavy' 327 | } 328 | 329 | descriptors = { 330 | 'MI': 'Shallow', 331 | 'PR': 'Partial', 332 | 'BC': 'Patches', 333 | 'DR': 'Drifting', 334 | 'BL': 'Blowing', 335 | 'SH': 'Showers of', 336 | 'TS': 'Thundery', 337 | 'FZ': 'Freezing', 338 | 'VC': 'In the vicinity:' 339 | } 340 | 341 | phenomena = { 342 | 'DZ': 'Drizzle', 343 | 'RA': 'Rain', 344 | 'SN': 'Snow', 345 | 'SG': 'Snow Grains', 346 | 'IC': 'Ice Crystals', 347 | 'PL': 'Ice Pellets', 348 | 'GR': 'Hail', 349 | 'GS': 'Small Hail', 350 | 'UP': 'Unknown Precipitation', 351 | 'BR': 'Mist', 352 | 'FG': 'Fog', 353 | 'FU': 'Smoke', 354 | 'VA': 'Volcanic Ash', 355 | 'DU': 'Dust', 356 | 'SA': 'Sand', 357 | 'HZ': 'Haze', 358 | 'PY': 'Spray', 359 | 'PO': 'Whirls', 360 | 'SQ': 'Squalls', 361 | 'FC': 'Tornado', 362 | 'SS': 'Sandstorm', 363 | 'DS': 'Duststorm', 364 | # ? Cf. http://swhack.com/logs/2007-10-05#T07-58-56 365 | 'TS': 'Thunderstorm', 366 | 'SH': 'Showers' 367 | } 368 | 369 | for c in conds: 370 | if c.endswith('//'): 371 | if cond: cond += ', ' 372 | cond += 'Some Precipitation' 373 | elif len(c) == 5: 374 | intensity = intensities[c[0]] 375 | descriptor = descriptors[c[1:3]] 376 | phenomenon = phenomena.get(c[3:], c[3:]) 377 | if cond: cond += ', ' 378 | cond += intensity + ' ' + descriptor + ' ' + phenomenon 379 | elif len(c) == 4: 380 | descriptor = descriptors.get(c[:2], c[:2]) 381 | phenomenon = phenomena.get(c[2:], c[2:]) 382 | if cond: cond += ', ' 383 | cond += descriptor + ' ' + phenomenon 384 | elif len(c) == 3: 385 | intensity = intensities.get(c[0], c[0]) 386 | phenomenon = phenomena.get(c[1:], c[1:]) 387 | if cond: cond += ', ' 388 | cond += intensity + ' ' + phenomenon 389 | elif len(c) == 2: 390 | phenomenon = phenomena.get(c, c) 391 | if cond: cond += ', ' 392 | cond += phenomenon 393 | 394 | # if not cond: 395 | # format = u'%s at %s: %s, %s, %s, %s' 396 | # args = (icao, time, cover, temp, pressure, wind) 397 | # else: 398 | # format = u'%s at %s: %s, %s, %s, %s, %s' 399 | # args = (icao, time, cover, temp, pressure, cond, wind) 400 | 401 | if not cond: 402 | format = u'%s, %s, %s, %s - %s %s' 403 | args = (cover, temp, pressure, wind, str(icao_code), time) 404 | else: 405 | format = u'%s, %s, %s, %s, %s - %s, %s' 406 | args = (cover, temp, pressure, cond, wind, str(icao_code), time) 407 | 408 | self.msg(origin.sender, format.encode('utf-8') % args) 409 | f_weather.rule = (['weather'], r'(.*)') 410 | 411 | if __name__ == '__main__': 412 | print __doc__.strip() 413 | --------------------------------------------------------------------------------