# Copyright (c) 2002 Sean R. Lynch # # 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 # import sys, os, asyncore, asynchat, socket, string, struct, stat import transutil # Global constants are all caps; global variables start with _ BALLOONXOFFSET = 15 HOME = os.path.expanduser('~/.OpenVerse') ANIMDIR = os.path.join(HOME, 'anims') DLDIR = os.path.join(HOME, 'download') ICONDIR = os.path.join(HOME, 'icons') IMAGEDIR = os.path.join(HOME, 'images') OBJDIR = os.path.join(HOME, 'objects') RIMAGEDIR = os.path.join(HOME, 'rimages') ROOMDIR = os.path.join(HOME, 'rooms') def checkcache(filename, size): try: s = os.stat(filename)[stat.ST_SIZE] except OSError: return None else: if s == size or size < 0: return filename class DCC(asyncore.dispatcher): def __init__(self, host, port, filename, size, progress_callback, close_callback, sock=()): asyncore.dispatcher.__init__(self, sock) self.host = host self.port = port self.filename = filename self.size = size self.length = 0 self.outbuf = '' self.buffer = '' self.progress_callback = progress_callback self.close_callback = close_callback def __repr__(self): return '' % (self.filename, self.host, self.port, self.length, self.size) def handle_connect(self): pass def handle_write(self): sent = self.send(self.outbuf) self.outbuf = self.outbuf[sent:] def writable(self): return len(self.outbuf) > 0 def handle_close(self): print self, 'closing' self.close() if self.close_callback is not None: apply(self.close_callback, (self.tempfilename, self.filename, self.size)) class DCCGet(DCC): def __init__(self, host, port, filename, size, progress_callback, close_callback): DCC.__init__(self, host, port, filename, size, progress_callback, close_callback) self.tempfilename = os.path.join(RIMAGEDIR, filename) self.file = open(self.tempfilename, 'wb') self.size = size self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) def handle_read(self): data = self.recv(4096) if data: self.file.write(data) self.length = self.length + len(data) self.outbuf = self.outbuf + struct.pack('>I', self.length) if self.progress_callback is not None: apply(self.progress_callback, (self.length, self.tempfilename, self.filename, self.size)) if self.length == self.size: self.file.close() class DCCSendPassive(DCC): def __init__(self, host, port, filename, size, progress_callback, close_callback): DCC.__init__(self, host, port, filename, size, progress_callback, close_callback) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) self.file = open(filename, 'rb') self.outbuf = self.file.read(4096) self.sent = 0 def handle_read(self): data = self.recv(4) print self, 'received %d bytes' % len(data) if data: data = self.buffer + data # Drop all but one length received drop = len(data) / 4 * 4 - 4 data = data[drop:] if len(data) >= 4: # Get the number of bytes received by the client (self.length,) = struct.unpack('>I', data[:4]) self.buffer = data[4:] print self if self.progress_callback is not None: apply(self.progress_callback, (self.length, self.filename, self.size)) if self.length == self.size: self.file.close() self.handle_close() def handle_write(self): #if self.length < self.sent: return sent = self.send(self.outbuf) self.outbuf = self.outbuf[sent:] self.sent = self.sent + sent if len(self.outbuf) < 4096: self.outbuf = self.outbuf + self.file.read(4096) 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))))) 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] self.nx = avdata[1] self.ny = avdata[2] self.bx = avdata[3] self.by = avdata[4] # Connect last self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) def handle_connect(self): size = os.stat(os.path.join(IMAGEDIR, 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, size, self.bx, self.by)) def handle_close(self): transutil.Connection.handle_close(self) self.client.close() def debug(self, info): self.client.debug(info) # Utility functions def get_image(self, filename, size, command, pcallback, callback, args=()): # Prevent possible embedded '/' attacks filename = os.path.basename(filename) if filename == 'default.gif': size = -1 try: image = self.images[filename, size] except KeyError: # Check in locally cached images file = checkcache(os.path.join(RIMAGEDIR, filename), size) if file is None: # Check my own avatars as well file = checkcache(os.path.join(IMAGEDIR, filename), size) if file is not None: image = self.client.newimage(file) self.images[filename, size] = image return image blob = (pcallback, callback, args) # Take the callback out if it's already in there for pending in self.pending_images.values(): if blob in pending: pending.remove(blob) try: self.pending_images[filename, size].append(blob) except KeyError: self.pending_images[filename, size] = [blob] else: pending.append(blob) self.write('%s %s\r\n' % (command, filename)) else: return image def progress_callback(self, length, tempfilename, filename, size): for pcallback, callback, args in self.pending_images[(filename, size)]: if pcallback: apply(pcallback, args + (length,tempfilename,size)) def image_callback(self, tempfilename, filename, size): image = self.client.newimage(tempfilename) self.images[(filename, size)] = image for pcallback, callback, args in self.pending_images[(filename, size)]: apply(callback, args + (image,)) # Client functions def getnick(self): return self.nick def gethostport(self): return (self.host, self.port) def new_connect(self, host, port): self.close() self.host = host self.port = port self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) def move(self, pos): x, y = pos self.write('MOVE %s %d %d 1\r\n' % (self.nick, x, y)) def push(self): self.write('PUSH 100\r\n') def effect(self, action): self.write('EFFECT %s\r\n' % action.lower()) def privmsg(self, nicks, text): for n in nicks: self.write('PRIVMSG %s %s\r\n' % (n, text)) return self.nick def url(self, nicks, url): for n in nicks: self.write('URL %s %s\r\n' % (n, url)) def chat(self, text): self.write('CHAT %s\r\n' % text) def set_nick(self, nick): self.nick = nick self.write('NICK %s\r\n' % nick) def ignore(self, what, nick): self.write('IGNORE %s %s\r\n' % (what.upper(), nick)) def unignore(self, what, nick): self.write('UNIGNORE %s %s\r\n' % (what.upper(), nick)) def quote(self, text): """Send raw commands to the server""" self.write('%s\r\n' % text) def set_avatar(self, avatar): av = self.parse_anim(avatar) try: size = os.stat(os.path.join(IMAGEDIR, av[0]))[stat.ST_SIZE] except OSError, info: self.client.debug(info) else: self.avatar_filename = av[0] self.nx = av[1] self.ny = av[2] 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])) def parse_anim(self, avatar): """Parse the avatar definition file for information""" try: avfile = open(os.path.join(ANIMDIR, avatar), 'r') except: try: avfile = open(os.path.join(ANIMDIR, avatar + '.av'), 'r') except: print "Avatar", avatar, "not found." if self.avatar_filename is not None: return [self.avatar_filename, self.nx, self.ny, self.bx, self.by] return ["default.gif", 0, 36, 24, 6] animdata = avfile.readlines() avfile.close() for animitem in animdata: # parse the whole file for future addition of animation splitanimitem = animitem.split() if splitanimitem[1] == "MV(anim.x_off)": nx = int(splitanimitem[2]) if splitanimitem[1] == "MV(anim.y_off)": ny = int(splitanimitem[2]) if splitanimitem[1] == "MV(anim.baloon_x)": bx = int(splitanimitem[2]) if splitanimitem[1] == "MV(anim.baloon_y)": by = int(splitanimitem[2]) if splitanimitem[1] == "MV(anim.0)": avimage = splitanimitem[2].strip() return [avimage, nx, ny, bx, by] def whois(self, nick): self.write('WHOIS %s\r\n' % nick) def whichmouseover(self, name): self.client.setmouseover(name) # Command handlers def cmd_ABOVE(self, name): """Raise the named object to the top of the stacking order""" self.client.raise_object(name) def cmd_CHAT(self, nick, text): self.client.chat(nick, text) def cmd_SCHAT(self, emote, nick, text): 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_EFFECT(self, nick, action): self.client.effect(nick, action) def cmd_PRIVMSG(self, nick, text): #PRIVMSG nick text / nick text self.client.privmsg(nick, text) def cmd_ROOM(self, filename, filesize): """Set a new background image""" 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): image = self.get_image(filename, size, 'DCCSENDAV', self.client.avatar_progress, self.client.avatar_image, (nick,)) if image is None: image = self.client.newimage() # Need to shift 15 pixels to the left because OV uses the edge of # the balloon rather than the arrow as the offset point. I do this # here because it's OV specific. self.client.avatar(nick, image, (nx, ny), (bx-BALLOONXOFFSET, by)) def cmd_PING(self): self.write('PONG\r\n') def cmd_URL(self, nick, text): self.client.url(nick, text) def cmd_NEW(self, nick, x, y, filename, nx, ny, size, bx, by): image = self.get_image(filename, size, 'DCCSENDAV', self.client.avatar_progress, self.client.avatar_image, (nick,)) if image is None: image = self.client.newimage() self.client.new_avatar(nick, (x, y), image, (nx, ny), (bx-BALLOONXOFFSET, by)) def cmd_NOMORE(self, nick): self.client.del_avatar(nick) def cmd_EXIT_OBJ(self, name, x1, y1, x2, y2, duration, host, port): self.client.exit_obj(name, host, port) def cmd_DCCGET(self, port, filename, size): DCCGet(self.host, port, filename, size, self.progress_callback, self.image_callback) def cmd_DCCSENDAV(self, port, filename): 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): self.client.set_title(name) def cmd_MOUSEOVER(self, name, x, y, image1, size1, image2, size2, flag): image1 = self.get_image(image1, size1, 'DCCSENDOB', None, self.client.mouseover_image1, (name,)) image2 = self.get_image(image2, size2, 'DCCSENDOB', None, self.client.mouseover_image2, (name,)) if image1 is None: image1 = self.client.newimage() 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_WHOIS(self, nick, text): self.client.chat(nick, '*%s* is %s' % (nick, text)) def poll(): asyncore.poll()