pythonverse

Python-based client for OpenVerse with extra features
git clone https://code.literati.org/pythonverse.git
Log | Files | Refs | README | LICENSE

commit bdfd37ebcee0f36b14623d3b5a7faf5428995196
parent fea74af58bb80cf74ceb0070c40fb34f4c96c4b0
Author: seanl <seanl>
Date:   Sat, 23 Mar 2002 01:20:02 +0000

Added a bunch of sample files for how to write a frontend for PV.

Diffstat:
MOpenVerse.py | 161++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mclient.py | 7+++++--
Afroggirl.py | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Akanjidic.py | 368+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amicrohal.py | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtransutil.py | 12++++++++----
Awebutil.py | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 937 insertions(+), 77 deletions(-)

diff --git a/OpenVerse.py b/OpenVerse.py @@ -145,49 +145,16 @@ class DCCSendPassive(DCC): class ServerConnection(transutil.Connection): def __init__(self, host, port, client, nick, avatar): - transutil.Connection.__init__(self, transutil.InputHandler( - (('ABOVE', self.cmd_ABOVE, '(\S+)', (str,)), - ('CHAT', self.cmd_CHAT, '(\S+) (.*)', (str, str)), - ('SCHAT', self.cmd_SCHAT, '(\S+) (\S+) (.*)', (str, str, str)), - ('MOVE', self.cmd_MOVE, '(\S+) (\d+) (\d+) (\d+)', - (str, int, int, int)), - ('EFFECT', self.cmd_EFFECT, '(\S+) (\S+)', (str, str)), - ('PRIVMSG', self.cmd_PRIVMSG, '(\S+) (.*)', - (str, str)), - ('AVATAR', self.cmd_AVATAR, - '(\S+) (\S+) (-?\d+) (-?\d+) (\d+) (-?\d+) (-?\d+)', - (str, str, int, int, int, int, int)), - ('URL', self.cmd_URL, '(\S+) (.*)', (str, str)), - ('NEW', self.cmd_NEW, - '(\S+) (\d+) (\d+) (\S+) (-?\d+) (-?\d+) (\d+) (-?\d+) (-?\d+)', - (str, int, int, str, int, int, int, int, int)), - ('NOMORE', self.cmd_NOMORE, '(\S+)', (str,)), - ('EXIT_OBJ', self.cmd_EXIT_OBJ, - '(\S+) (-?\d+) (-?\d+) (-?\d+) (-?\d+) (\d+) (\S+) (\d+)', - (str, int, int, int, int, int, str, int)), - ('DCCGETAV', self.cmd_DCCGET, '(\d+) (\S+) (\d+)', - (int, str, int)), - ('DCCGETROOM', self.cmd_DCCGET, '(\d+) (\S+) (\d+)', - (int, str, int)), - ('DCCGETOB', self.cmd_DCCGET, '(\d+) (\S+) (\d+)', - (int, str, int)), - ('PING', self.cmd_PING, '', ()), - ('ROOM', self.cmd_ROOM, '(\S+) (\d+)', (str, int)), - ('ROOMNAME', self.cmd_ROOMNAME, '(.*)', (str,)), - ('MOUSEOVER', self.cmd_MOUSEOVER, - '(\S+) (\d+) (\d+) (\S+) (\d+) (\S+) (\d+) (\d+)', - (str, int, int, str, int, str, int, int)), - ('DCCSENDAV', self.cmd_DCCSENDAV, '(\d+) (\S+)', (int, str)), - ('SUB', self.cmd_SUB, '(\S+) (\S+) (.*)', (str, str, str)), - ('WHOIS', self.cmd_WHOIS, '(\S+) (.*)', (str, str))))) + transutil.Connection.__init__(self) self.host = host self.port = port self.pending_images = {} self.client = client self.images = {} self.nick = nick - avdata = self.parse_anim(avatar) - self.avatar_filename = avdata[0] + if type(avatar) == type(''): avdata = self.parse_anim(avatar) + else: avdata = avatar + self.avatar_filename = avdata[0] self.nx = avdata[1] self.ny = avdata[2] self.bx = avdata[3] @@ -195,22 +162,25 @@ class ServerConnection(transutil.Connection): # Connect last self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) - + + # Handlers for asynchat def handle_connect(self): - size = os.stat(os.path.join(IMAGEDIR, - self.avatar_filename))[stat.ST_SIZE] + size = os.stat(self.avatar_filename)[stat.ST_SIZE] self.write("AUTH %s %d %d %s %d %d %d %d %d\r\n" % - (self.nick, 320, 200, self.avatar_filename, self.nx, self.ny, + (self.nick, 320, 200, os.path.basename(self.avatar_filename), self.nx, self.ny, size, self.bx, self.by)) def handle_close(self): transutil.Connection.handle_close(self) self.client.close() + # Utility functions + def set_client(self, client): + """Change the client object that this object calls to""" + self.client = client + def debug(self, info): self.client.debug(info) - - # Utility functions def get_image(self, filename, size, command, pcallback, callback, args=()): # Prevent possible embedded '/' attacks @@ -269,7 +239,7 @@ class ServerConnection(transutil.Connection): def move(self, pos): x, y = pos - self.write('MOVE %s %d %d 1\r\n' % (self.nick, x, y)) + self.write('MOVE %s %d %d 20\r\n' % (self.nick, x, y)) def push(self): self.write('PUSH 100\r\n') @@ -278,11 +248,13 @@ class ServerConnection(transutil.Connection): self.write('EFFECT %s\r\n' % action.lower()) def privmsg(self, nicks, text): + if type(nicks) == type(''): nicks = [nicks] for n in nicks: self.write('PRIVMSG %s %s\r\n' % (n, text)) return self.nick def url(self, nicks, url): + if type(nicks) == type(''): nicks = [nicks] for n in nicks: self.write('URL %s %s\r\n' % (n, url)) @@ -304,7 +276,8 @@ class ServerConnection(transutil.Connection): self.write('%s\r\n' % text) def set_avatar(self, avatar): - av = self.parse_anim(avatar) + if type(avatar) == type(''): av = self.parse_anim(avatar) + else: av = avatar try: size = os.stat(os.path.join(IMAGEDIR, av[0]))[stat.ST_SIZE] except OSError, info: self.client.debug(info) else: @@ -314,7 +287,7 @@ class ServerConnection(transutil.Connection): self.bx = av[3] self.by = av[4] self.write('AVATAR %s %d %d %d %d %d\r\n' % - (av[0], av[1], av[2], size, av[3], av[4])) + (os.path.basename(av[0]), av[1], av[2], size, av[3], av[4])) def parse_anim(self, avatar): """Parse the avatar definition file for information""" @@ -342,7 +315,7 @@ class ServerConnection(transutil.Connection): by = int(splitanimitem[2]) if splitanimitem[1] == "MV(anim.0)": avimage = splitanimitem[2].strip() - return [avimage, nx, ny, bx, by] + return os.path.join(IMAGEDIR, avimage), nx, ny, bx, by def whois(self, nick): self.write('WHOIS %s\r\n' % nick) @@ -352,35 +325,46 @@ class ServerConnection(transutil.Connection): # Command handlers - def cmd_ABOVE(self, name): + def cmd_ABOVE(self, line): """Raise the named object to the top of the stacking order""" - self.client.raise_object(name) + self.client.raise_object(line.split()[1]) - def cmd_CHAT(self, nick, text): + def cmd_CHAT(self, line): + cmd, nick, text = line.split(' ', 2) self.client.chat(nick, text) - def cmd_SCHAT(self, emote, nick, text): + def cmd_SCHAT(self, line): + cmd, emote, nick, text = line.split(' ', 3) self.client.chat(nick, '*%s* %s' % (emote, text)) - def cmd_MOVE(self, nick, x, y, speed): - # FIXME: Need to use speed too - self.client.move_avatar(nick, x, y, speed) + def cmd_MOVE(self, line): + cmd, nick, x, y, speed = line.split() + self.client.move_avatar(nick, int(x), int(y), int(speed)) - def cmd_EFFECT(self, nick, action): + def cmd_EFFECT(self, line): + cmd, nick, action = line.split() self.client.effect(nick, action) - def cmd_PRIVMSG(self, nick, text): - #PRIVMSG nick text / nick text + def cmd_PRIVMSG(self, line): + cmd, nick, text = line.split(' ', 2) self.client.privmsg(nick, text) - def cmd_ROOM(self, filename, filesize): + def cmd_ROOM(self, line): """Set a new background image""" + cmd, filename, filesize = line.split() + filesize = int(filesize) image = self.get_image(filename, filesize, 'DCCSENDROOM', self.client.background_progress, self.client.background_image) if image is not None: self.client.background_image(image) - def cmd_AVATAR(self, nick, filename, nx, ny, size, bx, by): + def cmd_AVATAR(self, line): + cmd, nick, filename, nx, ny, size, bx, by = line.split() + nx = int(nx) + ny = int(ny) + size = int(size) + bx = int(bx) + by = int(by) image = self.get_image(filename, size, 'DCCSENDAV', self.client.avatar_progress, self.client.avatar_image, (nick,)) @@ -390,13 +374,22 @@ class ServerConnection(transutil.Connection): # here because it's OV specific. self.client.avatar(nick, image, (nx, ny), (bx-BALLOONXOFFSET, by)) - def cmd_PING(self): + def cmd_PING(self, line): self.write('PONG\r\n') - def cmd_URL(self, nick, text): + def cmd_URL(self, line): + cmd, nick, text = line.split(' ', 2) self.client.url(nick, text) - def cmd_NEW(self, nick, x, y, filename, nx, ny, size, bx, by): + def cmd_NEW(self, line): + cmd, nick, x, y, filename, nx, ny, size, bx, by = line.split() + x = int(x) + y = int(y) + nx = int(nx) + ny = int(ny) + size = int(size) + bx = int(bx) + by = int(by) image = self.get_image(filename, size, 'DCCSENDAV', self.client.avatar_progress, self.client.avatar_image, (nick,)) @@ -404,27 +397,45 @@ class ServerConnection(transutil.Connection): self.client.new_avatar(nick, (x, y), image, (nx, ny), (bx-BALLOONXOFFSET, by)) - def cmd_NOMORE(self, nick): + def cmd_NOMORE(self, line): + cmd, nick = line.split() self.client.del_avatar(nick) - def cmd_EXIT_OBJ(self, name, x1, y1, x2, y2, duration, host, port): + def cmd_EXIT_OBJ(self, line): + cmd, name, x1, y1, x2, y2, duration, host, port = line.split() self.client.exit_obj(name, host, port) - def cmd_DCCGET(self, port, filename, size): + def cmd_DCCGETAV(self, line): + cmd, port, filename, size = line.split() + port = int(port) + size = int(size) DCCGet(self.host, port, filename, size, self.progress_callback, self.image_callback) - def cmd_DCCSENDAV(self, port, filename): + def cmd_DCCGETROOM(self, line): return self.cmd_DCCGETAV(line) + + def cmd_DCCGETOB(self, line): return self.cmd_DCCGETAV(line) + + def cmd_DCCSENDAV(self, line): + cmd, port, filename = line.split() + port = int(port) filename = os.path.join(IMAGEDIR, os.path.basename(filename)) try: size = os.stat(filename)[stat.ST_SIZE] DCCSendPassive(self.host, port, filename, size, None, None) except IOError, info: self.debug(info) - def cmd_ROOMNAME(self, name): + def cmd_ROOMNAME(self, line): + cmd, name = line.split(' ', 1) self.client.set_title(name) - def cmd_MOUSEOVER(self, name, x, y, image1, size1, image2, size2, flag): + def cmd_MOUSEOVER(self, line): + cmd, name, x, y, image1, size1, image2, size2, flag = line.split() + x = int(x) + y = int(y) + size1 = int(size1) + size2 = int(size2) + flag = int(flag) image1 = self.get_image(image1, size1, 'DCCSENDOB', None, self.client.mouseover_image1, (name,)) image2 = self.get_image(image2, size2, 'DCCSENDOB', None, @@ -433,11 +444,19 @@ class ServerConnection(transutil.Connection): if image2 is None: image2 = self.client.newimage() self.client.mouseover(name, (x, y), image1, image2) - def cmd_SUB(self, nick, command, cmdargs): - print 'TODO: Implement SUB' + def cmd_SUB(self, line): + self.client.debug('TODO: Implement SUB') - def cmd_WHOIS(self, nick, text): + def cmd_WHOIS(self, line): + cmd, nick, text = line.split(' ', 2) self.client.chat(nick, '*%s* is %s' % (nick, text)) + + def cmd_PUSH(self, line): + cmd, x, y, speed = line.split() + self.client.push(int(x), int(y), int(speed)) + + def unknown(self, line): + self.client.debug('Unknown: %s' % line) def poll(): diff --git a/client.py b/client.py @@ -28,6 +28,8 @@ class Client: def debug(self, t): print >> sys.stderr, t + def close(self): pass + def background_image(self, image): pass def background_progress(self, length, filename, size): pass @@ -66,4 +68,6 @@ class Client: def privmsg(self, nick, s): pass - def chat(self, nick, s): pass- \ No newline at end of file + def chat(self, nick, s): pass + + def push(self, x, y, speed): pass diff --git a/froggirl.py b/froggirl.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org> +# +# This file is part of PythonVerse. +# +# PythonVerse is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PythonVerse is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PythonVerse; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# -*-Python-*- + +import sys, os, string, re, asyncore, codecs, traceback, whrandom, time +import OpenVerse, client +import webutil, kanjidic, microhal + +#os.environ['WNHOME'] = '/usr/share/wordnet' +#os.environ['WNSEARCHDIR'] = '/usr/share/wordnet' + +#from wordnet import * +#from wntools import * + +languages = {'e': 'en', + 'f': 'fr', + 'i': 'it', + 'g': 'de', + 'p': 'pt', + 's': 'es', + 'j': 'ja'} + +fromutf8 = codecs.lookup('UTF8')[1] +fromlatin1 = codecs.lookup('ISO-8859-1')[1] + +class HalClient(client.Client): + commands = [('([efigpsj])((2[efigpsj])+)\s+(.+)$', + 'self.do_babel(m[1], m[2], m[4])', 0, 0), + ('identify (\w+)$', 'self.do_identify(nick, m[1])', 0, 2), + ('unidentify$', 'self.do_unidentify()', 2, 1), + ('move (\d+) (\d+)$', + 'self.do_move(int(m[1]), int(m[2]))', 1, 1), + ('(jump|shiver)', 'self.server.effect(m[1])', 1, 1), + ('avatar (\S+)$', 'self.server.set_avatar(m[1])', 1, 1), + ('die', 'self.server.close()', 1, 1), + ('google (.+)$', 'self.do_google(m[1])', 0, 0), + ('groups (.+)$', 'self.do_google(m[1], 1)', 0, 0), + ('image (.+)$', 'self.do_image(nick, m[1])', 0, 0), + ('say (.+)', 'self.server.chat(m[1])', 1, 1), + ('wx (\w+)$', 'webutil.metar(m[1]) or "No weather information available for " + m[1]', 0, 0), + ('weather (\d\d\d\d\d)$', 'webutil.weather(m[1])', 0, 0), + ('follow (\S+)$', 'self.do_follow(m[1])', 1, 1), + # Catch all semidirect statements + ('.*', 'self.do_reply(m[0])', 0, 1), + # Send everything else to megahal without replying + ('.*', 'self.do_random(m[0])', 0, 0)] + + def __init__(self, nick): + self.image_time = 0 + self.nick = nick + self.master = None + self.nicks = {} + self.pos = None + self.following = None + self.brain = microhal.load_brain('microhal.brn') + + def save(self): + print >> sys.stderr, 'Saving brain...' + self.brain.save('microhal.brn') + print >> sys.stderr, 'Done.' + + # commands. + def do_reply(self, sentence): + reply = self.brain.reply(sentence) + self.brain.train(sentence) + return reply + + def do_follow(self, nick): + self.following = nick + if self.nicks.has_key(nick): + x, y = self.nicks[nick] + self.do_move(x, y) + + def do_identify(self, nick, arg): + if arg == 'Rumpelstiltskin': + self.master = nick + return 'Your wish is my command.' + else: return 'Nice try.' + + def do_unidentify(self): + self.master = None + return 'Your wish is no longer my command' + + def do_babel(self, lang1, langs, phrase): + lang1 = languages[lang1] + r = fromlatin1(phrase)[0].encode('UTF8') + for l in langs[1:].split('2'): + lang2 = languages[l] + print 'Translating %s from %s to %s' % (r, lang1, lang2) + try: r = webutil.translate(r, lang1, lang2) + except: + exception, info = sys.exc_info()[:2] + r = ' '.join(traceback.format_exception_only(exception, info)) + traceback.print_exc() + break + else: + print r + lang1 = lang2 + + return kanjidic.romajify(fromutf8(r)[0]).encode('ISO-8859-1', 'replace') + + def do_google(self, phrase, groups=0): + hits = webutil.google(phrase, groups) + if not hits: return "Sorry, couldn't find anything." + hit = whrandom.choice(hits) + text = hit[2] + title = hit[1] + return '%s: %s' % (title, text) + + def do_image(self, nick, phrase): + now = time.time() + if now - self.image_time < 30: + self.server.privmsg(nick, 'Please wait at least %d seconds.' % (31-now+self.image_time)) + return + images = webutil.google_image(phrase) + # Filter out banners. + images = filter(lambda im: float(im[1]) / float(im[2]) < 3, images) + if not images: return 'Nothing found.' + imageurl = whrandom.choice(images)[0] + filename = os.path.expanduser('~/.OpenVerse/images/google.gif') + webutil.image(imageurl, filename) + self.server.set_avatar('google') + self.image_time = time.time() + + def do_random(self, phrase): + if whrandom.randint(1, 5) == 1: + r = self.do_reply(phrase) + self.server.chat(r) + else: self.brain.train(phrase) + + def do_move(self, x, y): + self.pos = (x, y) + self.server.move((x, y)) + + # Parse stuff + def parse(self, nick, s, private=0): + m = re.match('(\w+)[,:;]+\W*(.+)', s) + if m: + mynick = m.group(1) + s = m.group(2) + else: + mynick = None + + if self.master: + if nick == self.master: secure = 2 + else: secure = 0 + else: secure = 1 + + if private: direct = 2 + elif mynick and string.lower(mynick) == string.lower(self.nick): + direct = 1 + else: direct = 0 + + for regex, cmd, priv, d in self.commands: + if priv > secure: continue + if d > direct: continue + m = re.match(regex, s) + if m: + match = (m.group(0),) + tuple(m.groups()) + vars = {'self': self, 'nick': nick, 'm': match} + vars.update(m.groupdict()) + + try: r = eval(cmd, globals(), vars) + except SystemExit: + sys.exit() + except: + exception, info = sys.exc_info()[:2] + r = ' '.join(traceback.format_exception_only(exception, info)) + traceback.print_exc() + break + else: r = None + + if r: + if private: self.server.privmsg(nick, r) + else: self.server.chat(r) + + # Client methods + #def new_avatar(self, nick, pos, image, noffset, boffset): +# self.nicks[nick] = pos +# r = self.brain.reply(nick) +# self.server.chat(r) +# + def del_avatar(self, nick): + """Unbind from my master if s/he leaves.""" + if nick == self.master: self.master = None + try: del self.nicks[nick] + except KeyError: pass + + def move_avatar(self, nick, x, y, speed): + self.nicks[nick] = x, y + if self.following == nick: self.do_move(x, y) + + def privmsg(self, nick, s): + if self.master and nick != self.master: + self.server.privmsg([self.master], "<%s> %s" % (nick, s)) + + self.parse(nick, s, private=1) + + def chat(self, nick, s): + if nick == self.nick: return + self.parse(nick, s) + + def push(self, x, y, speed): + if self.pos: self.server.move(self.pos) + + +def main(argv, env): + nick = argv[1] + host = argv[2] + port = int(argv[3]) + avatar = argv[4] + + c = HalClient(nick) + server = OpenVerse.ServerConnection(host, port, c, nick, avatar) + c.set_server(server) + try: asyncore.loop() + finally: c.save() + +if __name__ == '__main__': + main(sys.argv, os.environ) diff --git a/kanjidic.py b/kanjidic.py @@ -0,0 +1,368 @@ +import string, japanese.euc_jp, re, unicodedata, shelve + +# Maximum length of a dictionary entry +MAXLEN = 38 + +kanji_dict = shelve.open('kanjidic.dat') +edict_dict = shelve.open('edict.dat') + +romaji_dict = { + u'\u3041': 'small a', + u'\u3042': 'a', + u'\u3043': 'small i', + u'\u3044': 'i', + u'\u3045': 'small u', + u'\u3046': 'u', + u'\u3047': 'small e', + u'\u3048': 'e', + u'\u3049': 'small o', + u'\u304a': 'o', + u'\u304b': 'ka', + u'\u304c': 'ga', + u'\u304d': 'ki', + u'\u304e': 'gi', + u'\u304f': 'ku', + u'\u3050': 'gu', + u'\u3051': 'ke', + u'\u3052': 'ge', + u'\u3053': 'ko', + u'\u3054': 'go', + u'\u3055': 'sa', + u'\u3056': 'za', + u'\u3057': 'shi', + u'\u3058': 'zi', + u'\u3059': 'su', + u'\u305a': 'zu', + u'\u305b': 'se', + u'\u305c': 'ze', + u'\u305d': 'so', + u'\u305e': 'zo', + u'\u305f': 'ta', + u'\u3060': 'da', + u'\u3061': 'chi', + u'\u3062': 'di', + u'\u3063': 'small tsu', + u'\u3064': 'tsu', + u'\u3065': 'du', + u'\u3066': 'te', + u'\u3067': 'de', + u'\u3068': 'to', + u'\u3069': 'do', + u'\u306a': 'na', + u'\u306b': 'ni', + u'\u306c': 'nu', + u'\u306d': 'ne', + u'\u306e': 'no', + u'\u306f': 'ha', + u'\u3070': 'ba', + u'\u3071': 'pa', + u'\u3072': 'hi', + u'\u3073': 'bi', + u'\u3074': 'pi', + u'\u3075': 'fu', + u'\u3076': 'bu', + u'\u3077': 'pu', + u'\u3078': 'he', + u'\u3079': 'be', + u'\u307a': 'pe', + u'\u307b': 'ho', + u'\u307c': 'bo', + u'\u307d': 'po', + u'\u307e': 'ma', + u'\u307f': 'mi', + u'\u3080': 'mu', + u'\u3081': 'me', + u'\u3082': 'mo', + u'\u3083': 'small ya', + u'\u3084': 'ya', + u'\u3085': 'small yu', + u'\u3086': 'yu', + u'\u3087': 'small yo', + u'\u3088': 'yo', + u'\u3089': 'ra', + u'\u308a': 'ri', + u'\u308b': 'ru', + u'\u308c': 're', + u'\u308d': 'ro', + u'\u308e': 'small wa', + u'\u308f': 'wa', + u'\u3090': 'wi', + u'\u3091': 'we', + u'\u3092': 'wo', + u'\u3093': 'n', + u'\u3094': 'vu', + u'\u30a1': 'SMALL A', + u'\u30a2': 'A', + u'\u30a3': 'SMALL I', + u'\u30a4': 'I', + u'\u30a5': 'SMALL U', + u'\u30a6': 'U', + u'\u30a7': 'SMALL E', + u'\u30a8': 'E', + u'\u30a9': 'SMALL O', + u'\u30aa': 'O', + u'\u30ab': 'KA', + u'\u30ac': 'GA', + u'\u30ad': 'KI', + u'\u30ad\u30e3': 'KYA', + u'\u30ad\u30e5': 'KYU', + u'\u30ad\u30e7': 'KYO', + u'\u30ae': 'GI', + u'\u30ae\u30e3': 'GYA', + u'\u30ae\u30e5': 'GYU', + u'\u30ae\u30e7': 'GYO', + u'\u30af': 'KU', + u'\u30b0': 'GU', + u'\u30b1': 'KE', + u'\u30b2': 'GE', + u'\u30b3': 'KO', + u'\u30b4': 'GO', + u'\u30b5': 'SA', + u'\u30b6': 'ZA', + u'\u30b7': 'SHI', + u'\u30b7\u30e3': 'JA', + u'\u30b7\u30e5': 'JU', + u'\u30b7\u30e7': 'JO', + u'\u30b8': 'ZI', + u'\u30b9': 'SU', + u'\u30ba': 'ZU', + u'\u30bb': 'SE', + u'\u30bc': 'ZE', + u'\u30bd': 'SO', + u'\u30be': 'ZO', + u'\u30bf': 'TA', + u'\u30c0': 'DA', + u'\u30c1': 'CHI', + u'\u30d2\u30e3': 'CHA', + u'\u30d2\u30e5': 'CHU', + u'\u30d2\u30e7': 'CHO', + u'\u30c2': 'DI', + u'\u30c3': 'SMALL TSU', + u'\u30c4': 'TSU', + u'\u30c5': 'DU', + u'\u30c6': 'TE', + u'\u30c7': 'DE', + u'\u30c8': 'TO', + u'\u30c9': 'DO', + u'\u30ca': 'NA', + u'\u30cb': 'NI', + u'\u30cc': 'NU', + u'\u30cd': 'NE', + u'\u30ce': 'NO', + u'\u30cf': 'HA', + u'\u30d0': 'BA', + u'\u30d1': 'PA', + u'\u30d2': 'HI', + u'\u30d2\u30e3': 'HYA', + u'\u30d2\u30e5': 'HYU', + u'\u30d2\u30e7': 'HYO', + u'\u30d3': 'BI', + u'\u30d3\u30e3': 'BYA', + u'\u30d3\u30e5': 'BYU', + u'\u30d3\u30e7': 'BYO', + u'\u30d4': 'PI', + u'\u30d4\u30e3': 'BYA', + u'\u30d4\u30e5': 'BYU', + u'\u30d4\u30e7': 'BYO', + u'\u30d5': 'FU', + u'\u30d6': 'BU', + u'\u30d7': 'PU', + u'\u30d8': 'HE', + u'\u30d9': 'BE', + u'\u30da': 'PE', + u'\u30db': 'HO', + u'\u30dc': 'BO', + u'\u30dd': 'PO', + u'\u30de': 'MA', + u'\u30df': 'MI', + u'\u30e0': 'MU', + u'\u30e1': 'ME', + u'\u30e2': 'MO', + u'\u30e3': 'SMALL YA', + u'\u30e4': 'YA', + u'\u30e5': 'SMALL YU', + u'\u30e6': 'YU', + u'\u30e7': 'SMALL YO', + u'\u30e8': 'YO', + u'\u30e9': 'RA', + u'\u30ea': 'RI', + u'\u30eb': 'RU', + u'\u30ec': 'RE', + u'\u30ed': 'RO', + u'\u30ee': 'SMALL WA', + u'\u30ef': 'WA', + u'\u30f0': 'WI', + u'\u30f1': 'WE', + u'\u30f2': 'WO', + u'\u30f3': 'N', + u'\u30f4': 'VU', + u'\u30f5': 'SMALL KA', + u'\u30f6': 'SMALL KE', + u'\u30f7': 'VA', + u'\u30f8': 'VI', + u'\u30f9': 'VE', + u'\u30fa': 'VO', + u'\u30fc': '-' +} + +class Kanji: + def __init__(self, code, readings, meanings): + self.code = code + self.readings = readings + self.meanings = meanings + +decode = japanese.euc_jp.getregentry()[1] +fields = 'UNBCSGHFPKLIQMEYWOVDXZ' +meaning_regex = re.compile('}\s*{') +def parse_kanji(line): + if not line or line[0] == '#': return None + line = decode(line)[0].split() + i = 2 + while i < len(line) and line[i][0] in fields: + if line[i][0] == 'U': code = int(line[i][1:], 16) + i = i + 1 + + readings = [] + while i < len(line) and line[i][0] not in 'T{': + readings.append(line[i]) + i = i + 1 + + if not readings: return None + + # Skip over the special readings for now + while i < len(line) and line[i][0] != '{': + i = i + 1 + + meanings = meaning_regex.split(' '.join(line[i:])[1:-1]) + + return Kanji(code, readings, meanings) + +def make_kanjidat(filename): + dict = shelve.open(filename, 'c') + infile = open('kanjidic') + while 1: + line = infile.readline() + if not line: break + line = line.strip() + kanji = parse_kanji(line) + if not kanji: continue + dict[unichr(kanji.code).encode('UTF16')] = kanji + + dict.close() + + +class Definition: + def __init__(self, kana, meaning): + self.kana = kana + self.meaning = meaning + + +edict_regex = re.compile('(\S+)(\s+\[([^\]]*)\])?\s+/([^/]*)/') +def make_edictdat(file, filename): + dict = shelve.open(filename, 'c') + maxlen = 0 + while 1: + line = file.readline() + if not line: break + line = line.strip() + if not line: continue + line = decode(line)[0] + m = edict_regex.match(line) + key = m.group(1).encode('UTF16') + kana = m.group(3) + meaning = m.group(4) + if dict.has_key(key): dict[key] = dict[key] + [Definition(kana, meaning)] + else: dict[key] = [Definition(kana, meaning)] + if len(key) > maxlen: maxlen = len(key) + + dict.close() + return maxlen + +def edict_lookup(text): + """Look up the longest match we can find""" + l = min(len(text), MAXLEN) + while l > 0: + key = text[:l].encode('UTF16') + if edict_dict.has_key(key): + return l, edict_dict[key] + + l = l - 1 + + return 0, None + +def kanji_lookup(kanji): + key = kanji.encode('UTF16') + try: return kanji_dict[key] + except KeyError: return None + + +KANA_LOW = 0x3040 + +def kana_romaji(text): + global romaji_dict + + lastchar = None + r = '' + for char in text: + if ord(char) < KANA_LOW: + r = r + char + continue + if char == u'\u30fc': + # Extend the sound + r = r + r[-1] + continue + try: romaji = romaji_dict[char] + except KeyError: + print 'Cannot translate %r' % char + r = r + char + else: + if romaji[:6].lower() == 'small ': + if romaji[6].lower() == 'y': + # Back up by one and write the character + r = r[:-1] + romaji[6:] + elif romaji[6:].lower() == 'tsu': r = r + r[-1] + else: r = r + romaji + else: r = r + romaji + + return r + +def romajify(text): + r = '' + i = 0 + while i < len(text): + # Find Japanese sections to convert + while i < len(text) and ord(text[i]) < KANA_LOW: + r = r + text[i] + i = i + 1 + + if i >= len(text): break + # Try to find the longest definition in the dictionary + matchlen, definitions = edict_lookup(text[i:]) + if definitions is not None: + r = r + '[' + ' | '.join(map(lambda d, t=text[i:i+matchlen]: '%s: %s' % (d.kana or t, d.meaning), definitions)) + ']' + i = i + matchlen + else: + kanji = kanji_lookup(text[i]) + if kanji is None: + r = r + text[i] + i = i + 1 + else: + r = r + '{' + kana_romaji('|'.join(kanji.readings)) + meanings = kanji.meanings + if meanings: + r = r + ': ' + ', '.join(meanings) + + r = r + ']' + + return kana_romaji(r) + +romaji_regex = re.compile('(HIRAGANA|KATAKANA) LETTER (.+)') +def make_dict(): + for i in range(65535): + char = unichr(i) + m = romaji_regex.match(unicodedata.name(char, '')) + if m: + if m.group(1) == 'HIRAGANA': print ' %r: %r,' % (char, m.group(2).lower()) + else: print ' %r: %r,' % (char, m.group(2)) + + diff --git a/microhal.py b/microhal.py @@ -0,0 +1,137 @@ +#!/usr/bin/python + +import re, string, whrandom +from math import * +from cPickle import dump, load + +class Entry: + def __init__(self, key): + self.key = key + self.freqs = {} + self.count = 0 + + def add_symbol(self, symbol): + try: self.freqs[symbol] = self.freqs[symbol] + 1 + except KeyError: self.freqs[symbol] = 1 + self.count = self.count + 1 + + def remove_symbol(self, symbol): + """Remove a symbol from this entry, returning the remaining count""" + try: freq = self.freqs[symbol] + except KeyError: pass + else: + del self.freqs[symbol] + self.count = self.count - freq + return self.count + + def choose(self): + """Pick a random symbol from the list of symbols with the correct probability.""" + total = 0 + n = whrandom.randint(1, self.count) + for symbol, freq in self.freqs.items(): + total = total + freq + if n <= total: return symbol, -log(float(freq)/float(self.count))/log(2) + + +class Brain: + split_re = re.compile('(\W+)') + whitespace_re = re.compile('\s+') + + def __init__(self): + self.leaders = {} + self.followers = {} + + def parse(self, sentence): + """Convert all whitespace to a single space, split up the sentence into alternating words and non-words, and remove any empty symbols.""" + return map(intern, filter(lambda s: s, self.split_re.split(self.whitespace_re.sub(' ', sentence.strip()).lower()))) + + def train(self, sentence): + # Split up the sentence and add sentinels at the beginning and end + sentence = [None, None, None, None] + self.parse(sentence) + [None, None, None, None] + for i in range(len(sentence) - 4): + symbol, sym1, sym2, sym3, sym4 = sentence[i:i+5] + + key = symbol, sym1, sym2, sym3 + try: entry = self.followers[key] + except KeyError: + entry = Entry(key) + self.followers[key] = entry + + entry.add_symbol(sym4) + + key = sym1, sym2, sym3, sym4 + try: entry = self.leaders[key] + except KeyError: + entry = Entry(key) + self.leaders[key] = entry + + entry.add_symbol(symbol) + + def train_from_file(self, file): + if type(file) == type(''): file = open(file) + line = file.readline() + while line: + if line[0] != '#': self.train(line) + line = file.readline() + + def generate_replies(self, keywords, n=10): + """Generate a random sentence from a keyword.""" + # Initial seed is the word preceded by randomly selected preceder symbols + # Pick one seed per keyword + seeds = [] + for keyword in keywords: + candidates = filter(lambda key, k=keyword: key[3] == k, self.followers.keys()) + if candidates: seeds.append(whrandom.choice(candidates)) + + if not seeds: + # Pick n random seeds + keys = self.followers.keys() + for i in range(n): seeds.append(whrandom.choice(keys)) + + replies = [] + for i in range(n): + seed = whrandom.choice(seeds) + replies.append(self.generate_sentence(list(seed))) + + return replies + + def generate_sentence(self, sentence): + """Destructively modifies the seed!""" + total_entropy = 0.0 + symbols = 0 + while sentence[-1]: + symbol, entropy = self.followers[tuple(sentence[-4:])].choose() + sentence.append(symbol) + total_entropy = total_entropy + entropy + symbols = symbols + 1 + + # Generate the beginning of the sentence. + while sentence[0]: + symbol, entropy = self.leaders[tuple(sentence[:4])].choose() + sentence = [symbol] + sentence + total_entropy = total_entropy + entropy + symbols = symbols + 1 + + # Strip sentinels + while not sentence[0]: del sentence[0] + while not sentence[-1]: del sentence[-1] + + return total_entropy / symbols, sentence + + def reply(self, sentence): + # Pick only word symbols that are in the frequency dict + keywords = filter(lambda k: k[0] in string.letters, self.parse(sentence)) + # Generate 10 replies for now + candidates = self.generate_replies(keywords, 10) + candidates.sort() + return ''.join(candidates[-1][1]).capitalize() + + def save(self, file): + if type(file) == type(''): file = open(file, 'w') + dump(self, file, 1) + + +def load_brain(file): + if type(file) == type(''): file = open(file, 'r') + return load(file) + diff --git a/transutil.py b/transutil.py @@ -60,11 +60,10 @@ class InputHandler: class Connection(asynchat.async_chat): """Connection object that can handle a client or server""" - def __init__(self, handler, sock=None): + def __init__(self, sock=None): asynchat.async_chat.__init__(self, sock) self.buffer = '' self.outbuf = '' - self.handler = handler self.set_terminator('\r\n') # asynchat/asyncore handlers @@ -80,8 +79,11 @@ class Connection(asynchat.async_chat): def found_terminator(self): self.debug('-> %s' % string.strip(self.buffer)) - try: self.handler.handle(self.buffer) - except HandlerError, info: self.debug(str(info)) + data = self.buffer.strip() + cmd = data.split(' ', 1)[0] + try: meth = getattr(self, 'cmd_' + cmd) + except AttributeError: meth = self.unknown + meth(data) self.buffer = '' def writable(self): @@ -95,3 +97,5 @@ class Connection(asynchat.async_chat): self.debug("<- %s" % string.strip(data)) self.outbuf = self.outbuf + data + def unknown(self): pass + diff --git a/webutil.py b/webutil.py @@ -0,0 +1,92 @@ +import urllib, re, string, Image, os +from htmlentitydefs import entitydefs + +babel_regex = re.compile('name="q">([^<]+)</textarea>', re.MULTILINE) +def translate(phrase, lang1, lang2): + params = urllib.urlencode( { 'BabelFishFrontPage' : 'yes', + 'doit' : 'done', + 'urltext' : phrase, + 'lp' : lang1 + '_' + lang2 } ) + data = urllib.urlopen('http://babelfish.altavista.com/tr', params).read() + m = babel_regex.search(data) + if m: return string.strip(m.group(1)) + else: return 'Cannot translate' + +def weather(zip): + forecast='' + temp='' + wind='' + reported='' + msg=None + + wurl=urllib.urlopen("http://www.weather.com/weather/local/%s" % (zip)) + for line in wurl.readlines(): + if string.find(line, 'insert forecast text') != -1: + forecast=string.split(string.split(line, '-->')[1], '</td>')[0] + elif string.find(line, 'insert current temp') != -1: + temp=string.split(string.split(line, '-->')[1], '&')[0] + elif string.find(line, 'insert wind information') != -1: + wind=string.split(string.split(line, '-->')[1], '&')[0] + elif string.find(line, 'insert reported by and last updated info') != -1: + reported=string.strip(string.join(string.split(string.split(line, '-->')[1], '&nbsp;'))) + + if forecast != '' and temp != '' and wind != '' and reported != '': + msg="Current Forcast: %s. Temp: %sF. Wind: %smph (%s)" % (forecast, temp, wind,reported) + return msg + + +metar_regex = re.compile('The observation is:(<[^>]*>|\s+)*([^<]+)<') + +def metar(station): + data = urllib.urlopen("http://weather.noaa.gov/cgi-bin/mgetmetar.pl?cccc=%s" % station).read() + m = metar_regex.search(data) + if not m: return + return string.strip(m.group(2)) + + +image_regex = re.compile('<img src=(/images\?q=tbn:\S+) width=(\d+) height=(\d+)') + +def google_image(phrase): + data = urllib.urlopen('http://images.google.com/images?q=%s&imgsafe=off' % urllib.quote_plus(phrase)).read() + images = [] + while data: + m = image_regex.search(data) + if not m: break + images.append(('http://images.google.com%s' % m.group(1), int(m.group(2)), int(m.group(3)))) + data = data[m.end():] + return images + +def image(url, filename): + localfile = urllib.urlretrieve(url)[0] + im = Image.open(localfile).convert('P', palette=Image.ADAPTIVE) + im.save(filename) + urllib.urlcleanup() + + +clean_regex = re.compile('&(\w+);') + +def clean(text): + data = clean_regex.split(text) + for i in range(1, len(data), 2): + data[i] = entitydefs[data[i]] + + return ''.join(data) + + +google_regex = re.compile('<p><a href=([^>]+)>(.*)</a>(.*)') +tag_regex = re.compile('<[^>]*>') + +def google(phrase, groups=0): + phrase = urllib.quote_plus(phrase) + if groups: searchurl = 'http://groups.google.com/groups?q=%s' % phrase + else: searchurl = 'http://www.google.com/search?q=%s' % phrase + data = urllib.urlopen(searchurl).read() + hits = [] + data = google_regex.split(data) + for i in range(1, len(data), 4): + url = data[i] + title = clean(''.join(tag_regex.split(data[i+1]))) + text = clean(''.join(tag_regex.split(data[i+2]))) + hits.append((url, title, text)) + + return hits