pythonverse

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

OpenVerse.py (16510B)


      1 # Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org>
      2 #
      3 # This file is part of PythonVerse.
      4 #
      5 # PythonVerse is free software; you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as published by
      7 # the Free Software Foundation; either version 2 of the License, or
      8 # (at your option) any later version.
      9 # 
     10 # PythonVerse is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU General Public License for more details.
     14 # 
     15 # You should have received a copy of the GNU General Public License
     16 # along with PythonVerse; if not, write to the Free Software
     17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     18 #
     19 import sys, os, asyncore, asynchat, socket, string, struct, stat, codecs
     20 import transutil
     21 
     22 # Global constants are all caps; global variables start with _
     23 
     24 ENCODING = 'ISO-8859-1'
     25 BALLOONXOFFSET = 15
     26 HOME = os.path.expanduser('~/.OpenVerse')
     27 ANIMDIR = os.path.join(HOME, 'anims')
     28 DLDIR = os.path.join(HOME, 'download')
     29 ICONDIR = os.path.join(HOME, 'icons')
     30 IMAGEDIR = os.path.join(HOME, 'images')
     31 OBJDIR = os.path.join(HOME, 'objects')
     32 RIMAGEDIR = os.path.join(HOME, 'rimages')
     33 ROOMDIR = os.path.join(HOME, 'rooms')
     34 
     35 text_decode = codecs.lookup(ENCODING)[1]
     36 
     37 def decode(s):
     38     return text_decode(s)[0]
     39 
     40 def checkcache(filename, size):
     41     try: s = os.stat(filename)[stat.ST_SIZE]
     42     except OSError: return None
     43     else:
     44         if s == size or size < 0: return filename
     45 
     46 
     47 class DCC(asyncore.dispatcher):
     48     def __init__(self, host, port, filename, size, progress_callback,
     49                  close_callback, sock=()):
     50         asyncore.dispatcher.__init__(self, sock)
     51         self.host = host
     52         self.port = port
     53         self.filename = filename
     54         self.size = size
     55         self.length = 0
     56         self.outbuf = ''
     57         self.buffer = ''
     58         self.progress_callback = progress_callback
     59         self.close_callback = close_callback
     60      
     61     def __repr__(self):
     62         return '<DCC %s %s:%d %d/%d>' % (self.filename, self.host, self.port,
     63                                          self.length, self.size)
     64 
     65     def handle_connect(self):
     66         pass
     67     
     68     def handle_write(self):
     69         sent = self.send(self.outbuf)
     70         self.outbuf = self.outbuf[sent:]
     71 
     72     def writable(self):
     73         return len(self.outbuf) > 0
     74 
     75     def handle_close(self):
     76         print self, 'closing'
     77         self.close()
     78         if self.close_callback is not None:
     79             apply(self.close_callback, (self.tempfilename, self.filename,
     80                                         self.size))
     81                     
     82 
     83     
     84 class DCCGet(DCC):
     85     def __init__(self, host, port, filename, size,
     86 		 progress_callback, close_callback):
     87         DCC.__init__(self, host, port, filename, size, progress_callback,
     88                      close_callback)
     89         self.tempfilename = os.path.join(RIMAGEDIR, filename)
     90         self.file = open(self.tempfilename, 'wb')
     91         self.size = size
     92         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
     93         self.connect((host, port))
     94 
     95     def handle_read(self):
     96         data = self.recv(4096)
     97         if data:
     98             self.file.write(data)
     99             self.length = self.length + len(data)
    100             self.outbuf = self.outbuf + struct.pack('>I', self.length)
    101             if self.progress_callback is not None:
    102                 apply(self.progress_callback, (self.length, self.tempfilename,
    103                                                self.filename, self.size))
    104 
    105             if self.length == self.size:
    106                 self.file.close()
    107 
    108 
    109 class DCCSendPassive(DCC):
    110     def __init__(self, host, port, filename, size, progress_callback,
    111                  close_callback):
    112         DCC.__init__(self, host, port, filename, size, progress_callback,
    113                      close_callback)
    114         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    115         self.connect((host, port))
    116         
    117         self.file = open(filename, 'rb')
    118         self.outbuf = self.file.read(4096)
    119         self.sent = 0
    120 
    121     def handle_read(self):
    122         data = self.recv(4)
    123         print self, 'received %d bytes' % len(data)
    124         if data:
    125             data = self.buffer + data
    126             # Drop all but one length received
    127             drop = len(data) / 4 * 4 - 4
    128             data = data[drop:]
    129             if len(data) >= 4:
    130                 # Get the number of bytes received by the client
    131                 (self.length,) = struct.unpack('>I', data[:4])
    132                 self.buffer = data[4:]
    133                 print self
    134                 if self.progress_callback is not None:
    135                     apply(self.progress_callback, (self.length, self.filename,
    136                                                    self.size))
    137                     
    138                 if self.length == self.size:
    139                     self.file.close()
    140                     self.handle_close()
    141 
    142     def handle_write(self):
    143         #if self.length < self.sent: return
    144         sent = self.send(self.outbuf)
    145         self.outbuf = self.outbuf[sent:]
    146         self.sent = self.sent + sent
    147         if len(self.outbuf) < 4096:
    148             self.outbuf = self.outbuf + self.file.read(4096)
    149 
    150 
    151 class ServerConnection(transutil.Connection):
    152     def __init__(self, host, port, client, nick, avatar):
    153         transutil.Connection.__init__(self)
    154         self.host = host
    155         self.port = port
    156         self.pending_images = {}
    157         self.client = client
    158         self.images = {}
    159         self.nick = nick
    160 	if type(avatar) == type(''): avdata = self.parse_anim(avatar)
    161 	else: avdata = avatar
    162 	self.avatar_filename = avdata[0]
    163         self.nx = avdata[1]
    164         self.ny = avdata[2]
    165         self.bx = avdata[3]
    166         self.by = avdata[4]
    167         # Connect last
    168         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    169         self.connect((host, port))
    170                                 
    171     # Handlers for asynchat
    172     def handle_connect(self):
    173         size = os.stat(self.avatar_filename)[stat.ST_SIZE]
    174         self.write("AUTH %s %d %d %s %d %d %d %d %d\r\n" %
    175                   (self.nick.encode(ENCODING, 'replace'), 320, 200, os.path.basename(self.avatar_filename), self.nx, self.ny,
    176                    size, self.bx, self.by))
    177 
    178     def handle_close(self):
    179         transutil.Connection.handle_close(self)
    180         self.client.close()
    181 
    182     # Utility functions
    183     def set_client(self, client):
    184         """Change the client object that this object calls to"""
    185 	self.client = client
    186 
    187     def debug(self, info):
    188         self.client.debug(info)
    189 
    190     def get_image(self, filename, size, command, pcallback, callback, args=()):
    191 	# Prevent possible embedded '/' attacks
    192 	filename = os.path.basename(filename)
    193         if filename == 'default.gif': size = -1
    194         try: image = self.images[filename, size]
    195         except KeyError:
    196             # Check in locally cached images
    197             file = checkcache(os.path.join(RIMAGEDIR, filename), size)
    198             if file is None:
    199                 # Check my own avatars as well
    200                 file = checkcache(os.path.join(IMAGEDIR, filename), size)
    201 
    202             if file is not None:
    203                 image = self.client.newimage(file)
    204                 self.images[filename, size] = image
    205                 return image
    206                         
    207             blob = (pcallback, callback, args)
    208             # Take the callback out if it's already in there
    209             for pending in self.pending_images.values():
    210                 if blob in pending: pending.remove(blob)
    211                 
    212             try: self.pending_images[filename, size].append(blob)
    213             except KeyError: self.pending_images[filename, size] = [blob]
    214             else: pending.append(blob)
    215                 
    216             self.write('%s %s\r\n' % (command, filename))
    217             
    218         else: return image
    219 
    220     def progress_callback(self, length, tempfilename, filename, size):
    221         for pcallback, callback, args in self.pending_images[(filename, size)]:
    222             if pcallback: apply(pcallback, args + (length,tempfilename,size))
    223 
    224     def image_callback(self, tempfilename, filename, size):
    225         image = self.client.newimage(tempfilename)
    226         self.images[(filename, size)] = image
    227         for pcallback, callback, args in self.pending_images[(filename, size)]:
    228             apply(callback, args + (image,))
    229 
    230     # Client functions
    231 
    232     def getnick(self):
    233 	return self.nick
    234 
    235     def gethostport(self):
    236 	return (self.host, self.port)
    237 
    238     def new_connect(self, host, port):
    239 	self.close()
    240 	self.host = host
    241 	self.port = port
    242         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    243 	self.connect((host, port))
    244 
    245     def move(self, pos):
    246         x, y = pos
    247         self.write('MOVE %s %d %d 20\r\n' % (self.nick.encode(ENCODING, 'replace'), x, y))
    248 
    249     def push(self):
    250 	self.write('PUSH 100\r\n')
    251 
    252     def effect(self, action):
    253 	self.write('EFFECT %s\r\n' % action.lower())
    254 
    255     def privmsg(self, nicks, text):
    256 	if type(nicks) == type(''): nicks = [nicks]
    257 	for n in nicks:
    258 	    self.write('PRIVMSG %s %s\r\n' % (n.encode(ENCODING, 'replace'), text))
    259 	return self.nick
    260 
    261     def url(self, nicks, url):
    262 	if type(nicks) == type(''): nicks = [nicks]
    263 	for n in nicks:
    264 	    self.write('URL %s %s\r\n' % (n.encode(ENCODING, 'replace'), url))
    265 
    266     def chat(self, text):
    267         self.write('CHAT %s\r\n' % text.encode(ENCODING, 'replace'))
    268 
    269     def set_nick(self, nick):
    270         self.nick = nick
    271         self.write('NICK %s\r\n' % nick)
    272 
    273     def ignore(self, what, nick):
    274 	self.write('IGNORE %s %s\r\n' % (what.upper(), nick))
    275 
    276     def unignore(self, what, nick):
    277 	self.write('UNIGNORE %s %s\r\n' % (what.upper(), nick))
    278 
    279     def quote(self, text):
    280         """Send raw commands to the server"""
    281         self.write('%s\r\n' % text)
    282         
    283     def set_avatar(self, avatar):
    284         if type(avatar) == type(''): av = self.parse_anim(avatar)
    285 	else: av = avatar
    286 	try: size = os.stat(os.path.join(IMAGEDIR, av[0]))[stat.ST_SIZE]
    287 	except OSError, info: self.client.debug(info)
    288 	else:
    289             self.avatar_filename = av[0]
    290             self.nx = av[1]
    291             self.ny = av[2]
    292             self.bx = av[3]
    293             self.by = av[4]
    294 	    self.write('AVATAR %s %d %d %d %d %d\r\n' %
    295 		      (os.path.basename(av[0]), av[1], av[2], size, av[3], av[4]))
    296 
    297     def parse_anim(self, avatar):
    298 	"""Parse the avatar definition file for information"""
    299 	try: avfile = open(os.path.join(ANIMDIR, avatar), 'r')
    300 	except:
    301 	    try: avfile = open(os.path.join(ANIMDIR, avatar + '.av'), 'r')
    302 	    except:
    303 		print "Avatar", avatar, "not found."
    304 		if self.avatar_filename is not None:
    305 		    return [self.avatar_filename,
    306 			    self.nx, self.ny, self.bx, self.by]
    307 		return ["default.gif", 0, 36, 24, 6]
    308 	animdata = avfile.readlines()
    309     	avfile.close()
    310 	for animitem in animdata:
    311 	    # parse the whole file for future addition of animation
    312 	    splitanimitem = animitem.split()
    313 	    if splitanimitem[1] == "MV(anim.x_off)":
    314 	        nx = int(splitanimitem[2])
    315 	    if splitanimitem[1] == "MV(anim.y_off)":
    316 	        ny = int(splitanimitem[2])
    317 	    if splitanimitem[1] == "MV(anim.baloon_x)":
    318 	        bx = int(splitanimitem[2])
    319 	    if splitanimitem[1] == "MV(anim.baloon_y)":
    320 	        by = int(splitanimitem[2])
    321 	    if splitanimitem[1] == "MV(anim.0)":
    322 	        avimage = splitanimitem[2].strip()
    323 	return os.path.join(IMAGEDIR, avimage), nx, ny, bx, by
    324 
    325     def whois(self, nick):
    326         self.write('WHOIS %s\r\n' % nick)
    327 
    328     def whichmouseover(self, name):
    329 	self.client.setmouseover(name)
    330 
    331     # Command handlers
    332         
    333     def cmd_ABOVE(self, line):
    334 	"""Raise the named object to the top of the stacking order"""
    335 	self.client.raise_object(line.split()[1])
    336 
    337     def cmd_CHAT(self, line):
    338         cmd, nick, text = line.split(' ', 2)
    339         self.client.chat(decode(nick), decode(text))
    340 
    341     def cmd_SCHAT(self, line):
    342         cmd, emote, nick, text = line.split(' ', 3)
    343         self.client.chat(nick, '*%s* %s' % (emote, decode(text)))
    344 
    345     def cmd_MOVE(self, line):
    346 	cmd, nick, x, y, speed = line.split()
    347         self.client.move_avatar(nick, int(x), int(y), int(speed))
    348 
    349     def cmd_EFFECT(self, line):
    350     	cmd, nick, action = line.split()
    351 	self.client.effect(decode(nick), action)
    352 
    353     def cmd_PRIVMSG(self, line):
    354         cmd, nick, text = line.split(' ', 2)
    355         self.client.privmsg(nick, text)
    356 
    357     def cmd_ROOM(self, line):
    358         """Set a new background image"""
    359 	cmd, filename, filesize = line.split()
    360 	filesize = int(filesize)
    361         image = self.get_image(filename, filesize, 'DCCSENDROOM',
    362                                self.client.background_progress,
    363                                self.client.background_image)
    364         if image is not None: self.client.background_image(image)
    365 
    366     def cmd_AVATAR(self, line):
    367         cmd, nick, filename, nx, ny, size, bx, by = line.split()
    368 	nick = decode(nick)
    369 	nx = int(nx)
    370 	ny = int(ny)
    371 	size = int(size)
    372 	bx = int(bx)
    373 	by = int(by)
    374         image = self.get_image(filename, size, 'DCCSENDAV',
    375                                self.client.avatar_progress,
    376                                self.client.avatar_image, (nick,))
    377         if image is None: image = self.client.newimage()
    378         # Need to shift 15 pixels to the left because OV uses the edge of
    379         # the balloon rather than the arrow as the offset point. I do this
    380         # here because it's OV specific.
    381         self.client.avatar(nick, image, (nx, ny), (bx-BALLOONXOFFSET, by))
    382 
    383     def cmd_PING(self, line):
    384         self.write('PONG\r\n')
    385         
    386     def cmd_URL(self, line):
    387     	cmd, nick, text = line.split(' ', 2)
    388         self.client.url(decode(nick), text)
    389 
    390     def cmd_NEW(self, line):
    391         cmd, nick, x, y, filename, nx, ny, size, bx, by = line.split()
    392 	nick = decode(nick)
    393 	x = int(x)
    394 	y = int(y)
    395 	nx = int(nx)
    396 	ny = int(ny)
    397 	size = int(size)
    398 	bx = int(bx)
    399 	by = int(by)
    400         image = self.get_image(filename, size, 'DCCSENDAV',
    401                                self.client.avatar_progress,
    402                                self.client.avatar_image, (nick,))
    403         if image is None: image = self.client.newimage()
    404         self.client.new_avatar(nick, (x, y), image, (nx, ny),
    405                                (bx-BALLOONXOFFSET, by))
    406 
    407     def cmd_NOMORE(self, line):
    408         cmd, nick = line.split()
    409         self.client.del_avatar(decode(nick))
    410 
    411     def cmd_EXIT_OBJ(self, line):
    412         cmd, name, x1, y1, x2, y2, duration, host, port = line.split()
    413 	self.client.exit_obj(decode(name), host, int(port))
    414 
    415     def cmd_DCCGETAV(self, line):
    416         cmd, port, filename, size = line.split()
    417 	port = int(port)
    418 	size = int(size)
    419         DCCGet(self.host, port, filename, size,
    420 	       self.progress_callback, self.image_callback)
    421 
    422     def cmd_DCCGETROOM(self, line): return self.cmd_DCCGETAV(line)
    423 
    424     def cmd_DCCGETOB(self, line): return self.cmd_DCCGETAV(line)
    425 
    426     def cmd_DCCSENDAV(self, line):
    427         cmd, port, filename = line.split()
    428 	port = int(port)
    429         filename = os.path.join(IMAGEDIR, os.path.basename(filename))
    430         try:
    431             size = os.stat(filename)[stat.ST_SIZE]
    432             DCCSendPassive(self.host, port, filename, size, None, None)
    433         except IOError, info: self.debug(info)
    434         
    435     def cmd_ROOMNAME(self, line):
    436         cmd, name = line.split(' ', 1)
    437         self.client.set_title(decode(name))
    438 
    439     def cmd_MOUSEOVER(self, line):
    440         cmd, name, x, y, image1, size1, image2, size2, flag = line.split()
    441 	name = decode(name)
    442 	x = int(x)
    443 	y = int(y)
    444 	size1 = int(size1)
    445 	size2 = int(size2)
    446 	flag = int(flag)
    447         image1 = self.get_image(image1, size1, 'DCCSENDOB', None,
    448                                 self.client.mouseover_image1, (name,))
    449         image2 = self.get_image(image2, size2, 'DCCSENDOB', None,
    450                                 self.client.mouseover_image2, (name,))
    451         if image1 is None: image1 = self.client.newimage()
    452         if image2 is None: image2 = self.client.newimage()
    453         self.client.mouseover(name, (x, y), image1, image2)
    454 
    455     def cmd_SUB(self, line):
    456 	self.client.debug('TODO: Implement SUB')
    457 
    458     def cmd_WHOIS(self, line):
    459         cmd, nick, text = line.split(' ', 2)
    460 	nick = decode(nick)
    461 	text = decode(text)
    462         self.client.chat(nick, '*%s* is %s' % (nick, text))
    463 
    464     def cmd_PUSH(self, line):
    465         cmd, x, y, speed = line.split()
    466         self.client.push(int(x), int(y), int(speed))
    467 
    468     def unknown(self, line):
    469         self.client.debug('Unknown: %s' % line)
    470         
    471 
    472 def poll():
    473     asyncore.poll()