1# -*- test-case-name: twisted.words.test -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5
6"""
7An implementation of the OSCAR protocol, which AIM and ICQ use to communcate.
8
9Maintainer: Paul Swartz
10"""
11
12import struct
13import string
14import socket
15import random
16import types
17import re
18from hashlib import md5
19
20from twisted.internet import reactor, defer, protocol
21from twisted.python import log
22
23def logPacketData(data):
24    lines = len(data)/16
25    if lines*16 != len(data): lines=lines+1
26    for i in range(lines):
27        d = tuple(data[16*i:16*i+16])
28        hex = map(lambda x: "%02X"%ord(x),d)
29        text = map(lambda x: (len(repr(x))>3 and '.') or x, d)
30        log.msg(' '.join(hex)+ ' '*3*(16-len(d)) +''.join(text))
31    log.msg('')
32
33def SNAC(fam,sub,id,data,flags=[0,0]):
34    header="!HHBBL"
35    head=struct.pack(header,fam,sub,
36                     flags[0],flags[1],
37                     id)
38    return head+str(data)
39
40def readSNAC(data):
41    header="!HHBBL"
42    head=list(struct.unpack(header,data[:10]))
43    return head+[data[10:]]
44
45def TLV(type,value):
46    header="!HH"
47    head=struct.pack(header,type,len(value))
48    return head+str(value)
49
50def readTLVs(data,count=None):
51    header="!HH"
52    dict={}
53    while data and len(dict)!=count:
54        head=struct.unpack(header,data[:4])
55        dict[head[0]]=data[4:4+head[1]]
56        data=data[4+head[1]:]
57    if not count:
58        return dict
59    return dict,data
60
61def encryptPasswordMD5(password,key):
62    m=md5()
63    m.update(key)
64    m.update(md5(password).digest())
65    m.update("AOL Instant Messenger (SM)")
66    return m.digest()
67
68def encryptPasswordICQ(password):
69    key=[0xF3,0x26,0x81,0xC4,0x39,0x86,0xDB,0x92,0x71,0xA3,0xB9,0xE6,0x53,0x7A,0x95,0x7C]
70    bytes=map(ord,password)
71    r=""
72    for i in range(len(bytes)):
73        r=r+chr(bytes[i]^key[i%len(key)])
74    return r
75
76def dehtml(text):
77    text=string.replace(text,"<br>","\n")
78    text=string.replace(text,"<BR>","\n")
79    text=string.replace(text,"<Br>","\n") # XXX make this a regexp
80    text=string.replace(text,"<bR>","\n")
81    text=re.sub('<.*?>','',text)
82    text=string.replace(text,'&gt;','>')
83    text=string.replace(text,'&lt;','<')
84    text=string.replace(text,'&nbsp;',' ')
85    text=string.replace(text,'&#34;','"')
86    text=string.replace(text,'&amp;','&')
87    return text
88
89def html(text):
90    text=string.replace(text,'"','&#34;')
91    text=string.replace(text,'&','&amp;')
92    text=string.replace(text,'<','&lt;')
93    text=string.replace(text,'>','&gt;')
94    text=string.replace(text,"\n","<br>")
95    return '<html><body bgcolor="white"><font color="black">%s</font></body></html>'%text
96
97class OSCARUser:
98    def __init__(self, name, warn, tlvs):
99        self.name = name
100        self.warning = warn
101        self.flags = []
102        self.caps = []
103        for k,v in tlvs.items():
104            if k == 1: # user flags
105                v=struct.unpack('!H',v)[0]
106                for o, f in [(1,'trial'),
107                             (2,'unknown bit 2'),
108                             (4,'aol'),
109                             (8,'unknown bit 4'),
110                             (16,'aim'),
111                             (32,'away'),
112                             (1024,'activebuddy')]:
113                    if v&o: self.flags.append(f)
114            elif k == 2: # member since date
115                self.memberSince = struct.unpack('!L',v)[0]
116            elif k == 3: # on-since
117                self.onSince = struct.unpack('!L',v)[0]
118            elif k == 4: # idle time
119                self.idleTime = struct.unpack('!H',v)[0]
120            elif k == 5: # unknown
121                pass
122            elif k == 6: # icq online status
123                if v[2] == '\x00':
124                    self.icqStatus = 'online'
125                elif v[2] == '\x01':
126                    self.icqStatus = 'away'
127                elif v[2] == '\x02':
128                    self.icqStatus = 'dnd'
129                elif v[2] == '\x04':
130                    self.icqStatus = 'out'
131                elif v[2] == '\x10':
132                    self.icqStatus = 'busy'
133                else:
134                    self.icqStatus = 'unknown'
135            elif k == 10: # icq ip address
136                self.icqIPaddy = socket.inet_ntoa(v)
137            elif k == 12: # icq random stuff
138                self.icqRandom = v
139            elif k == 13: # capabilities
140                caps=[]
141                while v:
142                    c=v[:16]
143                    if c==CAP_ICON: caps.append("icon")
144                    elif c==CAP_IMAGE: caps.append("image")
145                    elif c==CAP_VOICE: caps.append("voice")
146                    elif c==CAP_CHAT: caps.append("chat")
147                    elif c==CAP_GET_FILE: caps.append("getfile")
148                    elif c==CAP_SEND_FILE: caps.append("sendfile")
149                    elif c==CAP_SEND_LIST: caps.append("sendlist")
150                    elif c==CAP_GAMES: caps.append("games")
151                    else: caps.append(("unknown",c))
152                    v=v[16:]
153                caps.sort()
154                self.caps=caps
155            elif k == 14: pass
156            elif k == 15: # session length (aim)
157                self.sessionLength = struct.unpack('!L',v)[0]
158            elif k == 16: # session length (aol)
159                self.sessionLength = struct.unpack('!L',v)[0]
160            elif k == 30: # no idea
161                pass
162            else:
163                log.msg("unknown tlv for user %s\nt: %s\nv: %s"%(self.name,k,repr(v)))
164
165    def __str__(self):
166        s = '<OSCARUser %s' % self.name
167        o = []
168        if self.warning!=0: o.append('warning level %s'%self.warning)
169        if hasattr(self, 'flags'): o.append('flags %s'%self.flags)
170        if hasattr(self, 'sessionLength'): o.append('online for %i minutes' % (self.sessionLength/60,))
171        if hasattr(self, 'idleTime'): o.append('idle for %i minutes' % self.idleTime)
172        if self.caps: o.append('caps %s'%self.caps)
173        if o:
174            s=s+', '+', '.join(o)
175        s=s+'>'
176        return s
177
178
179class SSIGroup:
180    def __init__(self, name, tlvs = {}):
181        self.name = name
182        #self.tlvs = []
183        #self.userIDs = []
184        self.usersToID = {}
185        self.users = []
186        #if not tlvs.has_key(0xC8): return
187        #buddyIDs = tlvs[0xC8]
188        #while buddyIDs:
189        #    bid = struct.unpack('!H',buddyIDs[:2])[0]
190        #    buddyIDs = buddyIDs[2:]
191        #    self.users.append(bid)
192
193    def findIDFor(self, user):
194        return self.usersToID[user]
195
196    def addUser(self, buddyID, user):
197        self.usersToID[user] = buddyID
198        self.users.append(user)
199        user.group = self
200
201    def oscarRep(self, groupID, buddyID):
202        tlvData = TLV(0xc8, reduce(lambda x,y:x+y, [struct.pack('!H',self.usersToID[x]) for x in self.users]))
203        return struct.pack('!H', len(self.name)) + self.name + \
204               struct.pack('!HH', groupID, buddyID) + '\000\001' + tlvData
205
206
207class SSIBuddy:
208    def __init__(self, name, tlvs = {}):
209        self.name = name
210        self.tlvs = tlvs
211        for k,v in tlvs.items():
212            if k == 0x013c: # buddy comment
213                self.buddyComment = v
214            elif k == 0x013d: # buddy alerts
215                actionFlag = ord(v[0])
216                whenFlag = ord(v[1])
217                self.alertActions = []
218                self.alertWhen = []
219                if actionFlag&1:
220                    self.alertActions.append('popup')
221                if actionFlag&2:
222                    self.alertActions.append('sound')
223                if whenFlag&1:
224                    self.alertWhen.append('online')
225                if whenFlag&2:
226                    self.alertWhen.append('unidle')
227                if whenFlag&4:
228                    self.alertWhen.append('unaway')
229            elif k == 0x013e:
230                self.alertSound = v
231
232    def oscarRep(self, groupID, buddyID):
233        tlvData = reduce(lambda x,y: x+y, map(lambda (k,v):TLV(k,v), self.tlvs.items()), '\000\000')
234        return struct.pack('!H', len(self.name)) + self.name + \
235               struct.pack('!HH', groupID, buddyID) + '\000\000' + tlvData
236
237
238class OscarConnection(protocol.Protocol):
239    def connectionMade(self):
240        self.state=""
241        self.seqnum=0
242        self.buf=''
243        self.stopKeepAliveID = None
244        self.setKeepAlive(4*60) # 4 minutes
245
246    def connectionLost(self, reason):
247        log.msg("Connection Lost! %s" % self)
248        self.stopKeepAlive()
249
250#    def connectionFailed(self):
251#        log.msg("Connection Failed! %s" % self)
252#        self.stopKeepAlive()
253
254    def sendFLAP(self,data,channel = 0x02):
255        header="!cBHH"
256        self.seqnum=(self.seqnum+1)%0xFFFF
257        seqnum=self.seqnum
258        head=struct.pack(header,'*', channel,
259                         seqnum, len(data))
260        self.transport.write(head+str(data))
261#        if isinstance(self, ChatService):
262#            logPacketData(head+str(data))
263
264    def readFlap(self):
265        header="!cBHH"
266        if len(self.buf)<6: return
267        flap=struct.unpack(header,self.buf[:6])
268        if len(self.buf)<6+flap[3]: return
269        data,self.buf=self.buf[6:6+flap[3]],self.buf[6+flap[3]:]
270        return [flap[1],data]
271
272    def dataReceived(self,data):
273#        if isinstance(self, ChatService):
274#            logPacketData(data)
275        self.buf=self.buf+data
276        flap=self.readFlap()
277        while flap:
278            func=getattr(self,"oscar_%s"%self.state,None)
279            if not func:
280                log.msg("no func for state: %s" % self.state)
281            state=func(flap)
282            if state:
283                self.state=state
284            flap=self.readFlap()
285
286    def setKeepAlive(self,t):
287        self.keepAliveDelay=t
288        self.stopKeepAlive()
289        self.stopKeepAliveID = reactor.callLater(t, self.sendKeepAlive)
290
291    def sendKeepAlive(self):
292        self.sendFLAP("",0x05)
293        self.stopKeepAliveID = reactor.callLater(self.keepAliveDelay, self.sendKeepAlive)
294
295    def stopKeepAlive(self):
296        if self.stopKeepAliveID:
297            self.stopKeepAliveID.cancel()
298            self.stopKeepAliveID = None
299
300    def disconnect(self):
301        """
302        send the disconnect flap, and sever the connection
303        """
304        self.sendFLAP('', 0x04)
305        def f(reason): pass
306        self.connectionLost = f
307        self.transport.loseConnection()
308
309
310class SNACBased(OscarConnection):
311    snacFamilies = {
312        # family : (version, toolID, toolVersion)
313    }
314    def __init__(self,cookie):
315        self.cookie=cookie
316        self.lastID=0
317        self.supportedFamilies = ()
318        self.requestCallbacks={} # request id:Deferred
319
320    def sendSNAC(self,fam,sub,data,flags=[0,0]):
321        """
322        send a snac and wait for the response by returning a Deferred.
323        """
324        reqid=self.lastID
325        self.lastID=reqid+1
326        d = defer.Deferred()
327        d.reqid = reqid
328
329        #d.addErrback(self._ebDeferredError,fam,sub,data) # XXX for testing
330
331        self.requestCallbacks[reqid] = d
332        self.sendFLAP(SNAC(fam,sub,reqid,data))
333        return d
334
335    def _ebDeferredError(self, error, fam, sub, data):
336        log.msg('ERROR IN DEFERRED %s' % error)
337        log.msg('on sending of message, family 0x%02x, subtype 0x%02x' % (fam, sub))
338        log.msg('data: %s' % repr(data))
339
340    def sendSNACnr(self,fam,sub,data,flags=[0,0]):
341        """
342        send a snac, but don't bother adding a deferred, we don't care.
343        """
344        self.sendFLAP(SNAC(fam,sub,0x10000*fam+sub,data))
345
346    def oscar_(self,data):
347        self.sendFLAP("\000\000\000\001"+TLV(6,self.cookie), 0x01)
348        return "Data"
349
350    def oscar_Data(self,data):
351        snac=readSNAC(data[1])
352        if self.requestCallbacks.has_key(snac[4]):
353            d = self.requestCallbacks[snac[4]]
354            del self.requestCallbacks[snac[4]]
355            if snac[1]!=1:
356                d.callback(snac)
357            else:
358                d.errback(snac)
359            return
360        func=getattr(self,'oscar_%02X_%02X'%(snac[0],snac[1]),None)
361        if not func:
362            self.oscar_unknown(snac)
363        else:
364            func(snac[2:])
365        return "Data"
366
367    def oscar_unknown(self,snac):
368        log.msg("unknown for %s" % self)
369        log.msg(snac)
370
371
372    def oscar_01_03(self, snac):
373        numFamilies = len(snac[3])/2
374        self.supportedFamilies = struct.unpack("!"+str(numFamilies)+'H', snac[3])
375        d = ''
376        for fam in self.supportedFamilies:
377            if self.snacFamilies.has_key(fam):
378                d=d+struct.pack('!2H',fam,self.snacFamilies[fam][0])
379        self.sendSNACnr(0x01,0x17, d)
380
381    def oscar_01_0A(self,snac):
382        """
383        change of rate information.
384        """
385        # this can be parsed, maybe we can even work it in
386        pass
387
388    def oscar_01_18(self,snac):
389        """
390        host versions, in the same format as we sent
391        """
392        self.sendSNACnr(0x01,0x06,"") #pass
393
394    def clientReady(self):
395        """
396        called when the client is ready to be online
397        """
398        d = ''
399        for fam in self.supportedFamilies:
400            if self.snacFamilies.has_key(fam):
401                version, toolID, toolVersion = self.snacFamilies[fam]
402                d = d + struct.pack('!4H',fam,version,toolID,toolVersion)
403        self.sendSNACnr(0x01,0x02,d)
404
405class BOSConnection(SNACBased):
406    snacFamilies = {
407        0x01:(3, 0x0110, 0x059b),
408        0x13:(3, 0x0110, 0x059b),
409        0x02:(1, 0x0110, 0x059b),
410        0x03:(1, 0x0110, 0x059b),
411        0x04:(1, 0x0110, 0x059b),
412        0x06:(1, 0x0110, 0x059b),
413        0x08:(1, 0x0104, 0x0001),
414        0x09:(1, 0x0110, 0x059b),
415        0x0a:(1, 0x0110, 0x059b),
416        0x0b:(1, 0x0104, 0x0001),
417        0x0c:(1, 0x0104, 0x0001)
418    }
419
420    capabilities = None
421
422    def __init__(self,username,cookie):
423        SNACBased.__init__(self,cookie)
424        self.username=username
425        self.profile = None
426        self.awayMessage = None
427        self.services = {}
428
429        if not self.capabilities:
430            self.capabilities = [CAP_CHAT]
431
432    def parseUser(self,data,count=None):
433        l=ord(data[0])
434        name=data[1:1+l]
435        warn,foo=struct.unpack("!HH",data[1+l:5+l])
436        warn=int(warn/10)
437        tlvs=data[5+l:]
438        if count:
439            tlvs,rest = readTLVs(tlvs,foo)
440        else:
441            tlvs,rest = readTLVs(tlvs), None
442        u = OSCARUser(name, warn, tlvs)
443        if rest == None:
444            return u
445        else:
446            return u, rest
447
448    def oscar_01_05(self, snac, d = None):
449        """
450        data for a new service connection
451        d might be a deferred to be called back when the service is ready
452        """
453        tlvs = readTLVs(snac[3][2:])
454        service = struct.unpack('!H',tlvs[0x0d])[0]
455        ip = tlvs[5]
456        cookie = tlvs[6]
457        #c = serviceClasses[service](self, cookie, d)
458        c = protocol.ClientCreator(reactor, serviceClasses[service], self, cookie, d)
459        def addService(x):
460            self.services[service] = x
461        c.connectTCP(ip, 5190).addCallback(addService)
462        #self.services[service] = c
463
464    def oscar_01_07(self,snac):
465        """
466        rate paramaters
467        """
468        self.sendSNACnr(0x01,0x08,"\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05") # ack
469        self.initDone()
470        self.sendSNACnr(0x13,0x02,'') # SSI rights info
471        self.sendSNACnr(0x02,0x02,'') # location rights info
472        self.sendSNACnr(0x03,0x02,'') # buddy list rights
473        self.sendSNACnr(0x04,0x04,'') # ICBM parms
474        self.sendSNACnr(0x09,0x02,'') # BOS rights
475
476    def oscar_01_10(self,snac):
477        """
478        we've been warned
479        """
480        skip = struct.unpack('!H',snac[3][:2])[0]
481        newLevel = struct.unpack('!H',snac[3][2+skip:4+skip])[0]/10
482        if len(snac[3])>4+skip:
483            by = self.parseUser(snac[3][4+skip:])
484        else:
485            by = None
486        self.receiveWarning(newLevel, by)
487
488    def oscar_01_13(self,snac):
489        """
490        MOTD
491        """
492        pass # we don't care for now
493
494    def oscar_02_03(self, snac):
495        """
496        location rights response
497        """
498        tlvs = readTLVs(snac[3])
499        self.maxProfileLength = tlvs[1]
500
501    def oscar_03_03(self, snac):
502        """
503        buddy list rights response
504        """
505        tlvs = readTLVs(snac[3])
506        self.maxBuddies = tlvs[1]
507        self.maxWatchers = tlvs[2]
508
509    def oscar_03_0B(self, snac):
510        """
511        buddy update
512        """
513        self.updateBuddy(self.parseUser(snac[3]))
514
515    def oscar_03_0C(self, snac):
516        """
517        buddy offline
518        """
519        self.offlineBuddy(self.parseUser(snac[3]))
520
521#    def oscar_04_03(self, snac):
522
523    def oscar_04_05(self, snac):
524        """
525        ICBM parms response
526        """
527        self.sendSNACnr(0x04,0x02,'\x00\x00\x00\x00\x00\x0b\x1f@\x03\xe7\x03\xe7\x00\x00\x00\x00') # IM rights
528
529    def oscar_04_07(self, snac):
530        """
531        ICBM message (instant message)
532        """
533        data = snac[3]
534        cookie, data = data[:8], data[8:]
535        channel = struct.unpack('!H',data[:2])[0]
536        data = data[2:]
537        user, data = self.parseUser(data, 1)
538        tlvs = readTLVs(data)
539        if channel == 1: # message
540            flags = []
541            multiparts = []
542            for k, v in tlvs.items():
543                if k == 2:
544                    while v:
545                        v = v[2:] # skip bad data
546                        messageLength, charSet, charSubSet = struct.unpack('!3H', v[:6])
547                        messageLength -= 4
548                        message = [v[6:6+messageLength]]
549                        if charSet == 0:
550                            pass # don't add anything special
551                        elif charSet == 2:
552                            message.append('unicode')
553                        elif charSet == 3:
554                            message.append('iso-8859-1')
555                        elif charSet == 0xffff:
556                            message.append('none')
557                        if charSubSet == 0xb:
558                            message.append('macintosh')
559                        if messageLength > 0: multiparts.append(tuple(message))
560                        v = v[6+messageLength:]
561                elif k == 3:
562                    flags.append('acknowledge')
563                elif k == 4:
564                    flags.append('auto')
565                elif k == 6:
566                    flags.append('offline')
567                elif k == 8:
568                    iconLength, foo, iconSum, iconStamp = struct.unpack('!LHHL',v)
569                    if iconLength:
570                        flags.append('icon')
571                        flags.append((iconLength, iconSum, iconStamp))
572                elif k == 9:
573                    flags.append('buddyrequest')
574                elif k == 0xb: # unknown
575                    pass
576                elif k == 0x17:
577                    flags.append('extradata')
578                    flags.append(v)
579                else:
580                    log.msg('unknown TLV for incoming IM, %04x, %s' % (k,repr(v)))
581
582#  unknown tlv for user SNewdorf
583#  t: 29
584#  v: '\x00\x00\x00\x05\x02\x01\xd2\x04r\x00\x01\x01\x10/\x8c\x8b\x8a\x1e\x94*\xbc\x80}\x8d\xc4;\x1dEM'
585# XXX what is this?
586            self.receiveMessage(user, multiparts, flags)
587        elif channel == 2: # rondevouz
588            status = struct.unpack('!H',tlvs[5][:2])[0]
589            requestClass = tlvs[5][10:26]
590            moreTLVs = readTLVs(tlvs[5][26:])
591            if requestClass == CAP_CHAT: # a chat request
592                exchange = struct.unpack('!H',moreTLVs[10001][:2])[0]
593                name = moreTLVs[10001][3:-2]
594                instance = struct.unpack('!H',moreTLVs[10001][-2:])[0]
595                if not self.services.has_key(SERVICE_CHATNAV):
596                    self.connectService(SERVICE_CHATNAV,1).addCallback(lambda x: self.services[SERVICE_CHATNAV].getChatInfo(exchange, name, instance).\
597                        addCallback(self._cbGetChatInfoForInvite, user, moreTLVs[12]))
598                else:
599                    self.services[SERVICE_CHATNAV].getChatInfo(exchange, name, instance).\
600                        addCallback(self._cbGetChatInfoForInvite, user, moreTLVs[12])
601            elif requestClass == CAP_SEND_FILE:
602                if moreTLVs.has_key(11): # cancel
603                    log.msg('cancelled file request')
604                    log.msg(status)
605                    return # handle this later
606                name = moreTLVs[10001][9:-7]
607                desc = moreTLVs[12]
608                log.msg('file request from %s, %s, %s' % (user, name, desc))
609                self.receiveSendFileRequest(user, name, desc, cookie)
610            else:
611                log.msg('unsupported rondevouz: %s' % requestClass)
612                log.msg(repr(moreTLVs))
613        else:
614            log.msg('unknown channel %02x' % channel)
615            log.msg(tlvs)
616
617    def _cbGetChatInfoForInvite(self, info, user, message):
618        apply(self.receiveChatInvite, (user,message)+info)
619
620    def oscar_09_03(self, snac):
621        """
622        BOS rights response
623        """
624        tlvs = readTLVs(snac[3])
625        self.maxPermitList = tlvs[1]
626        self.maxDenyList = tlvs[2]
627
628    def oscar_0B_02(self, snac):
629        """
630        stats reporting interval
631        """
632        self.reportingInterval = struct.unpack('!H',snac[3][:2])[0]
633
634    def oscar_13_03(self, snac):
635        """
636        SSI rights response
637        """
638        #tlvs = readTLVs(snac[3])
639        pass # we don't know how to parse this
640
641    # methods to be called by the client, and their support methods
642    def requestSelfInfo(self):
643        """
644        ask for the OSCARUser for ourselves
645        """
646        d = defer.Deferred()
647        self.sendSNAC(0x01, 0x0E, '').addCallback(self._cbRequestSelfInfo, d)
648        return d
649
650    def _cbRequestSelfInfo(self, snac, d):
651        d.callback(self.parseUser(snac[5]))
652
653    def initSSI(self):
654        """
655        this sends the rate request for family 0x13 (Server Side Information)
656        so we can then use it
657        """
658        return self.sendSNAC(0x13, 0x02, '').addCallback(self._cbInitSSI)
659
660    def _cbInitSSI(self, snac, d):
661        return {} # don't even bother parsing this
662
663    def requestSSI(self, timestamp = 0, revision = 0):
664        """
665        request the server side information
666        if the deferred gets None, it means the SSI is the same
667        """
668        return self.sendSNAC(0x13, 0x05,
669            struct.pack('!LH',timestamp,revision)).addCallback(self._cbRequestSSI)
670
671    def _cbRequestSSI(self, snac, args = ()):
672        if snac[1] == 0x0f: # same SSI as we have
673            return
674        itemdata = snac[5][3:]
675        if args:
676            revision, groups, permit, deny, permitMode, visibility = args
677        else:
678            version, revision = struct.unpack('!BH', snac[5][:3])
679            groups = {}
680            permit = []
681            deny = []
682            permitMode = None
683            visibility = None
684        while len(itemdata)>4:
685            nameLength = struct.unpack('!H', itemdata[:2])[0]
686            name = itemdata[2:2+nameLength]
687            groupID, buddyID, itemType, restLength = \
688                struct.unpack('!4H', itemdata[2+nameLength:10+nameLength])
689            tlvs = readTLVs(itemdata[10+nameLength:10+nameLength+restLength])
690            itemdata = itemdata[10+nameLength+restLength:]
691            if itemType == 0: # buddies
692                groups[groupID].addUser(buddyID, SSIBuddy(name, tlvs))
693            elif itemType == 1: # group
694                g = SSIGroup(name, tlvs)
695                if groups.has_key(0): groups[0].addUser(groupID, g)
696                groups[groupID] = g
697            elif itemType == 2: # permit
698                permit.append(name)
699            elif itemType == 3: # deny
700                deny.append(name)
701            elif itemType == 4: # permit deny info
702                if not tlvs.has_key(0xcb):
703                    continue # this happens with ICQ
704                permitMode = {1:'permitall',2:'denyall',3:'permitsome',4:'denysome',5:'permitbuddies'}[ord(tlvs[0xca])]
705                visibility = {'\xff\xff\xff\xff':'all','\x00\x00\x00\x04':'notaim'}[tlvs[0xcb]]
706            elif itemType == 5: # unknown (perhaps idle data)?
707                pass
708            else:
709                log.msg('%s %s %s %s %s' % (name, groupID, buddyID, itemType, tlvs))
710        timestamp = struct.unpack('!L',itemdata)[0]
711        if not timestamp: # we've got more packets coming
712            # which means add some deferred stuff
713            d = defer.Deferred()
714            self.requestCallbacks[snac[4]] = d
715            d.addCallback(self._cbRequestSSI, (revision, groups, permit, deny, permitMode, visibility))
716            return d
717        return (groups[0].users,permit,deny,permitMode,visibility,timestamp,revision)
718
719    def activateSSI(self):
720        """
721        active the data stored on the server (use buddy list, permit deny settings, etc.)
722        """
723        self.sendSNACnr(0x13,0x07,'')
724
725    def startModifySSI(self):
726        """
727        tell the OSCAR server to be on the lookout for SSI modifications
728        """
729        self.sendSNACnr(0x13,0x11,'')
730
731    def addItemSSI(self, item, groupID = None, buddyID = None):
732        """
733        add an item to the SSI server.  if buddyID == 0, then this should be a group.
734        this gets a callback when it's finished, but you can probably ignore it.
735        """
736        if groupID is None:
737            if isinstance(item, SSIGroup):
738                groupID = 0
739            else:
740                groupID = item.group.group.findIDFor(item.group)
741        if buddyID is None:
742            buddyID = item.group.findIDFor(item)
743        return self.sendSNAC(0x13,0x08, item.oscarRep(groupID, buddyID))
744
745    def modifyItemSSI(self, item, groupID = None, buddyID = None):
746        if groupID is None:
747            if isinstance(item, SSIGroup):
748                groupID = 0
749            else:
750                groupID = item.group.group.findIDFor(item.group)
751        if buddyID is None:
752            buddyID = item.group.findIDFor(item)
753        return self.sendSNAC(0x13,0x09, item.oscarRep(groupID, buddyID))
754
755    def delItemSSI(self, item, groupID = None, buddyID = None):
756        if groupID is None:
757            if isinstance(item, SSIGroup):
758                groupID = 0
759            else:
760                groupID = item.group.group.findIDFor(item.group)
761        if buddyID is None:
762            buddyID = item.group.findIDFor(item)
763        return self.sendSNAC(0x13,0x0A, item.oscarRep(groupID, buddyID))
764
765    def endModifySSI(self):
766        self.sendSNACnr(0x13,0x12,'')
767
768    def setProfile(self, profile):
769        """
770        set the profile.
771        send None to not set a profile (different from '' for a blank one)
772        """
773        self.profile = profile
774        tlvs = ''
775        if self.profile is not None:
776            tlvs =  TLV(1,'text/aolrtf; charset="us-ascii"') + \
777                    TLV(2,self.profile)
778
779        tlvs = tlvs + TLV(5, ''.join(self.capabilities))
780        self.sendSNACnr(0x02, 0x04, tlvs)
781
782    def setAway(self, away = None):
783        """
784        set the away message, or return (if away == None)
785        """
786        self.awayMessage = away
787        tlvs = TLV(3,'text/aolrtf; charset="us-ascii"') + \
788               TLV(4,away or '')
789        self.sendSNACnr(0x02, 0x04, tlvs)
790
791    def setIdleTime(self, idleTime):
792        """
793        set our idle time.  don't call more than once with a non-0 idle time.
794        """
795        self.sendSNACnr(0x01, 0x11, struct.pack('!L',idleTime))
796
797    def sendMessage(self, user, message, wantAck = 0, autoResponse = 0, offline = 0 ):  \
798                    #haveIcon = 0, ):
799        """
800        send a message to user (not an OSCARUseR).
801        message can be a string, or a multipart tuple.
802        if wantAck, we return a Deferred that gets a callback when the message is sent.
803        if autoResponse, this message is an autoResponse, as if from an away message.
804        if offline, this is an offline message (ICQ only, I think)
805        """
806        data = ''.join([chr(random.randrange(0, 127)) for i in range(8)]) # cookie
807        data = data + '\x00\x01' + chr(len(user)) + user
808        if not type(message) in (types.TupleType, types.ListType):
809            message = [[message,]]
810            if type(message[0][0]) == types.UnicodeType:
811                message[0].append('unicode')
812        messageData = ''
813        for part in message:
814            charSet = 0
815            if 'unicode' in part[1:]:
816                charSet = 2
817                part[0] = part[0].encode('utf-8')
818            elif 'iso-8859-1' in part[1:]:
819                charSet = 3
820                part[0] = part[0].encode('iso-8859-1')
821            elif 'none' in part[1:]:
822                charSet = 0xffff
823            if 'macintosh' in part[1:]:
824                charSubSet = 0xb
825            else:
826                charSubSet = 0
827            messageData = messageData + '\x01\x01' + \
828                          struct.pack('!3H',len(part[0])+4,charSet,charSubSet)
829            messageData = messageData + part[0]
830        data = data + TLV(2, '\x05\x01\x00\x03\x01\x01\x02'+messageData)
831        if wantAck:
832            data = data + TLV(3,'')
833        if autoResponse:
834            data = data + TLV(4,'')
835        if offline:
836            data = data + TLV(6,'')
837        if wantAck:
838            return self.sendSNAC(0x04, 0x06, data).addCallback(self._cbSendMessageAck, user, message)
839        self.sendSNACnr(0x04, 0x06, data)
840
841    def _cbSendMessageAck(self, snac, user, message):
842        return user, message
843
844    def connectService(self, service, wantCallback = 0, extraData = ''):
845        """
846        connect to another service
847        if wantCallback, we return a Deferred that gets called back when the service is online.
848        if extraData, append that to our request.
849        """
850        if wantCallback:
851            d = defer.Deferred()
852            self.sendSNAC(0x01,0x04,struct.pack('!H',service) + extraData).addCallback(self._cbConnectService, d)
853            return d
854        else:
855            self.sendSNACnr(0x01,0x04,struct.pack('!H',service))
856
857    def _cbConnectService(self, snac, d):
858        self.oscar_01_05(snac[2:], d)
859
860    def createChat(self, shortName):
861        """
862        create a chat room
863        """
864        if self.services.has_key(SERVICE_CHATNAV):
865            return self.services[SERVICE_CHATNAV].createChat(shortName)
866        else:
867            return self.connectService(SERVICE_CHATNAV,1).addCallback(lambda s: s.createChat(shortName))
868
869
870    def joinChat(self, exchange, fullName, instance):
871        """
872        join a chat room
873        """
874        #d = defer.Deferred()
875        return self.connectService(0x0e, 1, TLV(0x01, struct.pack('!HB',exchange, len(fullName)) + fullName +
876                          struct.pack('!H', instance))).addCallback(self._cbJoinChat) #, d)
877        #return d
878
879    def _cbJoinChat(self, chat):
880        del self.services[SERVICE_CHAT]
881        return chat
882
883    def warnUser(self, user, anon = 0):
884        return self.sendSNAC(0x04, 0x08, '\x00'+chr(anon)+chr(len(user))+user).addCallback(self._cbWarnUser)
885
886    def _cbWarnUser(self, snac):
887        oldLevel, newLevel = struct.unpack('!2H', snac[5])
888        return oldLevel, newLevel
889
890    def getInfo(self, user):
891        #if user.
892        return self.sendSNAC(0x02, 0x05, '\x00\x01'+chr(len(user))+user).addCallback(self._cbGetInfo)
893
894    def _cbGetInfo(self, snac):
895        user, rest = self.parseUser(snac[5],1)
896        tlvs = readTLVs(rest)
897        return tlvs.get(0x02,None)
898
899    def getAway(self, user):
900        return self.sendSNAC(0x02, 0x05, '\x00\x03'+chr(len(user))+user).addCallback(self._cbGetAway)
901
902    def _cbGetAway(self, snac):
903        user, rest = self.parseUser(snac[5],1)
904        tlvs = readTLVs(rest)
905        return tlvs.get(0x04,None) # return None if there is no away message
906
907    #def acceptSendFileRequest(self,
908
909    # methods to be overriden by the client
910    def initDone(self):
911        """
912        called when we get the rate information, which means we should do other init. stuff.
913        """
914        log.msg('%s initDone' % self)
915        pass
916
917    def updateBuddy(self, user):
918        """
919        called when a buddy changes status, with the OSCARUser for that buddy.
920        """
921        log.msg('%s updateBuddy %s' % (self, user))
922        pass
923
924    def offlineBuddy(self, user):
925        """
926        called when a buddy goes offline
927        """
928        log.msg('%s offlineBuddy %s' % (self, user))
929        pass
930
931    def receiveMessage(self, user, multiparts, flags):
932        """
933        called when someone sends us a message
934        """
935        pass
936
937    def receiveWarning(self, newLevel, user):
938        """
939        called when someone warns us.
940        user is either None (if it was anonymous) or an OSCARUser
941        """
942        pass
943
944    def receiveChatInvite(self, user, message, exchange, fullName, instance, shortName, inviteTime):
945        """
946        called when someone invites us to a chat room
947        """
948        pass
949
950    def chatReceiveMessage(self, chat, user, message):
951        """
952        called when someone in a chatroom sends us a message in the chat
953        """
954        pass
955
956    def chatMemberJoined(self, chat, member):
957        """
958        called when a member joins the chat
959        """
960        pass
961
962    def chatMemberLeft(self, chat, member):
963        """
964        called when a member leaves the chat
965        """
966        pass
967
968    def receiveSendFileRequest(self, user, file, description, cookie):
969        """
970        called when someone tries to send a file to us
971        """
972        pass
973
974class OSCARService(SNACBased):
975    def __init__(self, bos, cookie, d = None):
976        SNACBased.__init__(self, cookie)
977        self.bos = bos
978        self.d = d
979
980    def connectionLost(self, reason):
981        for k,v in self.bos.services.items():
982            if v == self:
983                del self.bos.services[k]
984                return
985
986    def clientReady(self):
987        SNACBased.clientReady(self)
988        if self.d:
989            self.d.callback(self)
990            self.d = None
991
992class ChatNavService(OSCARService):
993    snacFamilies = {
994        0x01:(3, 0x0010, 0x059b),
995        0x0d:(1, 0x0010, 0x059b)
996    }
997    def oscar_01_07(self, snac):
998        # rate info
999        self.sendSNACnr(0x01, 0x08, '\000\001\000\002\000\003\000\004\000\005')
1000        self.sendSNACnr(0x0d, 0x02, '')
1001
1002    def oscar_0D_09(self, snac):
1003        self.clientReady()
1004
1005    def getChatInfo(self, exchange, name, instance):
1006        d = defer.Deferred()
1007        self.sendSNAC(0x0d,0x04,struct.pack('!HB',exchange,len(name)) + \
1008                      name + struct.pack('!HB',instance,2)). \
1009            addCallback(self._cbGetChatInfo, d)
1010        return d
1011
1012    def _cbGetChatInfo(self, snac, d):
1013        data = snac[5][4:]
1014        exchange, length = struct.unpack('!HB',data[:3])
1015        fullName = data[3:3+length]
1016        instance = struct.unpack('!H',data[3+length:5+length])[0]
1017        tlvs = readTLVs(data[8+length:])
1018        shortName = tlvs[0x6a]
1019        inviteTime = struct.unpack('!L',tlvs[0xca])[0]
1020        info = (exchange,fullName,instance,shortName,inviteTime)
1021        d.callback(info)
1022
1023    def createChat(self, shortName):
1024        #d = defer.Deferred()
1025        data = '\x00\x04\x06create\xff\xff\x01\x00\x03'
1026        data = data + TLV(0xd7, 'en')
1027        data = data + TLV(0xd6, 'us-ascii')
1028        data = data + TLV(0xd3, shortName)
1029        return self.sendSNAC(0x0d, 0x08, data).addCallback(self._cbCreateChat)
1030        #return d
1031
1032    def _cbCreateChat(self, snac): #d):
1033        exchange, length = struct.unpack('!HB',snac[5][4:7])
1034        fullName = snac[5][7:7+length]
1035        instance = struct.unpack('!H',snac[5][7+length:9+length])[0]
1036        #d.callback((exchange, fullName, instance))
1037        return exchange, fullName, instance
1038
1039class ChatService(OSCARService):
1040    snacFamilies = {
1041        0x01:(3, 0x0010, 0x059b),
1042        0x0E:(1, 0x0010, 0x059b)
1043    }
1044    def __init__(self,bos,cookie, d = None):
1045        OSCARService.__init__(self,bos,cookie,d)
1046        self.exchange = None
1047        self.fullName = None
1048        self.instance = None
1049        self.name = None
1050        self.members = None
1051
1052    clientReady = SNACBased.clientReady # we'll do our own callback
1053
1054    def oscar_01_07(self,snac):
1055        self.sendSNAC(0x01,0x08,"\000\001\000\002\000\003\000\004\000\005")
1056        self.clientReady()
1057
1058    def oscar_0E_02(self, snac):
1059#        try: # this is EVIL
1060#            data = snac[3][4:]
1061#            self.exchange, length = struct.unpack('!HB',data[:3])
1062#            self.fullName = data[3:3+length]
1063#            self.instance = struct.unpack('!H',data[3+length:5+length])[0]
1064#            tlvs = readTLVs(data[8+length:])
1065#            self.name = tlvs[0xd3]
1066#            self.d.callback(self)
1067#        except KeyError:
1068        data = snac[3]
1069        self.exchange, length = struct.unpack('!HB',data[:3])
1070        self.fullName = data[3:3+length]
1071        self.instance = struct.unpack('!H',data[3+length:5+length])[0]
1072        tlvs = readTLVs(data[8+length:])
1073        self.name = tlvs[0xd3]
1074        self.d.callback(self)
1075
1076    def oscar_0E_03(self,snac):
1077        users=[]
1078        rest=snac[3]
1079        while rest:
1080            user, rest = self.bos.parseUser(rest, 1)
1081            users.append(user)
1082        if not self.fullName:
1083            self.members = users
1084        else:
1085            self.members.append(users[0])
1086            self.bos.chatMemberJoined(self,users[0])
1087
1088    def oscar_0E_04(self,snac):
1089        user=self.bos.parseUser(snac[3])
1090        for u in self.members:
1091            if u.name == user.name: # same person!
1092                self.members.remove(u)
1093        self.bos.chatMemberLeft(self,user)
1094
1095    def oscar_0E_06(self,snac):
1096        data = snac[3]
1097        user,rest=self.bos.parseUser(snac[3][14:],1)
1098        tlvs = readTLVs(rest[8:])
1099        message=tlvs[1]
1100        self.bos.chatReceiveMessage(self,user,message)
1101
1102    def sendMessage(self,message):
1103        tlvs=TLV(0x02,"us-ascii")+TLV(0x03,"en")+TLV(0x01,message)
1104        self.sendSNAC(0x0e,0x05,
1105                      "\x46\x30\x38\x30\x44\x00\x63\x00\x00\x03\x00\x01\x00\x00\x00\x06\x00\x00\x00\x05"+
1106                      struct.pack("!H",len(tlvs))+
1107                      tlvs)
1108
1109    def leaveChat(self):
1110        self.disconnect()
1111
1112class OscarAuthenticator(OscarConnection):
1113    BOSClass = BOSConnection
1114    def __init__(self,username,password,deferred=None,icq=0):
1115        self.username=username
1116        self.password=password
1117        self.deferred=deferred
1118        self.icq=icq # icq mode is disabled
1119        #if icq and self.BOSClass==BOSConnection:
1120        #    self.BOSClass=ICQConnection
1121
1122    def oscar_(self,flap):
1123        if not self.icq:
1124            self.sendFLAP("\000\000\000\001", 0x01)
1125            self.sendFLAP(SNAC(0x17,0x06,0,
1126                               TLV(TLV_USERNAME,self.username)+
1127                               TLV(0x004B,'')))
1128            self.state="Key"
1129        else:
1130            encpass=encryptPasswordICQ(self.password)
1131            self.sendFLAP('\000\000\000\001'+
1132                          TLV(0x01,self.username)+
1133                          TLV(0x02,encpass)+
1134                          TLV(0x03,'ICQ Inc. - Product of ICQ (TM).2001b.5.18.1.3659.85')+
1135                          TLV(0x16,"\x01\x0a")+
1136                          TLV(0x17,"\x00\x05")+
1137                          TLV(0x18,"\x00\x12")+
1138                          TLV(0x19,"\000\001")+
1139                          TLV(0x1a,"\x0eK")+
1140                          TLV(0x14,"\x00\x00\x00U")+
1141                          TLV(0x0f,"en")+
1142                          TLV(0x0e,"us"),0x01)
1143            self.state="Cookie"
1144
1145    def oscar_Key(self,data):
1146        snac=readSNAC(data[1])
1147        key=snac[5][2:]
1148        encpass=encryptPasswordMD5(self.password,key)
1149        self.sendFLAP(SNAC(0x17,0x02,0,
1150                           TLV(TLV_USERNAME,self.username)+
1151                           TLV(TLV_PASSWORD,encpass)+
1152                           TLV(0x004C, '')+ # unknown
1153                           TLV(TLV_CLIENTNAME,"AOL Instant Messenger (SM), version 4.8.2790/WIN32")+
1154                           TLV(0x0016,"\x01\x09")+
1155                           TLV(TLV_CLIENTMAJOR,"\000\004")+
1156                           TLV(TLV_CLIENTMINOR,"\000\010")+
1157                           TLV(0x0019,"\000\000")+
1158                           TLV(TLV_CLIENTSUB,"\x0A\xE6")+
1159                           TLV(0x0014,"\x00\x00\x00\xBB")+
1160                           TLV(TLV_LANG,"en")+
1161                           TLV(TLV_COUNTRY,"us")+
1162                           TLV(TLV_USESSI,"\001")))
1163        return "Cookie"
1164
1165    def oscar_Cookie(self,data):
1166        snac=readSNAC(data[1])
1167        if self.icq:
1168            i=snac[5].find("\000")
1169            snac[5]=snac[5][i:]
1170        tlvs=readTLVs(snac[5])
1171        if tlvs.has_key(6):
1172            self.cookie=tlvs[6]
1173            server,port=string.split(tlvs[5],":")
1174            d = self.connectToBOS(server, int(port))
1175            d.addErrback(lambda x: log.msg("Connection Failed! Reason: %s" % x))
1176            if self.deferred:
1177                d.chainDeferred(self.deferred)
1178            self.disconnect()
1179        elif tlvs.has_key(8):
1180            errorcode=tlvs[8]
1181            errorurl=tlvs[4]
1182            if errorcode=='\000\030':
1183                error="You are attempting to sign on again too soon.  Please try again later."
1184            elif errorcode=='\000\005':
1185                error="Invalid Username or Password."
1186            else: error=repr(errorcode)
1187            self.error(error,errorurl)
1188        else:
1189            log.msg('hmm, weird tlvs for %s cookie packet' % str(self))
1190            log.msg(tlvs)
1191            log.msg('snac')
1192            log.msg(str(snac))
1193        return "None"
1194
1195    def oscar_None(self,data): pass
1196
1197    def connectToBOS(self, server, port):
1198        c = protocol.ClientCreator(reactor, self.BOSClass, self.username, self.cookie)
1199        return c.connectTCP(server, int(port))
1200
1201    def error(self,error,url):
1202        log.msg("ERROR! %s %s" % (error,url))
1203        if self.deferred: self.deferred.errback((error,url))
1204        self.transport.loseConnection()
1205
1206FLAP_CHANNEL_NEW_CONNECTION = 0x01
1207FLAP_CHANNEL_DATA = 0x02
1208FLAP_CHANNEL_ERROR = 0x03
1209FLAP_CHANNEL_CLOSE_CONNECTION = 0x04
1210
1211SERVICE_CHATNAV = 0x0d
1212SERVICE_CHAT = 0x0e
1213serviceClasses = {
1214    SERVICE_CHATNAV:ChatNavService,
1215    SERVICE_CHAT:ChatService
1216}
1217TLV_USERNAME = 0x0001
1218TLV_CLIENTNAME = 0x0003
1219TLV_COUNTRY = 0x000E
1220TLV_LANG = 0x000F
1221TLV_CLIENTMAJOR = 0x0017
1222TLV_CLIENTMINOR = 0x0018
1223TLV_CLIENTSUB = 0x001A
1224TLV_PASSWORD = 0x0025
1225TLV_USESSI = 0x004A
1226
1227CAP_ICON = '\011F\023FL\177\021\321\202"DEST\000\000'
1228CAP_VOICE = '\011F\023AL\177\021\321\202"DEST\000\000'
1229CAP_IMAGE = '\011F\023EL\177\021\321\202"DEST\000\000'
1230CAP_CHAT = 't\217$ b\207\021\321\202"DEST\000\000'
1231CAP_GET_FILE = '\011F\023HL\177\021\321\202"DEST\000\000'
1232CAP_SEND_FILE = '\011F\023CL\177\021\321\202"DEST\000\000'
1233CAP_GAMES = '\011F\023GL\177\021\321\202"DEST\000\000'
1234CAP_SEND_LIST = '\011F\023KL\177\021\321\202"DEST\000\000'
1235CAP_SERV_REL = '\011F\023IL\177\021\321\202"DEST\000\000'
1236