toc.py   [plain text]



# Twisted, the Framework of Your Internet
# Copyright (C) 2001 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""
Implements a AOL Instant Messenger TOC server and client, using the Twisted framework.

TODO:
info,dir: see how gaim connects for this...it may never work if it tries to connect to the aim server automatically

This module is stable, but deprecated.

Maintainer: U{Paul Swartz<mailto:z3p@twistedmatrix.com>}
"""

# twisted imports
from twisted.internet import reactor, protocol
from twisted.python import log

# base imports
import struct
import string
import time
import base64
import os
import StringIO

SIGNON,DATA,ERROR,SIGNOFF,KEEP_ALIVE=range(1,6)
PERMITALL,DENYALL,PERMITSOME,DENYSOME=range(1,5)

DUMMY_CHECKSUM = -559038737 # 0xdeadbeef

def quote(s):
    rep=['\\','$','{','}','[',']','(',')','"']
    for r in rep:
        s=string.replace(s,r,"\\"+r)
    return "\""+s+"\""

def unquote(s):
    if s=="": return ""
    if s[0]!='"': return s
    r=string.replace
    s=s[1:-1]
    s=r(s,"\\\\","\\")
    s=r(s,"\\$","$")
    s=r(s,"\\{","{")
    s=r(s,"\\}","}")
    s=r(s,"\\[","[")
    s=r(s,"\\]","]")
    s=r(s,"\\(","(")
    s=r(s,"\\)",")")
    s=r(s,"\\\"","\"")
    return s

def unquotebeg(s):
    for i in range(1,len(s)):
        if s[i]=='"' and s[i-1]!='\\':
            q=unquote(s[:i+1])
            return [q,s[i+2:]]

def unroast(pw):
    roaststring="Tic/Toc"
    pw=string.lower(pw[2:])
    r=""
    count=0
    hex=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"]
    while pw:
        st,pw=pw[:2],pw[2:]
        value=(16*hex.index(st[0]))+hex.index(st[1])
        xor=ord(roaststring[count])
        count=(count+1)%len(roaststring)
        r=r+chr(value^xor)
    return r

def roast(pw):
    # contributed by jemfinch on #python
    key="Tic/Toc"
    ro="0x"
    i=0
    ascii=map(ord,pw)
    for c in ascii:
        ro=ro+'%02x'%(c^ord(key[i%len(key)]))
        i=i+1
    return string.lower(ro)

def checksum(b):
    return DUMMY_CHECKSUM # do it like gaim does, since the checksum
                      # formula doesn't work
##    # used in file transfers
##    check0 = check1 = 0x00ff
##    for i in range(len(b)):
##        if i%2:
##            if ord(b[i])>check1:
##                check1=check1+0x100 # wrap
##                if check0==0:
##                    check0=0x00ff
##                    if check1==0x100:
##                        check1=check1-1
##                else:
##                    check0=check0-1
##            check1=check1-ord(b[i])
##        else:
##            if ord(b[i])>check0: # wrap
##                check0=check0+0x100
##                if check1==0:
##                    check1=0x00ff
##                    if check0==0x100:
##                        check0=check0-1
##                else:
##                    check1=check1-1
##            check0=check0-ord(b[i])
##    check0=check0 & 0xff
##    check1=check1 & 0xff
##    checksum=(long(check0)*0x1000000)+(long(check1)*0x10000)
##    return checksum

def checksum_file(f):
    return DUMMY_CHECKSUM # do it like gaim does, since the checksum
                      # formula doesn't work
##    check0=check1=0x00ff
##    i=0
##    while 1:
##        b=f.read()
##        if not b: break
##        for char in b:
##            i=not i
##            if i:
##                if ord(char)>check1:
##                    check1=check1+0x100 # wrap
##                    if check0==0:
##                        check0=0x00ff
##                        if check1==0x100:
##                            check1=check1-1
##                    else:
##                        check0=check0-1
##                check1=check1-ord(char)
##            else:
##                if ord(char)>check0: # wrap
##                    check0=check0+0x100
##                    if check1==0:
##                        check1=0x00ff
##                        if check0==0x100:
##                            check0=check0-1
##                    else:
##                        check1=check1-1
##                check0=check0-ord(char)
##    check0=check0 & 0xff
##    check1=check1 & 0xff
##    checksum=(long(check0)*0x1000000)+(long(check1)*0x10000)
##    return checksum

def normalize(s):
    s=string.lower(s)
    s=string.replace(s," ","")
    return s


class TOCParseError(ValueError):
    pass


class TOC(protocol.Protocol):
    users={}

    def connectionMade(self):
        # initialization of protocol
        self._buf=""
        self._ourseqnum=0L
        self._theirseqnum=0L
        self._mode="Flapon"
        self._onlyflaps=0
        self._laststatus={} # the last status for a user
        self.username=None
        self.permitmode=PERMITALL
        self.permitlist=[]
        self.denylist=[]
        self.buddylist=[]
        self.signontime=0
        self.idletime=0
        self.userinfo="<br>"
        self.userclass=" O"
        self.away=""
        self.saved=None

    def _debug(self,data):
        log.msg(data)

    def connectionLost(self, reason):
        self._debug("dropped connection from %s" % self.username)
        try:
            del self.factory.users[self.username]
        except:
            pass
        for k in self.factory.chatroom.keys():
            try:
                self.factory.chatroom[k].leave(self)
            except TOCParseError:
                pass
        if self.saved:
            self.factory.savedusers[self.username]=self.saved
        self.updateUsers()

    def sendFlap(self,type,data):
        """
        send a FLAP to the client
        """
        send="*"
        self._debug(data)
        if type==DATA:
            data=data+"\000"
        length=len(data)
        send=send+struct.pack("!BHH",type,self._ourseqnum,length)
        send=send+data
        self._ourseqnum=self._ourseqnum+1
        if self._ourseqnum>(256L**4):
            self._ourseqnum=0
        self.transport.write(send)

    def dataReceived(self,data):
        self._buf=self._buf+data
        try:
            func=getattr(self,"mode%s"%self._mode)
        except:
            return
        self._mode=func()
        if self._onlyflaps and self.isFlap(): self.dataReceived("")

    def isFlap(self):
        """
        tests to see if a flap is actually on the buffer
        """
        if self._buf=='': return 0
        if self._buf[0]!="*": return 0
        if len(self._buf)<6: return 0
        foo,type,seqnum,length=struct.unpack("!BBHH",self._buf[:6])
        if type not in range(1,6): return 0
        if len(self._buf)<6+length: return 0
        return 1

    def readFlap(self):
        """
        read the first FLAP off self._buf, raising errors if it isn't in the right form.
        the FLAP is the basic TOC message format, and is logically equivilant to a packet in TCP
        """
        if self._buf=='': return None
        if self._buf[0]!="*":
            raise TOCParseError
        if len(self._buf)<6: return None
        foo,type,seqnum,length=struct.unpack("!BBHH",self._buf[:6])
        if len(self._buf)<6+length: return None
        data=self._buf[6:6+length]
        self._buf=self._buf[6+length:]
        if data and data[-1]=="\000":
            data=data[:-1]
        self._debug([type,data])
        return [type,data]

    #def modeWeb(self):
    #    try:
    #        line,rest=string.split(self._buf,"\n",1)
    #        get,username,http=string.split(line," ",2)
    #    except:
    #        return "Web" # not enough data
    #    foo,type,username=string.split(username,"/")
    #    if type=="info":
    #        user=self.factory.users[username]
    #        text="<HTML><HEAD><TITLE>User Information for %s</TITLE></HEAD><BODY>Username: <B>%s</B><br>\nWarning Level: <B>%s%</B><br>\n Online Since: <B>%s</B><br>\nIdle Minutes: <B>%s</B><br>\n<hr><br>\n%s\n<hr><br>\n"%(user.saved.nick, user.saved.nick, user.saved.evilness, time.asctime(user.signontime), int((time.time()-user.idletime)/60), user.userinfo)
    #        self.transport.write("HTTP/1.1 200 OK\n")
    #        self.transport.write("Content-Type: text/html\n")
    #        self.transport.write("Content-Length: %s\n\n"%len(text))
    #        self.transport.write(text)
    #        self.loseConnection()

    def modeFlapon(self):
        #if self._buf[:3]=="GET": self.modeWeb() # TODO: get this working
        if len(self._buf)<10: return "Flapon" # not enough bytes
        flapon,self._buf=self._buf[:10],self._buf[10:]
        if flapon!="FLAPON\r\n\r\n":
            raise TOCParseError
        self.sendFlap(SIGNON,"\000\000\000\001")
        self._onlyflaps=1
        return "Signon"

    def modeSignon(self):
        flap=self.readFlap()
        if flap==None:
            return "Signon"
        if flap[0]!=SIGNON: raise TOCParseError
        version,tlv,unlength=struct.unpack("!LHH",flap[1][:8])
        if version!=1 or tlv!=1 or unlength+8!=len(flap[1]):
            raise TOCParseError
        self.username=normalize(flap[1][8:])
        if self.username in self.factory.savedusers.keys():
            self.saved=self.factory.savedusers[self.username]
        else:
            self.saved=SavedUser()
            self.saved.nick=self.username
        return "TocSignon"

    def modeTocSignon(self):
        flap=self.readFlap()
        if flap==None:
            return "TocSignon"
        if flap[0]!=DATA: raise TOCParseError
        data=string.split(flap[1]," ")
        if data[0]!="toc_signon": raise TOCParseError
        for i in data:
            if not i:data.remove(i)
        password=unroast(data[4])
        if not(self.authorize(data[1],int(data[2]),data[3],password)):
            self.sendError(BAD_NICKNAME)
            self.transport.loseConnection()
            return
        self.sendFlap(DATA,"SIGN_ON:TOC1.0")
        self.sendFlap(DATA,"NICK:%s"%self.saved.nick)
        self.sendFlap(DATA,"CONFIG:%s"%self.saved.config)
        # sending user configuration goes here
        return "Connected"

    def authorize(self,server,port,username,password):
        if self.saved.password=="":
            self.saved.password=password
            return 1
        else:
            return self.saved.password==password

    def modeConnected(self):
        flap=self.readFlap()
        while flap!=None:
            if flap[0] not in [DATA,KEEP_ALIVE]: raise TOCParseError
            flapdata=string.split(flap[1]," ",1)
            tocname=flapdata[0][4:]
            if len(flapdata)==2:
                data=flapdata[1]
            else:
                data=""
            func=getattr(self,"toc_"+tocname,None)
            if func!=None:
                func(data)
            else:
                self.toc_unknown(tocname,data)
            flap=self.readFlap()
        return "Connected"

    def toc_unknown(self,tocname,data):
        self._debug("unknown! %s %s" % (tocname,data))

    def toc_init_done(self,data):
        """
        called when all the setup is done.

        toc_init_done
        """
        self.signontime=int(time.time())
        self.factory.users[self.username]=self
        self.updateUsers()

    def toc_add_permit(self,data):
        """
        adds users to the permit list.  if the list is null, then set the mode to DENYALL
        """
        if data=="":
            self.permitmode=DENYALL
            self.permitlist=[]
            self.denylist=[]
        else:
            self.permitmode=PERMITSOME
            self.denylist=[]
            users=string.split(data," ")
            map(self.permitlist.append,users)
        self.updateUsers()

    def toc_add_deny(self,data):
        """
        adds users to the deny list.  if the list is null, then set the mode to PERMITALL
        """
        if data=="":
            self.permitmode=PERMITALL
            self.permitlist=[]
            self.denylist=[]
        else:
            self.permitmode=DENYSOME
            self.permitlist=[]
            users=string.split(data," ")
            map(self.denylist.append,users)
        self.updateUsers()

    def toc_evil(self,data):
        """
        warns a user.

        toc_evil <username> <anon|norm>
        """
        username,nora=string.split(data," ")
        if nora=="anon":
            user=""
        else:
            user=self.saved.nick
        if not(self.factory.users.has_key(username)):
            self.sendError(CANT_WARN,username)
            return
        if self.factory.users[username].saved.evilness>=100:
            self.sendError(CANT_WARN,username)
            return
        self.factory.users[username].evilFrom(user)

    def toc_add_buddy(self,data):
        """
        adds users to the buddy list

        toc_add_buddy <buddyname1> [<buddyname2>] [<buddyname3>]...
        """
        buddies=map(normalize,string.split(data," "))
        for b in buddies:
            if b not in self.buddylist:
                self.buddylist.append(b)
        for buddy in buddies:
            try:
                buddy=self.factory.users[buddy]
            except:
                pass
            else:
                self.buddyUpdate(buddy)

    def toc_remove_buddy(self,data):
        """
        removes users from the buddy list

        toc_remove_buddy <buddyname1> [<buddyname2>] [<buddyname3>]...
        """
        buddies=string.split(data," ")
        for buddy in buddies:
            try:
                self.buddylist.remove(normalize(buddy))
            except: pass

    def toc_send_im(self,data):
        """
        incoming instant message

        toc_send_im <screenname> <quoted message> [auto]
        """
        username,data=string.split(data," ",1)
        auto=0
        if data[-4:]=="auto":
            auto=1
            data=data[:-5]
        data=unquote(data)
        if not(self.factory.users.has_key(username)):
            self.sendError(NOT_AVAILABLE,username)
            return
        user=self.factory.users[username]
        if not(self.canContact(user)):
            self.sendError(NOT_AVAILABLE,username)
            return
        user.hearWhisper(self,data,auto)

    def toc_set_info(self,data):
        """
        set the users information, retrivable with toc_get_info

        toc_set_info <user info (quoted)>
        """
        info=unquote(data)
        self._userinfo=info

    def toc_set_idle(self,data):
        """
        set/unset idle

        toc_set_idle <seconds>
        """
        seconds=int(data)
        self.idletime=time.time()-seconds # time when they started being idle
        self.updateUsers()

    def toc_set_away(self,data):
        """
        set/unset away message

        toc_set_away [<away message>]
        """
        away=unquote(data)
        if not self.away and away: # setting an away message
            self.away=away
            self.userclass=self.userclass+'U'
            self.updateUsers()
        elif self.away and not away: # coming back
            self.away=""
            self.userclass=self.userclass[:2]
            self.updateUsers()
        else:
            raise TOCParseError

    def toc_chat_join(self,data):
        """
        joins the chat room.

        toc_chat_join <exchange> <room name>
        """
        exchange,name=string.split(data," ",1)
        self.factory.getChatroom(int(exchange),unquote(name)).join(self)

    def toc_chat_invite(self,data):
        """
        invite others to the room.

        toc_chat_invite <room id> <invite message> <buddy 1> [<buddy2>]...
        """
        id,data=string.split(data," ",1)
        id=int(id)
        message,data=unquotebeg(data)
        buddies=string.split(data," ")
        for b in buddies:
            room=self.factory.chatroom[id]
            bud=self.factory.users[b]
            bud.chatInvite(room,self,message)

    def toc_chat_accept(self,data):
        """
        accept an invitation.

        toc_chat_accept <room id>
        """
        id=int(data)
        self.factory.chatroom[id].join(self)

    def toc_chat_send(self,data):
        """
        send a message to the chat room.

        toc_chat_send <room id> <message>
        """
        id,message=string.split(data," ",1)
        id=int(id)
        message=unquote(message)
        self.factory.chatroom[id].say(self,message)

    def toc_chat_whisper(self,data):
        id,user,message=string.split(data," ",2)
        id=int(id)
        room=self.factory.chatroom[id]
        message=unquote(message)
        self.factory.users[user].chatWhisper(room,self,message)

    def toc_chat_leave(self,data):
        """
        leave the room.

        toc_chat_leave <room id>
        """
        id=int(data)
        self.factory.chatroom[id].leave(self)

    def toc_set_config(self,data):
        """
        set the saved config.  this gets send when you log in.

        toc_set_config <config>
        """
        self.saved.config=unquote(data)

    def toc_get_info(self,data):
        """
        get the user info for a user

        toc_get_info <username>
        """
        if not self.factory.users.has_key(data):
            self.sendError(901,data)
            return
        self.sendFlap(2,"GOTO_URL:TIC:info/%s"%data)

    def toc_format_nickname(self,data):
        """
        change the format of your nickname.

        toc_format_nickname <new format>
        """
        # XXX may not work
        nick=unquote(data)
        if normalize(nick)==self.username:
            self.saved.nick=nick
            self.sendFlap(2,"ADMIN_NICK_STATUS:0")
        else:
            self.sendError(BAD_INPUT)

    def toc_change_passwd(self,data):
        orig,data=unquotebeg(data)
        new=unquote(data)
        if orig==self.saved.password:
            self.saved.password=new
            self.sendFlap(2,"ADMIN_PASSWD_STATUS:0")
        else:
            self.sendError(BAD_INPUT)

    def sendError(self,code,*varargs):
        """
        send an error to the user.  listing of error messages is below.
        """
        send="ERROR:%s"%code
        for v in varargs:
            send=send+":"+v
        self.sendFlap(DATA,send)

    def updateUsers(self):
        """
        Update the users who have us on their buddylist.
        Called when the user changes anything (idle,away) so people can get updates.
        """
        for user in self.factory.users.values():
            if self.username in user.buddylist and self.canContact(user):
                user.buddyUpdate(self)

    def getStatus(self,user):
        if self.canContact(user):
            if self in self.factory.users.values():ol='T'
            else: ol='F'
            idle=0
            if self.idletime:
                idle=int((time.time()-self.idletime)/60)
            return (self.saved.nick,ol,self.saved.evilness,self.signontime,idle,self.userclass)
        else:
            return (self.saved.nick,'F',0,0,0,self.userclass)

    def canContact(self,user):
        if self.permitmode==PERMITALL: return 1
        elif self.permitmode==DENYALL: return 0
        elif self.permitmode==PERMITSOME:
            if user.username in self.permitlist: return 1
            else: return 0
        elif self.permitmode==DENYSOME:
            if user.username in self.denylist: return 0
            else: return 1
        else:
            assert 0,"bad permitmode %s" % self.permitmode

    def buddyUpdate(self,user):
        """
        Update the buddy.  Called from updateUsers()
        """
        if not self.canContact(user): return
        status=user.getStatus(self)
        if not self._laststatus.has_key(user):
            self._laststatus[user]=()
        if self._laststatus[user]!=status:
            send="UPDATE_BUDDY:%s:%s:%s:%s:%s:%s"%status
            self.sendFlap(DATA,send)
            self._laststatus[user]=status

    def hearWhisper(self,user,data,auto=0):
        """
        Called when you get an IM.  If auto=1, it's an autoreply from an away message.
        """
        if not self.canContact(user): return
        if auto: auto='T'
        else: auto='F'
        send="IM_IN:%s:%s:%s"%(user.saved.nick,auto,data)
        self.sendFlap(DATA,send)

    def evilFrom(self,user):
        if user=="":
            percent=0.03
        else:
            percent=0.1
        self.saved.evilness=self.saved.evilness+int((100-self.saved.evilness)*percent)
        self.sendFlap(2,"EVILED:%s:%s"%(self.saved.evilness,user))
        self.updateUsers()

    def chatJoin(self,room):
        self.sendFlap(2,"CHAT_JOIN:%s:%s"%(room.id,room.name))
        f="CHAT_UPDATE_BUDDY:%s:T"%room.id
        for u in room.users:
            if u!=self:
                u.chatUserUpdate(room,self)
            f=f+":"+u.saved.nick
        self.sendFlap(2,f)

    def chatInvite(self,room,user,message):
        if not self.canContact(user): return
        self.sendFlap(2,"CHAT_INVITE:%s:%s:%s:%s"%(room.name,room.id,user.saved.nick,message))

    def chatUserUpdate(self,room,user):
        if user in room.users:
            inroom='T'
        else:
            inroom='F'
        self.sendFlap(2,"CHAT_UPDATE_BUDDY:%s:%s:%s"%(room.id,inroom,user.saved.nick))

    def chatMessage(self,room,user,message):
        if not self.canContact(user): return
        self.sendFlap(2,"CHAT_IN:%s:%s:F:%s"%(room.id,user.saved.nick,message))

    def chatWhisper(self,room,user,message):
        if not self.canContact(user): return
        self.sendFlap(2,"CHAT_IN:%s:%s:T:%s"%(room.id,user.saved.nick,message))

    def chatLeave(self,room):
        self.sendFlap(2,"CHAT_LEFT:%s"%(room.id))


class Chatroom:
    def __init__(self,fac,exchange,name,id):
        self.exchange=exchange
        self.name=name
        self.id=id
        self.factory=fac
        self.users=[]

    def join(self,user):
        if user in self.users:
            return
        self.users.append(user)
        user.chatJoin(self)

    def leave(self,user):
        if user not in self.users:
            raise TOCParseError
        self.users.remove(user)
        user.chatLeave(self)
        for u in self.users:
            u.chatUserUpdate(self,user)
        if len(self.users)==0:
            self.factory.remChatroom(self)

    def say(self,user,message):
        for u in self.users:
            u.chatMessage(self,user,message)


class SavedUser:
    def __init__(self):
        self.config=""
        self.nick=""
        self.password=""
        self.evilness=0


class TOCFactory(protocol.Factory):
    def __init__(self):
        self.users={}
        self.savedusers={}
        self.chatroom={}
        self.chatroomid=0

    def buildProtocol(self,addr):
        p=TOC()
        p.factory=self
        return p

    def getChatroom(self,exchange,name):
        for i in self.chatroom.values():
            if normalize(i.name)==normalize(name):
                return i
        self.chatroom[self.chatroomid]=Chatroom(self,exchange,name,self.chatroomid)
        self.chatroomid=self.chatroomid+1
        return self.chatroom[self.chatroomid-1]

    def remChatroom(self,room):
        id=room.id
        del self.chatroom[id]

MAXARGS={}
MAXARGS["CONFIG"]=0
MAXARGS["NICK"]=0
MAXARGS["IM_IN"]=2
MAXARGS["UPDATE_BUDDY"]=5
MAXARGS["ERROR"]=-1
MAXARGS["EVILED"]=1
MAXARGS["CHAT_JOIN"]=1
MAXARGS["CHAT_IN"]=3
MAXARGS["CHAT_UPDATE_BUDDY"]=-1
MAXARGS["CHAT_INVITE"]=3
MAXARGS["CHAT_LEFT"]=0
MAXARGS["ADMIN_NICK_STATUS"]=0
MAXARGS["ADMIN_PASSWD_STATUS"]=0


class TOCClient(protocol.Protocol):
    def __init__(self,username,password,authhost="login.oscar.aol.com",authport=5190):

        self.username=normalize(username) # our username
        self._password=password # our password
        self._mode="SendNick" # current mode
        self._ourseqnum=19071 # current sequence number (for sendFlap)
        self._authhost=authhost # authorization host
        self._authport=authport # authorization port
        self._online=0 # are we online?
        self._buddies=[] # the current buddy list
        self._privacymode=PERMITALL # current privacy mode
        self._permitlist=[] # list of users on the permit list
        self._roomnames={} # the names for each of the rooms we're in
        self._receivedchatmembers={} # have we gotten who's in our room yet?
        self._denylist=[]
        self._cookies={} # for file transfers
        self._buf='' # current data buffer
        self._awaymessage=''

    def _debug(self,data):
        log.msg(data)

    def sendFlap(self,type,data):
        if type==DATA:
            data=data+"\000"
        length=len(data)
        s="*"
        s=s+struct.pack("!BHH",type,self._ourseqnum,length)
        s=s+data
        self._ourseqnum=self._ourseqnum+1
        if self._ourseqnum>(256*256+256):
            self._ourseqnum=0
        self._debug(data)
        self.transport.write(s)

    def isFlap(self):
        """
        tests to see if a flap is actually on the buffer
        """
        if self._buf=='': return 0
        if self._buf[0]!="*": return 0
        if len(self._buf)<6: return 0
        foo,type,seqnum,length=struct.unpack("!BBHH",self._buf[:6])
        if type not in range(1,6): return 0
        if len(self._buf)<6+length: return 0
        return 1

    def readFlap(self):
        if self._buf=='': return None
        if self._buf[0]!="*":
            raise TOCParseError
        if len(self._buf)<6: return None
        foo,type,seqnum,length=struct.unpack("!BBHH",self._buf[:6])
        if len(self._buf)<6+length: return None
        data=self._buf[6:6+length]
        self._buf=self._buf[6+length:]
        if data and data[-1]=="\000":
            data=data[:-1]
        return [type,data]

    def connectionMade(self):
        self._debug("connection made! %s" % self.transport)
        self.transport.write("FLAPON\r\n\r\n")

    def connectionLost(self, reason):
        self._debug("connection lost!")
        self._online=0

    def dataReceived(self,data):
        self._buf=self._buf+data
        while self.isFlap():
            flap=self.readFlap()
            func=getattr(self,"mode%s"%self._mode)
            func(flap)

    def modeSendNick(self,flap):
        if flap!=[1,"\000\000\000\001"]: raise TOCParseError
        s="\000\000\000\001\000\001"+struct.pack("!H",len(self.username))+self.username
        self.sendFlap(1,s)
        s="toc_signon %s %s  %s %s english \"penguin\""%(self._authhost,\
            self._authport,self.username,roast(self._password))
        self.sendFlap(2,s)
        self._mode="Data"

    def modeData(self,flap):
        if not flap[1]:
            return
        if not ':' in flap[1]:
            self._debug("bad SNAC:%s"%(flap[1]))
            return
        command,rest=string.split(flap[1],":",1)
        if MAXARGS.has_key(command):
            maxsplit=MAXARGS[command]
        else:
            maxsplit=-1
        if maxsplit==-1:
            l=tuple(string.split(rest,":"))
        elif maxsplit==0:
            l=(rest,)
        else:
            l=tuple(string.split(rest,":",maxsplit))
        self._debug("%s %s"%(command,l))
        try:
            func=getattr(self,"toc%s"%command)
            self._debug("calling %s"%func)
        except:
            self._debug("calling %s"%self.tocUNKNOWN)
            self.tocUNKNOWN(command,l)
            return
        func(l)

    def tocUNKNOWN(self,command,data):
        pass

    def tocSIGN_ON(self,data):
        if data!=("TOC1.0",): raise TOCParseError
        self._debug("Whee, signed on!")
        if self._buddies: self.add_buddy(self._buddies)
        self._online=1
        self.onLine()

    def tocNICK(self,data):
        """
        NICK:<format of nickname>
        """
        self.username=data[0]

    def tocCONFIG(self,data):
        """
        CONFIG:<config>
        format of config data:
            - g: group.  all users until next g or end of config are in this group
            - b: buddy
            - p: person on the permit list
            - d: person on the deny list
            - m: permit/deny mode (1: permit all, 2: deny all, 3: permit some, 4: deny some)
        """
        data=data[0]
        if data and data[0]=="{":data=data[1:-1]
        lines=string.split(data,"\n")
        buddylist={}
        currentgroup=""
        permit=[]
        deny=[]
        mode=1
        for l in lines:
            if l:
                code,data=l[0],l[2:]
                if code=='g': # group
                    currentgroup=data
                    buddylist[currentgroup]=[]
                elif code=='b':
                    buddylist[currentgroup].append(data)
                elif code=='p':
                    permit.append(data)
                elif code=='d':
                    deny.append(data)
                elif code=='m':
                    mode=int(data)
        self.gotConfig(mode,buddylist,permit,deny)

    def tocIM_IN(self,data):
        """
        IM_IN:<user>:<autoreply T|F>:message
        """
        user=data[0]
        autoreply=(data[1]=='T')
        message=data[2]
        self.hearMessage(user,message,autoreply)

    def tocUPDATE_BUDDY(self,data):
        """
        UPDATE_BUDDY:<username>:<online T|F>:<warning level>:<signon time>:<idle time (minutes)>:<user class>
        """
        data=list(data)
        online=(data[1]=='T')
        if len(data[5])==2:
            data[5]=data[5]+" "
        away=(data[5][-1]=='U')
        if data[5][-1]=='U':
            data[5]=data[5][:-1]
        self.updateBuddy(data[0],online,int(data[2]),int(data[3]),int(data[4]),data[5],away)

    def tocERROR(self,data):
        """
        ERROR:<error code>:<misc. data>
        """
        code,args=data[0],data[1:]
        self.hearError(int(code),args)

    def tocEVILED(self,data):
        """
        EVILED:<current warning level>:<user who warned us>
        """
        self.hearWarning(data[0],data[1])

    def tocCHAT_JOIN(self,data):
        """
        CHAT_JOIN:<room id>:<room name>
        """
        #self.chatJoined(int(data[0]),data[1])
        self._roomnames[int(data[0])]=data[1]
        self._receivedchatmembers[int(data[0])]=0

    def tocCHAT_UPDATE_BUDDY(self,data):
        """
        CHAT_UPDATE_BUDDY:<room id>:<in room? T/F>:<user 1>:<user 2>...
        """
        roomid=int(data[0])
        inroom=(data[1]=='T')
        if self._receivedchatmembers[roomid]:
            for u in data[2:]:
                self.chatUpdate(roomid,u,inroom)
        else:
            self._receivedchatmembers[roomid]=1
            self.chatJoined(roomid,self._roomnames[roomid],list(data[2:]))

    def tocCHAT_IN(self,data):
        """
        CHAT_IN:<room id>:<username>:<whisper T/F>:<message>
        whisper isn't used
        """
        whisper=(data[2]=='T')
        if whisper:
            self.chatHearWhisper(int(data[0]),data[1],data[3])
        else:
            self.chatHearMessage(int(data[0]),data[1],data[3])

    def tocCHAT_INVITE(self,data):
        """
        CHAT_INVITE:<room name>:<room id>:<username>:<message>
        """
        self.chatInvited(int(data[1]),data[0],data[2],data[3])

    def tocCHAT_LEFT(self,data):
        """
        CHAT_LEFT:<room id>
        """
        self.chatLeft(int(data[0]))
        del self._receivedchatmembers[int(data[0])]
        del self._roomnames[int(data[0])]

    def tocRVOUS_PROPOSE(self,data):
        """
        RVOUS_PROPOSE:<user>:<uuid>:<cookie>:<seq>:<rip>:<pip>:<vip>:<port>
              [:tlv tag1:tlv value1[:tlv tag2:tlv value2[:...]]]
        """
        user,uid,cookie,seq,rip,pip,vip,port=data[:8]
        cookie=base64.decodestring(cookie)
        port=int(port)
        tlvs={}
        for i in range(8,len(data),2):
            key=data[i]
            value=base64.decodestring(data[i+1])
            tlvs[key]=value
        name=UUIDS[uid]
        try:
            func=getattr(self,"toc%s"%name)
        except:
            self._debug("no function for UID %s" % uid)
            return
        func(user,cookie,seq,pip,vip,port,tlvs)

    def tocSEND_FILE(self,user,cookie,seq,pip,vip,port,tlvs):
        if tlvs.has_key('12'):
            description=tlvs['12']
        else:
            description=""
        subtype,numfiles,size=struct.unpack("!HHI",tlvs['10001'][:8])
        name=tlvs['10001'][8:-4]
        while name[-1]=='\000':
            name=name[:-1]
        self._cookies[cookie]=[user,SEND_FILE_UID,pip,port,{'name':name}]
        self.rvousProposal("send",cookie,user,vip,port,description=description,
                           name=name,files=numfiles,size=size)

    def tocGET_FILE(self,user,cookie,seq,pip,vip,port,tlvs):
        return
        # XXX add this back in
        #reactor.clientTCP(pip,port,GetFileTransfer(self,cookie,os.path.expanduser("~")))
        #self.rvous_accept(user,cookie,GET_FILE_UID)

    def onLine(self):
        """
        called when we are first online
        """
        pass

    def gotConfig(self,mode,buddylist,permit,deny):
        """
        called when we get a configuration from the server
        mode := permit/deny mode
        buddylist := current buddylist
        permit := permit list
        deny := deny list
        """
        pass

    def hearError(self,code,args):
        """
        called when an error is received
        code := error code
        args := misc. arguments (username, etc.)
        """
        pass

    def hearWarning(self,newamount,username):
        """
        called when we get warned
        newamount := the current warning level
        username := the user who warned us, or '' if it's anonymous
        """
        pass

    def hearMessage(self,username,message,autoreply):
        """
        called when you receive an IM
        username := the user who the IM is from
        message := the message
        autoreply := true if the message is an autoreply from an away message
        """
        pass

    def updateBuddy(self,username,online,evilness,signontime,idletime,userclass,away):
        """
        called when a buddy changes state
        username := the user whos state changed
        online := true if the user is online
        evilness := the users current warning level
        signontime := the time the user signed on (UNIX epoch)
        idletime := the time the user has been idle (minutes)
        away := true if the user is away
        userclass := the class of the user (generally " O")
        """
        pass

    def chatJoined(self,roomid,roomname,users):
        """
        we just joined a chat room
        roomid := the AIM id for the room
        roomname := the name for the room
        users := a list of the users already in the room
        """
        pass

    def chatUpdate(self,roomid,username,inroom):
        """
        a user has joined the room
        roomid := the AIM id for the room
        username := the username
        inroom := true if the user is in the room
        """
        pass

    def chatHearMessage(self,roomid,username,message):
        """
        a message was sent to the room
        roomid := the AIM id for the room
        username := the user who sent the message
        message := the message
        """
        pass

    def chatHearWhisper(self,roomid,username,message):
        """
        someone whispered to us in a chatroom
        roomid := the AIM for the room
        username := the user who whispered to us
        message := the message
        """
        pass

    def chatInvited(self,roomid,roomname,username,message):
        """
        we were invited to a chat room
        roomid := the AIM id for the room
        roomname := the name of the room
        username := the user who invited us
        message := the invite message
        """
        pass

    def chatLeft(self,roomid):
        """
        we left the room
        roomid := the AIM id for the room
        """
        pass

    def rvousProposal(self,type,cookie,user,vip,port,**kw):
        """
        we were asked for a rondevouz
        type := the type of rondevous.  currently, one of ["send"]
        cookie := the cookie. pass this to rvous_accept()
        user := the user who asked us
        vip := their verified_ip
        port := the port they want us to conenct to
        kw := misc. args
        """
        pass #self.rvous_accept(cookie)

    def receiveBytes(self,user,file,chunk,sofar,total):
        """
        we received part of a file from a file transfer
        file := the name of the file
        chunk := the chunk of data
        sofar := how much data we've gotten so far
        total := the total amount of data
        """
        pass #print user,file,sofar,total

    def isaway(self):
        """
        return our away status
        """
        return len(self._awaymessage)>0

    def set_config(self,mode,buddylist,permit,deny):
        """
        set the server configuration
        mode := permit mode
        buddylist := buddy list
        permit := permit list
        deny := deny list
        """
        s="m %s\n"%mode
        for g in buddylist.keys():
            s=s+"g %s\n"%g
            for u in buddylist[g]:
                s=s+"b %s\n"%u
        for p in permit:
            s=s+"p %s\n"%p
        for d in deny:
            s=s+"d %s\n"%d
        #s="{\n"+s+"\n}"
        self.sendFlap(2,"toc_set_config %s"%quote(s))

    def add_buddy(self,buddies):
        s=""
        if type(buddies)==type(""): buddies=[buddies]
        for b in buddies:
            s=s+" "+normalize(b)
        self.sendFlap(2,"toc_add_buddy%s"%s)

    def del_buddy(self,buddies):
        s=""
        if type(buddies)==type(""): buddies=[buddies]
        for b in buddies:
            s=s+" "+b
        self.sendFlap(2,"toc_remove_buddy%s"%s)

    def add_permit(self,users):
        if type(users)==type(""): users=[users]
        s=""
        if self._privacymode!=PERMITSOME:
            self._privacymode=PERMITSOME
            self._permitlist=[]
        for u in users:
            u=normalize(u)
            if u not in self._permitlist:self._permitlist.append(u)
            s=s+" "+u
        if not s:
            self._privacymode=DENYALL
            self._permitlist=[]
            self._denylist=[]
        self.sendFlap(2,"toc_add_permit"+s)

    def del_permit(self,users):
        if type(users)==type(""): users=[users]
        p=self._permitlist[:]
        for u in users:
            u=normalize(u)
            if u in p:
                p.remove(u)
        self.add_permit([])
        self.add_permit(p)

    def add_deny(self,users):
        if type(users)==type(""): users=[users]
        s=""
        if self._privacymode!=DENYSOME:
            self._privacymode=DENYSOME
            self._denylist=[]
        for u in users:
            u=normalize(u)
            if u not in self._denylist:self._denylist.append(u)
            s=s+" "+u
        if not s:
            self._privacymode=PERMITALL
            self._permitlist=[]
            self._denylist=[]
        self.sendFlap(2,"toc_add_deny"+s)

    def del_deny(self,users):
        if type(users)==type(""): users=[users]
        d=self._denylist[:]
        for u in users:
            u=normalize(u)
            if u in d:
                d.remove(u)
        self.add_deny([])
        if d:
            self.add_deny(d)

    def signon(self):
        """
        called to finish the setup, and signon to the network
        """
        self.sendFlap(2,"toc_init_done")
        self.sendFlap(2,"toc_set_caps %s" % (SEND_FILE_UID,)) # GET_FILE_UID)

    def say(self,user,message,autoreply=0):
        """
        send a message
        user := the user to send to
        message := the message
        autoreply := true if the message is an autoreply (good for away messages)
        """
        if autoreply: a=" auto"
        else: a=''
        self.sendFlap(2,"toc_send_im %s %s%s"%(normalize(user),quote(message),a))

    def idle(self,idletime=0):
        """
        change idle state
        idletime := the seconds that the user has been away, or 0 if they're back
        """
        self.sendFlap(2,"toc_set_idle %s" % int(idletime))

    def evil(self,user,anon=0):
        """
        warn a user
        user := the user to warn
        anon := if true, an anonymous warning
        """
        self.sendFlap(2,"toc_evil %s %s"%(normalize(user), (not anon and "anon") or "norm"))

    def away(self,message=''):
        """
        change away state
        message := the message, or '' to come back from awayness
        """
        self._awaymessage=message
        if message:
            message=' '+quote(message)
        self.sendFlap(2,"toc_set_away%s"%message)

    def chat_join(self,exchange,roomname):
        """
        join a chat room
        exchange := should almost always be 4
        roomname := room name
        """
        roomname=string.replace(roomname," ","")
        self.sendFlap(2,"toc_chat_join %s %s"%(int(exchange),roomname))

    def chat_say(self,roomid,message):
        """
        send a message to a chatroom
        roomid := the AIM id for the room
        message := the message to send
        """
        self.sendFlap(2,"toc_chat_send %s %s"%(int(roomid),quote(message)))

    def chat_whisper(self,roomid,user,message):
        """
        whisper to another user in a chatroom
        roomid := the AIM id for the room
        user := the user to whisper to
        message := the message to send
        """
        self.sendFlap(2,"toc_chat_whisper %s %s %s"%(int(roomid),normalize(user),quote(message)))

    def chat_leave(self,roomid):
        """
        leave a chat room.
        roomid := the AIM id for the room
        """
        self.sendFlap(2,"toc_chat_leave %s" % int(roomid))

    def chat_invite(self,roomid,usernames,message):
        """
        invite a user[s] to the chat room
        roomid := the AIM id for the room
        usernames := either a string (one username) or a list (more than one)
        message := the message to invite them with
        """
        if type(usernames)==type(""): # a string, one username
            users=usernames
        else:
            users=""
            for u in usernames:
                users=users+u+" "
            users=users[:-1]
        self.sendFlap(2,"toc_chat_invite %s %s %s" % (int(roomid),quote(message),users))

    def chat_accept(self,roomid):
        """
        accept an invite to a chat room
        roomid := the AIM id for the room
        """
        self.sendFlap(2,"toc_chat_accept %s"%int(roomid))

    def rvous_accept(self,cookie):
        user,uuid,pip,port,d=self._cookies[cookie]
        self.sendFlap(2,"toc_rvous_accept %s %s %s" % (normalize(user),
                                                     cookie,uuid))
        if uuid==SEND_FILE_UID:
            protocol.ClientCreator(reactor, SendFileTransfer,self,cookie,user,d["name"]).connectTCP(pip,port)

    def rvous_cancel(self,cookie):
        user,uuid,pip,port,d=self._cookies[cookie]
        self.sendFlap(2,"toc_rvous_accept %s %s %s" % (normalize(user),
                                                       cookie,uuid))
        del self._cookies[cookie]


class SendFileTransfer(protocol.Protocol):
    header_fmt="!4s2H8s6H10I32s3c69s16s2H64s"

    def __init__(self,client,cookie,user,filename):
        self.client=client
        self.cookie=cookie
        self.user=user
        self.filename=filename
        self.hdr=[0,0,0]
        self.sofar=0

    def dataReceived(self,data):
        if not self.hdr[2]==0x202:
            self.hdr=list(struct.unpack(self.header_fmt,data[:256]))
            self.hdr[2]=0x202
            self.hdr[3]=self.cookie
            self.hdr[4]=0
            self.hdr[5]=0
            self.transport.write(apply(struct.pack,[self.header_fmt]+self.hdr))
            data=data[256:]
            if self.hdr[6]==1:
                self.name=self.filename
            else:
                self.name=self.filename+self.hdr[-1]
                while self.name[-1]=="\000":
                    self.name=self.name[:-1]
        if not data: return
        self.sofar=self.sofar+len(data)
        self.client.receiveBytes(self.user,self.name,data,self.sofar,self.hdr[11])
        if self.sofar==self.hdr[11]: # end of this file
            self.hdr[2]=0x204
            self.hdr[7]=self.hdr[7]-1
            self.hdr[9]=self.hdr[9]-1
            self.hdr[19]=DUMMY_CHECKSUM # XXX really calculate this
            self.hdr[18]=self.hdr[18]+1
            self.hdr[21]="\000"
            self.transport.write(apply(struct.pack,[self.header_fmt]+self.hdr))
            self.sofar=0
            if self.hdr[7]==0:
                self.transport.loseConnection()


class GetFileTransfer(protocol.Protocol):
    header_fmt="!4s 2H 8s 6H 10I 32s 3c 69s 16s 2H 64s"
    def __init__(self,client,cookie,dir):
        self.client=client
        self.cookie=cookie
        self.dir=dir
        self.buf=""

    def connectionMade(self):
        def func(f,path,names):
            names.sort(lambda x,y:cmp(string.lower(x),string.lower(y)))
            for n in names:
                name=os.path.join(path,n)
                lt=time.localtime(os.path.getmtime(name))
                size=os.path.getsize(name)
                f[1]=f[1]+size
                f.append("%02d/%02d/%4d %02d:%02d %8d %s" %
                             (lt[1],lt[2],lt[0],lt[3],lt[4],size,name[f[0]:]))
        f=[len(self.dir)+1,0]
        os.path.walk(self.dir,func,f)
        size=f[1]
        self.listing=string.join(f[2:],"\r\n")+"\r\n"
        open("\\listing.txt","w").write(self.listing)
        hdr=["OFT2",256,0x1108,self.cookie,0,0,len(f)-2,len(f)-2,1,1,size,
             len(self.listing),os.path.getmtime(self.dir),
             checksum(self.listing),0,0,0,0,0,0,"OFT_Windows ICBMFT V1.1 32",
             "\002",chr(0x1a),chr(0x10),"","",0,0,""]
        self.transport.write(apply(struct.pack,[self.header_fmt]+hdr))

    def dataReceived(self,data):
        self.buf=self.buf+data
        while len(self.buf)>=256:
            hdr=list(struct.unpack(self.header_fmt,self.buf[:256]))
            self.buf=self.buf[256:]
            if hdr[2]==0x1209:
                self.file=StringIO.StringIO(self.listing)
                self.transport.registerProducer(self,0)
            elif hdr[2]==0x120b: pass
            elif hdr[2]==0x120c: # file request
                file=hdr[-1]
                for k,v in [["\000",""],["\001",os.sep]]:
                    file=string.replace(file,k,v)
                self.name=os.path.join(self.dir,file)
                self.file=open(self.name,'rb')
                hdr[2]=0x0101
                hdr[6]=hdr[7]=1
                hdr[10]=hdr[11]=os.path.getsize(self.name)
                hdr[12]=os.path.getmtime(self.name)
                hdr[13]=checksum_file(self.file)
                self.file.seek(0)
                hdr[18]=hdr[19]=0
                hdr[21]=chr(0x20)
                self.transport.write(apply(struct.pack,[self.header_fmt]+hdr))
                log.msg("got file request for %s"%file,hex(hdr[13]))
            elif hdr[2]==0x0202:
                log.msg("sending file")
                self.transport.registerProducer(self,0)
            elif hdr[2]==0x0204:
                log.msg("real checksum: %s"%hex(hdr[19]))
                del self.file
            elif hdr[2]==0x0205: # resume
                already=hdr[18]
                if already:
                    data=self.file.read(already)
                else:
                    data=""
                log.msg("restarting at %s"%already)
                hdr[2]=0x0106
                hdr[19]=checksum(data)
                self.transport.write(apply(struct.pack,[self.header_fmt]+hdr))
            elif hdr[2]==0x0207:
                self.transport.registerProducer(self,0)
            else:
                log.msg("don't understand 0x%04x"%hdr[2])
                log.msg(hdr)

    def resumeProducing(self):
        data=self.file.read(4096)
        log.msg(len(data))
        if not data:
            self.transport.unregisterProducer()
        self.transport.write(data)

    def pauseProducing(self): pass

    def stopProducing(self): del self.file

# UUIDs
SEND_FILE_UID = "09461343-4C7F-11D1-8222-444553540000"
GET_FILE_UID  = "09461348-4C7F-11D1-8222-444553540000"
UUIDS={
    SEND_FILE_UID:"SEND_FILE",
    GET_FILE_UID:"GET_FILE"
}

# ERRORS
# general
NOT_AVAILABLE=901
CANT_WARN=902
MESSAGES_TOO_FAST=903
# admin
BAD_INPUT=911
BAD_ACCOUNT=912
REQUEST_ERROR=913
SERVICE_UNAVAILABLE=914
# chat
NO_CHAT_IN=950
# im and info
SEND_TOO_FAST=960
MISSED_BIG_IM=961
MISSED_FAST_IM=962
# directory
DIR_FAILURE=970
TOO_MANY_MATCHES=971
NEED_MORE_QUALIFIERS=972
DIR_UNAVAILABLE=973
NO_EMAIL_LOOKUP=974
KEYWORD_IGNORED=975
NO_KEYWORDS=976
BAD_LANGUAGE=977
BAD_COUNTRY=978
DIR_FAIL_UNKNOWN=979
# authorization
BAD_NICKNAME=980
SERVICE_TEMP_UNAVAILABLE=981
WARNING_TOO_HIGH=982
CONNECTING_TOO_QUICK=983
UNKNOWN_SIGNON=989

STD_MESSAGE={}
STD_MESSAGE[NOT_AVAILABLE]="%s not currently available"
STD_MESSAGE[CANT_WARN]="Warning of %s not currently available"
STD_MESSAGE[MESSAGES_TOO_FAST]="A message has been dropped, you are exceeding the server speed limit"
STD_MESSAGE[BAD_INPUT]="Error validating input"
STD_MESSAGE[BAD_ACCOUNT]="Invalid account"
STD_MESSAGE[REQUEST_ERROR]="Error encountered while processing request"
STD_MESSAGE[SERVICE_UNAVAILABLE]="Service unavailable"
STD_MESSAGE[NO_CHAT_IN]="Chat in %s is unavailable"
STD_MESSAGE[SEND_TOO_FAST]="You are sending messages too fast to %s"
STD_MESSAGE[MISSED_BIG_IM]="You missed an IM from %s because it was too big"
STD_MESSAGE[MISSED_FAST_IM]="You missed an IM from %s because it was sent too fast"
# skipping directory for now
STD_MESSAGE[BAD_NICKNAME]="Incorrect nickname or password"
STD_MESSAGE[SERVICE_TEMP_UNAVAILABLE]="The service is temporarily unavailable"
STD_MESSAGE[WARNING_TOO_HIGH]="Your warning level is currently too high to sign on"
STD_MESSAGE[CONNECTING_TOO_QUICK]="You have been connecting and disconnecting too frequently.  Wait 10 minutes and try again.  If you continue to try, you will need to wait even longer."
STD_MESSAGE[UNKNOWN_SIGNON]="An unknown signon error has occurred %s"