]> code.delx.au - pymsnt/blob - src/tlib/msn/msn.py
More generic P2P. Avatars almost working in testing suite.
[pymsnt] / src / tlib / msn / msn.py
1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
3 # Copyright (C) 2004-2005 James C. Bunton
4 #
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of version 2.1 of the GNU Lesser General Public
7 # License as published by the Free Software Foundation.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 #
18
19 """
20 MSNP11 Protocol (client only) - semi-experimental
21
22 Stability: unstable.
23
24 This module provides support for clients using the MSN Protocol (MSNP11).
25 There are basically 3 servers involved in any MSN session:
26
27 I{Dispatch server}
28
29 The DispatchClient class handles connections to the
30 dispatch server, which basically delegates users to a
31 suitable notification server.
32
33 You will want to subclass this and handle the gotNotificationReferral
34 method appropriately.
35
36 I{Notification Server}
37
38 The NotificationClient class handles connections to the
39 notification server, which acts as a session server
40 (state updates, message negotiation etc...)
41
42 I{Switcboard Server}
43
44 The SwitchboardClient handles connections to switchboard
45 servers which are used to conduct conversations with other users.
46
47 There are also two classes (FileSend and FileReceive) used
48 for file transfers.
49
50 Clients handle events in two ways.
51
52 - each client request requiring a response will return a Deferred,
53 the callback for same will be fired when the server sends the
54 required response
55 - Events which are not in response to any client request have
56 respective methods which should be overridden and handled in
57 an adequate manner
58
59 Most client request callbacks require more than one argument,
60 and since Deferreds can only pass the callback one result,
61 most of the time the callback argument will be a tuple of
62 values (documented in the respective request method).
63 To make reading/writing code easier, callbacks can be defined in
64 a number of ways to handle this 'cleanly'. One way would be to
65 define methods like: def callBack(self, (arg1, arg2, arg)): ...
66 another way would be to do something like:
67 d.addCallback(lambda result: myCallback(*result)).
68
69 If the server sends an error response to a client request,
70 the errback of the corresponding Deferred will be called,
71 the argument being the corresponding error code.
72
73 B{NOTE}:
74 Due to the lack of an official spec for MSNP11, extra checking
75 than may be deemed necessary often takes place considering the
76 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
77 main clients) is called, or an MSNProtocolError is raised, it's
78 probably a good idea to submit a bug report. ;)
79 Use of this module requires that PyOpenSSL is installed.
80
81 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
82 @author: U{James Bunton<mailto:james@delx.cjb.net>}
83 """
84
85 from __future__ import nested_scopes
86
87 # Sibling imports
88 from twisted.protocols.basic import LineReceiver
89 try:
90 from twisted.web.http import HTTPClient
91 except ImportError:
92 try:
93 from twisted.protocols.http import HTTPClient
94 except ImportError:
95 print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
96 raise
97 import msnp11chl
98
99 # Twisted imports
100 from twisted.internet import reactor, task
101 from twisted.internet.defer import Deferred
102 from twisted.internet.protocol import ClientFactory
103 from twisted.internet.ssl import ClientContextFactory
104 from twisted.python import failure, log
105 from twisted.xish.domish import unescapeFromXml
106
107 # Compat stuff
108 from tlib import xmlw
109
110 # System imports
111 import types, operator, os, sys, base64, random, struct, random, sha, base64, StringIO
112 from urllib import quote, unquote
113
114
115 MSN_PROTOCOL_VERSION = "MSNP11 CVR0" # protocol version
116 MSN_PORT = 1863 # default dispatch server port
117 MSN_MAX_MESSAGE = 1664 # max message length
118 MSN_CVR_STR = "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
119 MSN_AVATAR_GUID = "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
120 MSN_MSNFTP_GUID = "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
121
122 # auth constants
123 LOGIN_SUCCESS = 1
124 LOGIN_FAILURE = 2
125 LOGIN_REDIRECT = 3
126
127 # list constants
128 FORWARD_LIST = 1
129 ALLOW_LIST = 2
130 BLOCK_LIST = 4
131 REVERSE_LIST = 8
132 PENDING_LIST = 16
133
134 # phone constants
135 HOME_PHONE = "PHH"
136 WORK_PHONE = "PHW"
137 MOBILE_PHONE = "PHM"
138 HAS_PAGER = "MOB"
139 HAS_BLOG = "HSB"
140
141 # status constants
142 STATUS_ONLINE = 'NLN'
143 STATUS_OFFLINE = 'FLN'
144 STATUS_HIDDEN = 'HDN'
145 STATUS_IDLE = 'IDL'
146 STATUS_AWAY = 'AWY'
147 STATUS_BUSY = 'BSY'
148 STATUS_BRB = 'BRB'
149 STATUS_PHONE = 'PHN'
150 STATUS_LUNCH = 'LUN'
151
152 CR = "\r"
153 LF = "\n"
154 PINGSPEED = 50.0
155
156 LINEDEBUG = True
157 MESSAGEDEBUG = True
158 MSNP2PDEBUG = True
159
160 def getVal(inp):
161 return inp.split('=')[1]
162
163 def getVals(params):
164 userHandle = ""
165 screenName = ""
166 userGuid = ""
167 lists = -1
168 groups = []
169 for p in params:
170 if not p:
171 continue
172 elif p[0] == 'N':
173 userHandle = getVal(p)
174 elif p[0] == 'F':
175 screenName = unquote(getVal(p))
176 elif p[0] == 'C':
177 userGuid = getVal(p)
178 elif p.isdigit():
179 lists = int(p)
180 else: # Must be the groups
181 try:
182 groups = p.split(',')
183 except:
184 raise MSNProtocolError, "Unknown LST/ADC response" + str(params) # debug
185
186 return userHandle, screenName, userGuid, lists, groups
187
188 def b64enc(s):
189 return base64.encodestring(s).replace("\n", "")
190
191 def random_guid():
192 format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
193 data = []
194 for x in xrange(8):
195 data.append(random.random() * 0xAAFF + 0x1111)
196 data = tuple(data)
197
198 return format % data
199
200 def checkParamLen(num, expected, cmd, error=None):
201 if error == None: error = "Invalid Number of Parameters for %s" % cmd
202 if num != expected: raise MSNProtocolError, error
203
204 def _parseHeader(h, v):
205 """
206 Split a certin number of known
207 header values with the format:
208 field1=val,field2=val,field3=val into
209 a dict mapping fields to values.
210 @param h: the header's key
211 @param v: the header's value as a string
212 """
213
214 if h in ('passporturls','authentication-info','www-authenticate'):
215 v = v.replace('Passport1.4','').lstrip()
216 fields = {}
217 for fieldPair in v.split(','):
218 try:
219 field,value = fieldPair.split('=',1)
220 fields[field.lower()] = value
221 except ValueError:
222 fields[field.lower()] = ''
223 return fields
224 else: return v
225
226 def _parsePrimitiveHost(host):
227 # Ho Ho Ho
228 h,p = host.replace('https://','').split('/',1)
229 p = '/' + p
230 return h,p
231
232 def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
233 """
234 This function is used internally and should not ever be called
235 directly.
236 """
237 cb = Deferred()
238 def _cb(server, auth):
239 loginFac = ClientFactory()
240 loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
241 reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
242
243 if cached:
244 _cb(nexusServer, authData)
245 else:
246 fac = ClientFactory()
247 d = Deferred()
248 d.addCallbacks(_cb, callbackArgs=(authData,))
249 d.addErrback(lambda f: cb.errback(f))
250 fac.protocol = lambda : PassportNexus(d, nexusServer)
251 reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
252 return cb
253
254
255 class PassportNexus(HTTPClient):
256
257 """
258 Used to obtain the URL of a valid passport
259 login HTTPS server.
260
261 This class is used internally and should
262 not be instantiated directly -- that is,
263 The passport logging in process is handled
264 transparantly by NotificationClient.
265 """
266
267 def __init__(self, deferred, host):
268 self.deferred = deferred
269 self.host, self.path = _parsePrimitiveHost(host)
270
271 def connectionMade(self):
272 HTTPClient.connectionMade(self)
273 self.sendCommand('GET', self.path)
274 self.sendHeader('Host', self.host)
275 self.endHeaders()
276 self.headers = {}
277
278 def handleHeader(self, header, value):
279 h = header.lower()
280 self.headers[h] = _parseHeader(h, value)
281
282 def handleEndHeaders(self):
283 if self.connected: self.transport.loseConnection()
284 if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
285 self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
286 else:
287 self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
288
289 def handleResponse(self, r): pass
290
291 class PassportLogin(HTTPClient):
292 """
293 This class is used internally to obtain
294 a login ticket from a passport HTTPS
295 server -- it should not be used directly.
296 """
297
298 _finished = 0
299
300 def __init__(self, deferred, userHandle, passwd, host, authData):
301 self.deferred = deferred
302 self.userHandle = userHandle
303 self.passwd = passwd
304 self.authData = authData
305 self.host, self.path = _parsePrimitiveHost(host)
306
307 def connectionMade(self):
308 self.sendCommand('GET', self.path)
309 self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
310 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
311 self.sendHeader('Host', self.host)
312 self.endHeaders()
313 self.headers = {}
314
315 def handleHeader(self, header, value):
316 h = header.lower()
317 self.headers[h] = _parseHeader(h, value)
318
319 def handleEndHeaders(self):
320 if self._finished: return
321 self._finished = 1 # I think we need this because of HTTPClient
322 if self.connected: self.transport.loseConnection()
323 authHeader = 'authentication-info'
324 _interHeader = 'www-authenticate'
325 if self.headers.has_key(_interHeader): authHeader = _interHeader
326 try:
327 info = self.headers[authHeader]
328 status = info['da-status']
329 handler = getattr(self, 'login_%s' % (status,), None)
330 if handler:
331 handler(info)
332 else: raise Exception()
333 except Exception, e:
334 self.deferred.errback(failure.Failure(e))
335
336 def handleResponse(self, r): pass
337
338 def login_success(self, info):
339 ticket = info['from-pp']
340 ticket = ticket[1:len(ticket)-1]
341 self.deferred.callback((LOGIN_SUCCESS, ticket))
342
343 def login_failed(self, info):
344 self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
345
346 def login_redir(self, info):
347 self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
348
349 class MSNProtocolError(Exception):
350 """
351 This Exception is basically used for debugging
352 purposes, as the official MSN server should never
353 send anything _wrong_ and nobody in their right
354 mind would run their B{own} MSN server.
355 If it is raised by default command handlers
356 (handle_BLAH) the error will be logged.
357 """
358 pass
359
360 class MSNMessage:
361
362 """
363 I am the class used to represent an 'instant' message.
364
365 @ivar userHandle: The user handle (passport) of the sender
366 (this is only used when receiving a message)
367 @ivar screenName: The screen name of the sender (this is only used
368 when receiving a message)
369 @ivar message: The message
370 @ivar headers: The message headers
371 @type headers: dict
372 @ivar length: The message length (including headers and line endings)
373 @ivar ack: This variable is used to tell the server how to respond
374 once the message has been sent. If set to MESSAGE_ACK
375 (default) the server will respond with an ACK upon receiving
376 the message, if set to MESSAGE_NACK the server will respond
377 with a NACK upon failure to receive the message.
378 If set to MESSAGE_ACK_NONE the server will do nothing.
379 This is relevant for the return value of
380 SwitchboardClient.sendMessage (which will return
381 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
382 and will fire when the respective ACK or NACK is received).
383 If set to MESSAGE_ACK_NONE sendMessage will return None.
384 """
385 MESSAGE_ACK = 'A'
386 MESSAGE_ACK_FAT = 'D'
387 MESSAGE_NACK = 'N'
388 MESSAGE_ACK_NONE = 'U'
389
390 ack = MESSAGE_ACK
391
392 def __init__(self, length=0, userHandle="", screenName="", message="", specialMessage=False):
393 self.userHandle = userHandle
394 self.screenName = screenName
395 self.specialMessage = specialMessage
396 self.message = message
397 self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
398 self.length = length
399 self.readPos = 0
400
401 def _calcMessageLen(self):
402 """
403 used to calculte the number to send
404 as the message length when sending a message.
405 """
406 return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
407
408 def setHeader(self, header, value):
409 """ set the desired header """
410 self.headers[header] = value
411
412 def getHeader(self, header):
413 """
414 get the desired header value
415 @raise KeyError: if no such header exists.
416 """
417 return self.headers[header]
418
419 def hasHeader(self, header):
420 """ check to see if the desired header exists """
421 return self.headers.has_key(header)
422
423 def getMessage(self):
424 """ return the message - not including headers """
425 return self.message
426
427 def setMessage(self, message):
428 """ set the message text """
429 self.message = message
430
431
432 class MSNObject:
433 """
434 Used to represent a MSNObject. This can be currently only be an avatar.
435
436 @ivar creator: The userHandle of the creator of this picture.
437 @ivar imageData: The PNG image data (only for our own avatar)
438 @ivar type: Always set to 3, for avatar.
439 @ivar size: The size of the image.
440 @ivar location: The filename of the image.
441 @ivar friendly: Unknown.
442 @ivar text: The textual representation of this MSNObject.
443 """
444 def __init__(self, s=""):
445 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
446 if s:
447 self.parse(s)
448 else:
449 self.setNull()
450
451 def setData(self, creator, imageData):
452 """ Set the creator and imageData for this object """
453 self.creator = creator
454 self.imageData = imageData
455 self.size = len(imageData)
456 self.type = 3
457 self.location = "TMP" + str(random.randint(1000,9999))
458 self.friendly = "AAA="
459 self.sha1d = b64enc(sha.sha(imageData).digest())
460 self.makeText()
461
462 def setNull(self):
463 self.creator = ""
464 self.imageData = ""
465 self.size = 0
466 self.type = 0
467 self.location = ""
468 self.friendly = ""
469 self.sha1d = ""
470 self.text = ""
471
472 def makeText(self):
473 """ Makes a textual representation of this MSNObject. Stores it in self.text """
474 h = []
475 h.append("Creator")
476 h.append(self.creator)
477 h.append("Size")
478 h.append(str(self.size))
479 h.append("Type")
480 h.append(str(self.type))
481 h.append("Location")
482 h.append(self.location)
483 h.append("Friendly")
484 h.append(self.friendly)
485 h.append("SHA1D")
486 h.append(self.sha1d)
487 sha1c = b64enc(sha.sha("".join(h)).digest())
488 self.text = '<msnobj Creator="%s" Size="%s" Type="%s" Location="%s" Friendly="%s" SHA1D="%s" SHA1C="%s"/>' % (self.creator, str(self.size), str(self.type), self.location, self.friendly, self.sha1d, sha1c)
489
490 def parse(self, s):
491 e = xmlw.parseText(s, True)
492 self.creator = e.getAttribute("Creator")
493 self.size = int(e.getAttribute("Size"))
494 self.type = int(e.getAttribute("Type"))
495 self.location = e.getAttribute("Location")
496 self.friendly = e.getAttribute("Friendly")
497 self.sha1d = e.getAttribute("SHA1D")
498 self.text = s
499
500
501 class MSNContact:
502
503 """
504 This class represents a contact (user).
505
506 @ivar userGuid: The contact's user guid (unique string)
507 @ivar userHandle: The contact's user handle (passport).
508 @ivar screenName: The contact's screen name.
509 @ivar groups: A list of all the group IDs which this
510 contact belongs to.
511 @ivar lists: An integer representing the sum of all lists
512 that this contact belongs to.
513 @ivar caps: int, The capabilities of this client
514 @ivar msnobj: The MSNObject representing the contact's avatar
515 @ivar status: The contact's status code.
516 @type status: str if contact's status is known, None otherwise.
517 @ivar personal: The contact's personal message .
518 @type personal: str if contact's personal message is known, None otherwise.
519
520 @ivar homePhone: The contact's home phone number.
521 @type homePhone: str if known, otherwise None.
522 @ivar workPhone: The contact's work phone number.
523 @type workPhone: str if known, otherwise None.
524 @ivar mobilePhone: The contact's mobile phone number.
525 @type mobilePhone: str if known, otherwise None.
526 @ivar hasPager: Whether or not this user has a mobile pager
527 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
528 (true=yes, false=no)
529 """
530 MSNC1 = 0x10000000
531 MSNC2 = 0x20000000
532 MSNC3 = 0x30000000
533 MSNC4 = 0x40000000
534
535 def __init__(self, userGuid="", userHandle="", screenName="", lists=0, caps=0, msnobj=None, groups={}, status=None, personal=""):
536 self.userGuid = userGuid
537 self.userHandle = userHandle
538 self.screenName = screenName
539 self.lists = lists
540 self.caps = caps
541 self.msnobj = msnobj
542 self.msnobjGot = True
543 self.groups = [] # if applicable
544 self.status = status # current status
545 self.personal = personal
546
547 # phone details
548 self.homePhone = None
549 self.workPhone = None
550 self.mobilePhone = None
551 self.hasPager = None
552 self.hasBlog = None
553
554 def setPhone(self, phoneType, value):
555 """
556 set phone numbers/values for this specific user.
557 for phoneType check the *_PHONE constants and HAS_PAGER
558 """
559
560 t = phoneType.upper()
561 if t == HOME_PHONE: self.homePhone = value
562 elif t == WORK_PHONE: self.workPhone = value
563 elif t == MOBILE_PHONE: self.mobilePhone = value
564 elif t == HAS_PAGER: self.hasPager = value
565 elif t == HAS_BLOG: self.hasBlog = value
566 #else: raise ValueError, "Invalid Phone Type: " + t
567
568 def addToList(self, listType):
569 """
570 Update the lists attribute to
571 reflect being part of the
572 given list.
573 """
574 self.lists |= listType
575
576 def removeFromList(self, listType):
577 """
578 Update the lists attribute to
579 reflect being removed from the
580 given list.
581 """
582 self.lists ^= listType
583
584 class MSNContactList:
585 """
586 This class represents a basic MSN contact list.
587
588 @ivar contacts: All contacts on my various lists
589 @type contacts: dict (mapping user handles to MSNContact objects)
590 @ivar groups: a mapping of group ids to group names
591 (groups can only exist on the forward list)
592 @type groups: dict
593
594 B{Note}:
595 This is used only for storage and doesn't effect the
596 server's contact list.
597 """
598
599 def __init__(self):
600 self.contacts = {}
601 self.groups = {}
602 self.autoAdd = 0
603 self.privacy = 0
604 self.version = 0
605
606 def _getContactsFromList(self, listType):
607 """
608 Obtain all contacts which belong
609 to the given list type.
610 """
611 return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
612
613 def addContact(self, contact):
614 """
615 Add a contact
616 """
617 self.contacts[contact.userHandle] = contact
618
619 def remContact(self, userHandle):
620 """
621 Remove a contact
622 """
623 try:
624 del self.contacts[userHandle]
625 except KeyError: pass
626
627 def getContact(self, userHandle):
628 """
629 Obtain the MSNContact object
630 associated with the given
631 userHandle.
632 @return: the MSNContact object if
633 the user exists, or None.
634 """
635 try:
636 return self.contacts[userHandle]
637 except KeyError:
638 return None
639
640 def getBlockedContacts(self):
641 """
642 Obtain all the contacts on my block list
643 """
644 return self._getContactsFromList(BLOCK_LIST)
645
646 def getAuthorizedContacts(self):
647 """
648 Obtain all the contacts on my auth list.
649 (These are contacts which I have verified
650 can view my state changes).
651 """
652 return self._getContactsFromList(ALLOW_LIST)
653
654 def getReverseContacts(self):
655 """
656 Get all contacts on my reverse list.
657 (These are contacts which have added me
658 to their forward list).
659 """
660 return self._getContactsFromList(REVERSE_LIST)
661
662 def getContacts(self):
663 """
664 Get all contacts on my forward list.
665 (These are the contacts which I have added
666 to my list).
667 """
668 return self._getContactsFromList(FORWARD_LIST)
669
670 def setGroup(self, id, name):
671 """
672 Keep a mapping from the given id
673 to the given name.
674 """
675 self.groups[id] = name
676
677 def remGroup(self, id):
678 """
679 Removed the stored group
680 mapping for the given id.
681 """
682 try:
683 del self.groups[id]
684 except KeyError: pass
685 for c in self.contacts:
686 if id in c.groups: c.groups.remove(id)
687
688
689 class MSNEventBase(LineReceiver):
690 """
691 This class provides support for handling / dispatching events and is the
692 base class of the three main client protocols (DispatchClient,
693 NotificationClient, SwitchboardClient)
694 """
695
696 def __init__(self):
697 self.ids = {} # mapping of ids to Deferreds
698 self.currentID = 0
699 self.connected = 0
700 self.setLineMode()
701 self.currentMessage = None
702
703 def connectionLost(self, reason):
704 self.ids = {}
705 self.connected = 0
706
707 def connectionMade(self):
708 self.connected = 1
709
710 def _fireCallback(self, id, *args):
711 """
712 Fire the callback for the given id
713 if one exists and return 1, else return false
714 """
715 if self.ids.has_key(id):
716 self.ids[id][0].callback(args)
717 del self.ids[id]
718 return 1
719 return 0
720
721 def _nextTransactionID(self):
722 """ return a usable transaction ID """
723 self.currentID += 1
724 if self.currentID > 1000: self.currentID = 1
725 return self.currentID
726
727 def _createIDMapping(self, data=None):
728 """
729 return a unique transaction ID that is mapped internally to a
730 deferred .. also store arbitrary data if it is needed
731 """
732 id = self._nextTransactionID()
733 d = Deferred()
734 self.ids[id] = (d, data)
735 return (id, d)
736
737 def checkMessage(self, message):
738 """
739 process received messages to check for file invitations and
740 typing notifications and other control type messages
741 """
742 raise NotImplementedError
743
744 def sendLine(self, line):
745 if LINEDEBUG: log.msg(">> " + line)
746 LineReceiver.sendLine(self, line)
747
748 def lineReceived(self, line):
749 if LINEDEBUG: log.msg("<< " + line)
750 if self.currentMessage:
751 self.currentMessage.readPos += len(line+CR+LF)
752 try:
753 header, value = line.split(':')
754 self.currentMessage.setHeader(header, unquote(value).lstrip())
755 return
756 except ValueError:
757 #raise MSNProtocolError, "Invalid Message Header"
758 line = ""
759 if line == "" or self.currentMessage.specialMessage:
760 self.setRawMode()
761 if self.currentMessage.readPos == self.currentMessage.length: self.rawDataReceived("") # :(
762 return
763 try:
764 cmd, params = line.split(' ', 1)
765 except ValueError:
766 raise MSNProtocolError, "Invalid Message, %s" % repr(line)
767
768 if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
769 if cmd.isdigit():
770 if self.ids.has_key(params.split(' ')[0]):
771 self.ids[id].errback(int(cmd))
772 del self.ids[id]
773 return
774 else: # we received an error which doesn't map to a sent command
775 self.gotError(int(cmd))
776 return
777
778 handler = getattr(self, "handle_%s" % cmd.upper(), None)
779 if handler:
780 try: handler(params.split(' '))
781 except MSNProtocolError, why: self.gotBadLine(line, why)
782 else:
783 self.handle_UNKNOWN(cmd, params.split(' '))
784
785 def rawDataReceived(self, data):
786 extra = ""
787 self.currentMessage.readPos += len(data)
788 diff = self.currentMessage.readPos - self.currentMessage.length
789 if diff > 0:
790 self.currentMessage.message += data[:-diff]
791 extra = data[-diff:]
792 elif diff == 0:
793 self.currentMessage.message += data
794 else:
795 self.currentMessage.message += data
796 return
797 del self.currentMessage.readPos
798 m = self.currentMessage
799 self.currentMessage = None
800 if MESSAGEDEBUG: log.msg(m.message)
801 if not self.checkMessage(m):
802 self.setLineMode(extra)
803 return
804 self.gotMessage(m)
805 self.setLineMode(extra)
806
807 ### protocol command handlers - no need to override these.
808
809 def handle_MSG(self, params):
810 checkParamLen(len(params), 3, 'MSG')
811 try:
812 messageLen = int(params[2])
813 except ValueError: raise MSNProtocolError, "Invalid Parameter for MSG length argument"
814 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
815
816 def handle_UNKNOWN(self, cmd, params):
817 """ implement me in subclasses if you want to handle unknown events """
818 log.msg("Received unknown command (%s), params: %s" % (cmd, params))
819
820 ### callbacks
821
822 def gotBadLine(self, line, why):
823 """ called when a handler notifies me that this line is broken """
824 log.msg('Error in line: %s (%s)' % (line, why))
825
826 def gotError(self, errorCode):
827 """
828 called when the server sends an error which is not in
829 response to a sent command (ie. it has no matching transaction ID)
830 """
831 log.msg('Error %s' % (errorCodes[errorCode]))
832
833 class DispatchClient(MSNEventBase):
834 """
835 This class provides support for clients connecting to the dispatch server
836 @ivar userHandle: your user handle (passport) needed before connecting.
837 """
838
839 # eventually this may become an attribute of the
840 # factory.
841 userHandle = ""
842
843 def connectionMade(self):
844 MSNEventBase.connectionMade(self)
845 self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
846
847 ### protocol command handlers ( there is no need to override these )
848
849 def handle_VER(self, params):
850 versions = params[1:]
851 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
852 self.transport.loseConnection()
853 raise MSNProtocolError, "Invalid version response"
854 id = self._nextTransactionID()
855 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
856
857 def handle_CVR(self, params):
858 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
859
860 def handle_XFR(self, params):
861 if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
862 id, refType, addr = params[:3]
863 # was addr a host:port pair?
864 try:
865 host, port = addr.split(':')
866 except ValueError:
867 host = addr
868 port = MSN_PORT
869 if refType == "NS":
870 self.gotNotificationReferral(host, int(port))
871
872 ### callbacks
873
874 def gotNotificationReferral(self, host, port):
875 """
876 called when we get a referral to the notification server.
877
878 @param host: the notification server's hostname
879 @param port: the port to connect to
880 """
881 pass
882
883
884 class NotificationClient(MSNEventBase):
885 """
886 This class provides support for clients connecting
887 to the notification server.
888 """
889
890 factory = None # sssh pychecker
891
892 def __init__(self, currentID=0):
893 MSNEventBase.__init__(self)
894 self.currentID = currentID
895 self._state = ['DISCONNECTED', {}]
896 self.pingCounter = 0
897 self.pingCheckTask = None
898 self.msnobj = MSNObject()
899
900 def _setState(self, state):
901 self._state[0] = state
902
903 def _getState(self):
904 return self._state[0]
905
906 def _getStateData(self, key):
907 return self._state[1][key]
908
909 def _setStateData(self, key, value):
910 self._state[1][key] = value
911
912 def _remStateData(self, *args):
913 for key in args: del self._state[1][key]
914
915 def connectionMade(self):
916 MSNEventBase.connectionMade(self)
917 self._setState('CONNECTED')
918 self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
919
920 def connectionLost(self, reason):
921 self._setState('DISCONNECTED')
922 self._state[1] = {}
923 if self.pingCheckTask:
924 self.pingCheckTask.stop()
925 self.pingCheckTask = None
926 MSNEventBase.connectionLost(self, reason)
927
928 def _getEmailFields(self, message):
929 fields = message.getMessage().strip().split('\n')
930 values = {}
931 for i in fields:
932 a = i.split(':')
933 if len(a) != 2: continue
934 f, v = a
935 f = f.strip()
936 v = v.strip()
937 values[f] = v
938 return values
939
940 def _gotInitialEmailNotification(self, message):
941 values = self._getEmailFields(message)
942 try:
943 inboxunread = int(values["Inbox-Unread"])
944 foldersunread = int(values["Folders-Unread"])
945 except KeyError:
946 return
947 if foldersunread + inboxunread > 0: # For some reason MSN sends notifications about empty inboxes sometimes?
948 self.gotInitialEmailNotification(inboxunread, foldersunread)
949
950 def _gotEmailNotification(self, message):
951 values = self._getEmailFields(message)
952 try:
953 mailfrom = values["From"]
954 fromaddr = values["From-Addr"]
955 subject = values["Subject"]
956 junkbeginning = "=?\"us-ascii\"?Q?"
957 junkend = "?="
958 subject = subject.replace(junkbeginning, "").replace(junkend, "").replace("_", " ")
959 except KeyError:
960 # If any of the fields weren't found then it's not a big problem. We just ignore the message
961 return
962 self.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
963
964 def _gotMSNAlert(self, message):
965 notification = xmlw.parseText(message.message, beExtremelyLenient=True)
966 siteurl = notification.getAttribute("siteurl")
967 notid = notification.getAttribute("id")
968
969 msg = None
970 for e in notification.elements():
971 if e.name == "MSG":
972 msg = e
973 break
974 else: return
975
976 msgid = msg.getAttribute("id")
977
978 action = None
979 subscr = None
980 bodytext = None
981 for e in msg.elements():
982 if e.name == "ACTION":
983 action = e.getAttribute("url")
984 if e.name == "SUBSCR":
985 subscr = e.getAttribute("url")
986 if e.name == "BODY":
987 for e2 in e.elements():
988 if e2.name == "TEXT":
989 bodytext = e2.__str__()
990 if not (action and subscr and bodytext): return
991
992 actionurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (action, notid, msgid) # Used to have $siteurl// at the beginning, but it seems to not work with that now. Weird
993 subscrurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (subscr, notid, msgid)
994
995 self.gotMSNAlert(bodytext, actionurl, subscrurl)
996
997 def _gotUBX(self, message):
998 lm = message.message.lower()
999 p1 = lm.find("<psm>") + 5
1000 p2 = lm.find("</psm>")
1001 if p1 >= 0 and p2 >= 0:
1002 personal = unescapeFromXml(message.message[p1:p2])
1003 msnContact = self.factory.contacts.getContact(message.userHandle)
1004 if not msnContact: return
1005 msnContact.personal = personal
1006 self.contactPersonalChanged(message.userHandle, personal)
1007 else:
1008 self.contactPersonalChanged(message.userHandle, '')
1009
1010 def checkMessage(self, message):
1011 """ hook used for detecting specific notification messages """
1012 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
1013 if 'text/x-msmsgsprofile' in cTypes:
1014 self.gotProfile(message)
1015 return 0
1016 elif "text/x-msmsgsinitialemailnotification" in cTypes:
1017 self._gotInitialEmailNotification(message)
1018 return 0
1019 elif "text/x-msmsgsemailnotification" in cTypes:
1020 self._gotEmailNotification(message)
1021 return 0
1022 elif "NOTIFICATION" == message.userHandle and message.specialMessage == True:
1023 self._gotMSNAlert(message)
1024 return 0
1025 elif "UBX" == message.screenName and message.specialMessage == True:
1026 self._gotUBX(message)
1027 return 0
1028 return 1
1029
1030 ### protocol command handlers - no need to override these
1031
1032 def handle_VER(self, params):
1033 versions = params[1:]
1034 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
1035 self.transport.loseConnection()
1036 raise MSNProtocolError, "Invalid version response"
1037 self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
1038
1039 def handle_CVR(self, params):
1040 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
1041
1042 def handle_USR(self, params):
1043 if not (4 <= len(params) <= 6):
1044 raise MSNProtocolError, "Invalid Number of Parameters for USR"
1045
1046 mechanism = params[1]
1047 if mechanism == "OK":
1048 self.loggedIn(params[2], int(params[3]))
1049 elif params[2].upper() == "S":
1050 # we need to obtain auth from a passport server
1051 f = self.factory
1052 d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
1053 d.addCallback(self._passportLogin)
1054 d.addErrback(self._passportError)
1055
1056 def _passportLogin(self, result):
1057 if result[0] == LOGIN_REDIRECT:
1058 d = _login(self.factory.userHandle, self.factory.password,
1059 result[1], cached=1, authData=result[2])
1060 d.addCallback(self._passportLogin)
1061 d.addErrback(self._passportError)
1062 elif result[0] == LOGIN_SUCCESS:
1063 self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
1064 elif result[0] == LOGIN_FAILURE:
1065 self.loginFailure(result[1])
1066
1067 def _passportError(self, failure):
1068 self.loginFailure("Exception while authenticating: %s" % failure)
1069
1070 def handle_CHG(self, params):
1071 id = int(params[0])
1072 if not self._fireCallback(id, params[1]):
1073 if self.factory: self.factory.status = params[1]
1074 self.statusChanged(params[1])
1075
1076 def handle_ILN(self, params):
1077 #checkParamLen(len(params), 6, 'ILN')
1078 msnContact = self.factory.contacts.getContact(params[2])
1079 if not msnContact: return
1080 msnContact.status = params[1]
1081 msnContact.screenName = unquote(params[3])
1082 if len(params) > 4: msnContact.caps = int(params[4])
1083 if len(params) > 5:
1084 self.handleAvatarHelper(msnContact, params[5])
1085 else:
1086 self.handleAvatarGoneHelper(msnContact)
1087 self.gotContactStatus(params[1], params[2], unquote(params[3]))
1088
1089 def handleAvatarGoneHelper(self, msnContact):
1090 if msnContact.msnobj:
1091 msnContact.msnobj = None
1092 msnContact.msnobjGot = True
1093 self.contactAvatarChanged(msnContact.userHandle, "")
1094
1095 def handleAvatarHelper(self, msnContact, msnobjStr):
1096 msnobj = MSNObject(unquote(msnobjStr))
1097 if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
1098 if MSNP2PDEBUG: log.msg("Updated MSNObject received!" + msnobjStr)
1099 msnContact.msnobj = msnobj
1100 msnContact.msnobjGot = False
1101 self.contactAvatarChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
1102
1103 def handle_CHL(self, params):
1104 checkParamLen(len(params), 2, 'CHL')
1105 response = msnp11chl.doChallenge(params[1])
1106 self.sendLine("QRY %s %s %s" % (self._nextTransactionID(), msnp11chl.MSNP11_PRODUCT_ID, len(response)))
1107 self.transport.write(response)
1108
1109 def handle_QRY(self, params):
1110 pass
1111
1112 def handle_NLN(self, params):
1113 if not self.factory: return
1114 msnContact = self.factory.contacts.getContact(params[1])
1115 if not msnContact: return
1116 msnContact.status = params[0]
1117 msnContact.screenName = unquote(params[2])
1118 if len(params) > 3: msnContact.caps = int(params[3])
1119 if len(params) > 4:
1120 self.handleAvatarHelper(msnContact, params[4])
1121 else:
1122 self.handleAvatarGoneHelper(msnContact)
1123 self.contactStatusChanged(params[0], params[1], unquote(params[2]))
1124
1125 def handle_FLN(self, params):
1126 checkParamLen(len(params), 1, 'FLN')
1127 msnContact = self.factory.contacts.getContact(params[0])
1128 if msnContact:
1129 msnContact.status = STATUS_OFFLINE
1130 self.contactOffline(params[0])
1131
1132 def handle_LST(self, params):
1133 if self._getState() != 'SYNC': return
1134
1135 userHandle, screenName, userGuid, lists, groups = getVals(params)
1136
1137 if not userHandle or lists < 1:
1138 raise MSNProtocolError, "Unknown LST " + str(params) # debug
1139 contact = MSNContact(userGuid, userHandle, screenName, lists)
1140 if contact.lists & FORWARD_LIST:
1141 contact.groups.extend(map(str, groups))
1142 self._getStateData('list').addContact(contact)
1143 self._setStateData('last_contact', contact)
1144 sofar = self._getStateData('lst_sofar') + 1
1145 if sofar == self._getStateData('lst_reply'):
1146 # this is the best place to determine that
1147 # a syn realy has finished - msn _may_ send
1148 # BPR information for the last contact
1149 # which is unfortunate because it means
1150 # that the real end of a syn is non-deterministic.
1151 # to handle this we'll keep 'last_contact' hanging
1152 # around in the state data and update it if we need
1153 # to later.
1154 self._setState('SESSION')
1155 contacts = self._getStateData('list')
1156 phone = self._getStateData('phone')
1157 id = self._getStateData('synid')
1158 self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1159 self._fireCallback(id, contacts, phone)
1160 else:
1161 self._setStateData('lst_sofar',sofar)
1162
1163 def handle_BLP(self, params):
1164 # check to see if this is in response to a SYN
1165 if self._getState() == 'SYNC':
1166 self._getStateData('list').privacy = listCodeToID[params[0].lower()]
1167 else:
1168 id = int(params[0])
1169 self.factory.contacts.privacy = listCodeToID[params[1].lower()]
1170 self._fireCallback(id, params[1])
1171
1172 def handle_GTC(self, params):
1173 # check to see if this is in response to a SYN
1174 if self._getState() == 'SYNC':
1175 if params[0].lower() == "a": self._getStateData('list').autoAdd = 0
1176 elif params[0].lower() == "n": self._getStateData('list').autoAdd = 1
1177 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1178 else:
1179 id = int(params[0])
1180 if params[1].lower() == "a": self._fireCallback(id, 0)
1181 elif params[1].lower() == "n": self._fireCallback(id, 1)
1182 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1183
1184 def handle_SYN(self, params):
1185 id = int(params[0])
1186 self._setStateData('phone', []) # Always needs to be set
1187 if params[3] == 0: # No LST will be received. New account?
1188 self._setState('SESSION')
1189 self._fireCallback(id, None, None)
1190 else:
1191 contacts = MSNContactList()
1192 self._setStateData('list', contacts)
1193 self._setStateData('lst_reply', int(params[3]))
1194 self._setStateData('lsg_reply', int(params[4]))
1195 self._setStateData('lst_sofar', 0)
1196
1197 def handle_LSG(self, params):
1198 if self._getState() == 'SYNC':
1199 self._getStateData('list').groups[params[1]] = unquote(params[0])
1200
1201 def handle_PRP(self, params):
1202 if params[1] == "MFN":
1203 self._fireCallback(int(params[0]), unquote(params[2]))
1204 elif self._getState() == 'SYNC':
1205 self._getStateData('phone').append((params[0], unquote(params[1])))
1206 else:
1207 self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
1208
1209 def handle_BPR(self, params):
1210 if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
1211 numParams = len(params)
1212 if numParams == 2: # part of a syn
1213 self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
1214 elif numParams == 4:
1215 self.factory.contacts.version = int(params[0])
1216 userHandle, phoneType, number = params[1], params[2], unquote(params[3])
1217 self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1218 self.gotPhoneNumber(userHandle, phoneType, number)
1219
1220
1221 def handle_ADG(self, params):
1222 checkParamLen(len(params), 5, 'ADG')
1223 id = int(params[0])
1224 if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
1225 raise MSNProtocolError, "ADG response does not match up to a request" # debug
1226
1227 def handle_RMG(self, params):
1228 checkParamLen(len(params), 3, 'RMG')
1229 id = int(params[0])
1230 if not self._fireCallback(id, int(params[1]), int(params[2])):
1231 raise MSNProtocolError, "RMG response does not match up to a request" # debug
1232
1233 def handle_REG(self, params):
1234 checkParamLen(len(params), 5, 'REG')
1235 id = int(params[0])
1236 if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
1237 raise MSNProtocolError, "REG response does not match up to a request" # debug
1238
1239 def handle_ADC(self, params):
1240 if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list"
1241 numParams = len(params)
1242 if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
1243 raise MSNProtocolError, "Invalid Paramaters for ADC" # debug
1244 id = int(params[0])
1245 listType = params[1].lower()
1246 userHandle, screenName, userGuid, ignored1, groups = getVals(params[2:])
1247
1248 if groups and listType.upper() != FORWARD_LIST:
1249 raise MSNProtocolError, "Only forward list can contain groups" # debug
1250
1251 if not self._fireCallback(id, listCodeToID[listType], userGuid, userHandle, screenName):
1252 c = self.factory.contacts.getContact(userHandle)
1253 if not c:
1254 c = MSNContact(userGuid=userGuid, userHandle=userHandle, screenName=screenName)
1255 self.factory.contacts.addContact(c)
1256 c.addToList(PENDING_LIST)
1257 self.userAddedMe(userGuid, userHandle, screenName)
1258
1259 def handle_REM(self, params):
1260 if not self.factory.contacts: raise MSNProtocolError, "handle_REM called with no contact list available!"
1261 numParams = len(params)
1262 if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL'):
1263 raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1264 id = int(params[0])
1265 listType = params[1].lower()
1266 userHandle = params[2]
1267 groupID = None
1268 if numParams == 4:
1269 if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
1270 groupID = int(params[3])
1271 if not self._fireCallback(id, listCodeToID[listType], userHandle, groupID):
1272 if listType.upper() != "RL": return
1273 c = self.factory.contacts.getContact(userHandle)
1274 if not c: return
1275 c.removeFromList(REVERSE_LIST)
1276 if c.lists == 0: self.factory.contacts.remContact(c.userHandle)
1277 self.userRemovedMe(userHandle)
1278
1279 def handle_XFR(self, params):
1280 checkParamLen(len(params), 5, 'XFR')
1281 id = int(params[0])
1282 # check to see if they sent a host/port pair
1283 try:
1284 host, port = params[2].split(':')
1285 except ValueError:
1286 host = params[2]
1287 port = MSN_PORT
1288
1289 if not self._fireCallback(id, host, int(port), params[4]):
1290 raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1291
1292 def handle_RNG(self, params):
1293 checkParamLen(len(params), 6, 'RNG')
1294 # check for host:port pair
1295 try:
1296 host, port = params[1].split(":")
1297 port = int(port)
1298 except ValueError:
1299 host = params[1]
1300 port = MSN_PORT
1301 self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1302 unquote(params[5]))
1303
1304 def handle_NOT(self, params):
1305 checkParamLen(len(params), 1, 'NOT')
1306 try:
1307 messageLen = int(params[0])
1308 except ValueError: raise MSNProtocolError, "Invalid Parameter for NOT length argument"
1309 self.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", specialMessage=True)
1310 self.setRawMode()
1311
1312 def handle_UBX(self, params):
1313 checkParamLen(len(params), 2, 'UBX')
1314 try:
1315 messageLen = int(params[1])
1316 except ValueError: raise MSNProtocolError, "Invalid Parameter for UBX length argument"
1317 if messageLen > 0:
1318 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
1319 self.setRawMode()
1320 else:
1321 self.contactPersonalChanged(params[0], '')
1322
1323 def handle_UUX(self, params):
1324 checkParamLen(len(params), 2, 'UUX')
1325 if params[1] != '0': return
1326 id = int(params[0])
1327 self._fireCallback(id)
1328
1329 def handle_OUT(self, params):
1330 checkParamLen(len(params), 1, 'OUT')
1331 if params[0] == "OTH": self.multipleLogin()
1332 elif params[0] == "SSD": self.serverGoingDown()
1333 else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1334
1335 def handle_QNG(self, params):
1336 self.pingCounter = 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1337
1338 # callbacks
1339
1340 def pingChecker(self):
1341 if self.pingCounter > 5:
1342 # The server has ignored 5 pings, lets kill the connection
1343 self.transport.loseConnection()
1344 else:
1345 self.sendLine("PNG")
1346 self.pingCounter += 1
1347
1348 def pingCheckerStart(self, *args):
1349 self.pingCheckTask = task.LoopingCall(self.pingChecker)
1350 self.pingCheckTask.start(PINGSPEED)
1351
1352 def loggedIn(self, userHandle, verified):
1353 """
1354 Called when the client has logged in.
1355 The default behaviour of this method is to
1356 update the factory with our screenName and
1357 to sync the contact list (factory.contacts).
1358 When this is complete self.listSynchronized
1359 will be called.
1360
1361 @param userHandle: our userHandle
1362 @param verified: 1 if our passport has been (verified), 0 if not.
1363 (i'm not sure of the significace of this)
1364 @type verified: int
1365 """
1366 d = self.syncList()
1367 d.addCallback(self.listSynchronized)
1368 d.addCallback(self.pingCheckerStart)
1369
1370 def loginFailure(self, message):
1371 """
1372 Called when the client fails to login.
1373
1374 @param message: a message indicating the problem that was encountered
1375 """
1376 pass
1377
1378 def gotProfile(self, message):
1379 """
1380 Called after logging in when the server sends an initial
1381 message with MSN/passport specific profile information
1382 such as country, number of kids, etc.
1383 Check the message headers for the specific values.
1384
1385 @param message: The profile message
1386 """
1387 pass
1388
1389 def listSynchronized(self, *args):
1390 """
1391 Lists are now synchronized by default upon logging in, this
1392 method is called after the synchronization has finished
1393 and the factory now has the up-to-date contacts.
1394 """
1395 pass
1396
1397 def contactAvatarChanged(self, userHandle, hash):
1398 """
1399 Called when we receive the first, or a new <msnobj/> from a
1400 contact.
1401
1402 @param userHandle: contact who's msnobj has been changed
1403 @param hash: sha1 hash of their avatar
1404 """
1405
1406 def statusChanged(self, statusCode):
1407 """
1408 Called when our status changes and its not in response to a
1409 client command.
1410
1411 @param statusCode: 3-letter status code
1412 """
1413 pass
1414
1415 def gotContactStatus(self, statusCode, userHandle, screenName):
1416 """
1417 Called when we receive a list of statuses upon login.
1418
1419 @param statusCode: 3-letter status code
1420 @param userHandle: the contact's user handle (passport)
1421 @param screenName: the contact's screen name
1422 """
1423 pass
1424
1425 def contactStatusChanged(self, statusCode, userHandle, screenName):
1426 """
1427 Called when we're notified that a contact's status has changed.
1428
1429 @param statusCode: 3-letter status code
1430 @param userHandle: the contact's user handle (passport)
1431 @param screenName: the contact's screen name
1432 """
1433 pass
1434
1435 def contactPersonalChanged(self, userHandle, personal):
1436 """
1437 Called when a contact's personal message changes.
1438
1439 @param userHandle: the contact who changed their personal message
1440 @param personal : the new personal message
1441 """
1442 pass
1443
1444 def contactOffline(self, userHandle):
1445 """
1446 Called when a contact goes offline.
1447
1448 @param userHandle: the contact's user handle
1449 """
1450 pass
1451
1452 def gotMessage(self, message):
1453 """
1454 Called when there is a message from the notification server
1455 that is not understood by default.
1456
1457 @param message: the MSNMessage.
1458 """
1459 pass
1460
1461 def gotMSNAlert(self, body, action, subscr):
1462 """
1463 Called when the server sends an MSN Alert (http://alerts.msn.com)
1464
1465 @param body : the alert text
1466 @param action: a URL with more information for the user to view
1467 @param subscr: a URL the user can use to modify their alert subscription
1468 """
1469 pass
1470
1471 def gotInitialEmailNotification(self, inboxunread, foldersunread):
1472 """
1473 Called when the server sends you details about your hotmail
1474 inbox. This is only ever called once, on login.
1475
1476 @param inboxunread : the number of unread items in your inbox
1477 @param foldersunread: the number of unread items in other folders
1478 """
1479 pass
1480
1481 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
1482 """
1483 Called when the server sends us realtime email
1484 notification. This means that you have received
1485 a new email in your hotmail inbox.
1486
1487 @param mailfrom: the sender of the email
1488 @param fromaddr: the sender of the email (I don't know :P)
1489 @param subject : the email subject
1490 """
1491 pass
1492
1493 def gotPhoneNumber(self, userHandle, phoneType, number):
1494 """
1495 Called when the server sends us phone details about
1496 a specific user (for example after a user is added
1497 the server will send their status, phone details etc.
1498
1499 @param userHandle: the contact's user handle (passport)
1500 @param phoneType: the specific phoneType
1501 (*_PHONE constants or HAS_PAGER)
1502 @param number: the value/phone number.
1503 """
1504 pass
1505
1506 def userAddedMe(self, userGuid, userHandle, screenName):
1507 """
1508 Called when a user adds me to their list. (ie. they have been added to
1509 the reverse list.
1510
1511 @param userHandle: the userHandle of the user
1512 @param screenName: the screen name of the user
1513 """
1514 pass
1515
1516 def userRemovedMe(self, userHandle):
1517 """
1518 Called when a user removes us from their contact list
1519 (they are no longer on our reverseContacts list.
1520
1521 @param userHandle: the contact's user handle (passport)
1522 """
1523 pass
1524
1525 def gotSwitchboardInvitation(self, sessionID, host, port,
1526 key, userHandle, screenName):
1527 """
1528 Called when we get an invitation to a switchboard server.
1529 This happens when a user requests a chat session with us.
1530
1531 @param sessionID: session ID number, must be remembered for logging in
1532 @param host: the hostname of the switchboard server
1533 @param port: the port to connect to
1534 @param key: used for authorization when connecting
1535 @param userHandle: the user handle of the person who invited us
1536 @param screenName: the screen name of the person who invited us
1537 """
1538 pass
1539
1540 def multipleLogin(self):
1541 """
1542 Called when the server says there has been another login
1543 under our account, the server should disconnect us right away.
1544 """
1545 pass
1546
1547 def serverGoingDown(self):
1548 """
1549 Called when the server has notified us that it is going down for
1550 maintenance.
1551 """
1552 pass
1553
1554 # api calls
1555
1556 def changeStatus(self, status):
1557 """
1558 Change my current status. This method will add
1559 a default callback to the returned Deferred
1560 which will update the status attribute of the
1561 factory.
1562
1563 @param status: 3-letter status code (as defined by
1564 the STATUS_* constants)
1565 @return: A Deferred, the callback of which will be
1566 fired when the server confirms the change
1567 of status. The callback argument will be
1568 a tuple with the new status code as the
1569 only element.
1570 """
1571
1572 id, d = self._createIDMapping()
1573 self.sendLine("CHG %s %s %s %s" % (id, status, str(MSNContact.MSNC1 | MSNContact.MSNC2 | MSNContact.MSNC3 | MSNContact.MSNC4), quote(self.msnobj.text)))
1574 def _cb(r):
1575 self.factory.status = r[0]
1576 return r
1577 return d.addCallback(_cb)
1578
1579 def setPrivacyMode(self, privLevel):
1580 """
1581 Set my privacy mode on the server.
1582
1583 B{Note}:
1584 This only keeps the current privacy setting on
1585 the server for later retrieval, it does not
1586 effect the way the server works at all.
1587
1588 @param privLevel: This parameter can be true, in which
1589 case the server will keep the state as
1590 'al' which the official client interprets
1591 as -> allow messages from only users on
1592 the allow list. Alternatively it can be
1593 false, in which case the server will keep
1594 the state as 'bl' which the official client
1595 interprets as -> allow messages from all
1596 users except those on the block list.
1597
1598 @return: A Deferred, the callback of which will be fired when
1599 the server replies with the new privacy setting.
1600 The callback argument will be a tuple, the only element
1601 of which being either 'al' or 'bl' (the new privacy setting).
1602 """
1603
1604 id, d = self._createIDMapping()
1605 if privLevel: self.sendLine("BLP %s AL" % id)
1606 else: self.sendLine("BLP %s BL" % id)
1607 return d
1608
1609 def syncList(self):
1610 """
1611 Used for keeping an up-to-date contact list.
1612 A callback is added to the returned Deferred
1613 that updates the contact list on the factory
1614 and also sets my state to STATUS_ONLINE.
1615
1616 B{Note}:
1617 This is called automatically upon signing
1618 in using the version attribute of
1619 factory.contacts, so you may want to persist
1620 this object accordingly. Because of this there
1621 is no real need to ever call this method
1622 directly.
1623
1624 @return: A Deferred, the callback of which will be
1625 fired when the server sends an adequate reply.
1626 The callback argument will be a tuple with two
1627 elements, the new list (MSNContactList) and
1628 your current state (a dictionary). If the version
1629 you sent _was_ the latest list version, both elements
1630 will be None. To just request the list send a version of 0.
1631 """
1632
1633 self._setState('SYNC')
1634 id, d = self._createIDMapping(data=None)
1635 self._setStateData('synid',id)
1636 self.sendLine("SYN %s %s %s" % (id, 0, 0))
1637 def _cb(r):
1638 self.changeStatus(STATUS_ONLINE)
1639 if r[0] is not None:
1640 self.factory.contacts = r[0]
1641 return r
1642 return d.addCallback(_cb)
1643
1644 def setPhoneDetails(self, phoneType, value):
1645 """
1646 Set/change my phone numbers stored on the server.
1647
1648 @param phoneType: phoneType can be one of the following
1649 constants - HOME_PHONE, WORK_PHONE,
1650 MOBILE_PHONE, HAS_PAGER.
1651 These are pretty self-explanatory, except
1652 maybe HAS_PAGER which refers to whether or
1653 not you have a pager.
1654 @param value: for all of the *_PHONE constants the value is a
1655 phone number (str), for HAS_PAGER accepted values
1656 are 'Y' (for yes) and 'N' (for no).
1657
1658 @return: A Deferred, the callback for which will be fired when
1659 the server confirms the change has been made. The
1660 callback argument will be a tuple with 2 elements, the
1661 first being the new list version (int) and the second
1662 being the new phone number value (str).
1663 """
1664 raise "ProbablyDoesntWork"
1665 # XXX: Add a default callback which updates
1666 # factory.contacts.version and the relevant phone
1667 # number
1668 id, d = self._createIDMapping()
1669 self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1670 return d
1671
1672 def addListGroup(self, name):
1673 """
1674 Used to create a new list group.
1675 A default callback is added to the
1676 returned Deferred which updates the
1677 contacts attribute of the factory.
1678
1679 @param name: The desired name of the new group.
1680
1681 @return: A Deferred, the callbacck for which will be called
1682 when the server clarifies that the new group has been
1683 created. The callback argument will be a tuple with 3
1684 elements: the new list version (int), the new group name
1685 (str) and the new group ID (int).
1686 """
1687
1688 raise "ProbablyDoesntWork"
1689 id, d = self._createIDMapping()
1690 self.sendLine("ADG %s %s 0" % (id, quote(name)))
1691 def _cb(r):
1692 if self.factory.contacts:
1693 self.factory.contacts.version = r[0]
1694 self.factory.contacts.setGroup(r[1], r[2])
1695 return r
1696 return d.addCallback(_cb)
1697
1698 def remListGroup(self, groupID):
1699 """
1700 Used to remove a list group.
1701 A default callback is added to the
1702 returned Deferred which updates the
1703 contacts attribute of the factory.
1704
1705 @param groupID: the ID of the desired group to be removed.
1706
1707 @return: A Deferred, the callback for which will be called when
1708 the server clarifies the deletion of the group.
1709 The callback argument will be a tuple with 2 elements:
1710 the new list version (int) and the group ID (int) of
1711 the removed group.
1712 """
1713
1714 raise "ProbablyDoesntWork"
1715 id, d = self._createIDMapping()
1716 self.sendLine("RMG %s %s" % (id, groupID))
1717 def _cb(r):
1718 self.factory.contacts.version = r[0]
1719 self.factory.contacts.remGroup(r[1])
1720 return r
1721 return d.addCallback(_cb)
1722
1723 def renameListGroup(self, groupID, newName):
1724 """
1725 Used to rename an existing list group.
1726 A default callback is added to the returned
1727 Deferred which updates the contacts attribute
1728 of the factory.
1729
1730 @param groupID: the ID of the desired group to rename.
1731 @param newName: the desired new name for the group.
1732
1733 @return: A Deferred, the callback for which will be called
1734 when the server clarifies the renaming.
1735 The callback argument will be a tuple of 3 elements,
1736 the new list version (int), the group id (int) and
1737 the new group name (str).
1738 """
1739
1740 raise "ProbablyDoesntWork"
1741 id, d = self._createIDMapping()
1742 self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1743 def _cb(r):
1744 self.factory.contacts.version = r[0]
1745 self.factory.contacts.setGroup(r[1], r[2])
1746 return r
1747 return d.addCallback(_cb)
1748
1749 def addContact(self, listType, userHandle):
1750 """
1751 Used to add a contact to the desired list.
1752 A default callback is added to the returned
1753 Deferred which updates the contacts attribute of
1754 the factory with the new contact information.
1755 If you are adding a contact to the forward list
1756 and you want to associate this contact with multiple
1757 groups then you will need to call this method for each
1758 group you would like to add them to, changing the groupID
1759 parameter. The default callback will take care of updating
1760 the group information on the factory's contact list.
1761
1762 @param listType: (as defined by the *_LIST constants)
1763 @param userHandle: the user handle (passport) of the contact
1764 that is being added
1765
1766 @return: A Deferred, the callback for which will be called when
1767 the server has clarified that the user has been added.
1768 The callback argument will be a tuple with 4 elements:
1769 the list type, the contact's user handle, the new list
1770 version, and the group id (if relevant, otherwise it
1771 will be None)
1772 """
1773
1774 id, d = self._createIDMapping()
1775 try: # Make sure the contact isn't actually on the list
1776 if self.factory.contacts.getContact(userHandle).lists & listType: return
1777 except AttributeError: pass
1778 listType = listIDToCode[listType].upper()
1779 if listType == "FL":
1780 self.sendLine("ADC %s %s N=%s F=%s" % (id, listType, userHandle, userHandle))
1781 else:
1782 self.sendLine("ADC %s %s N=%s" % (id, listType, userHandle))
1783
1784 def _cb(r):
1785 if not self.factory: return
1786 c = self.factory.contacts.getContact(r[2])
1787 if not c:
1788 c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
1789 #if r[3]: c.groups.append(r[3])
1790 c.addToList(r[0])
1791 return r
1792 return d.addCallback(_cb)
1793
1794 def remContact(self, listType, userHandle):
1795 """
1796 Used to remove a contact from the desired list.
1797 A default callback is added to the returned deferred
1798 which updates the contacts attribute of the factory
1799 to reflect the new contact information.
1800
1801 @param listType: (as defined by the *_LIST constants)
1802 @param userHandle: the user handle (passport) of the
1803 contact being removed
1804
1805 @return: A Deferred, the callback for which will be called when
1806 the server has clarified that the user has been removed.
1807 The callback argument will be a tuple of 3 elements:
1808 the list type, the contact's user handle and the group ID
1809 (if relevant, otherwise it will be None)
1810 """
1811
1812 id, d = self._createIDMapping()
1813 try: # Make sure the contact is actually on this list
1814 if not (self.factory.contacts.getContact(userHandle).lists & listType): return
1815 except AttributeError: return
1816 listType = listIDToCode[listType].upper()
1817 if listType == "FL":
1818 try:
1819 c = self.factory.contacts.getContact(userHandle)
1820 userGuid = c.userGuid
1821 except AttributeError: return
1822 self.sendLine("REM %s FL %s" % (id, userGuid))
1823 else:
1824 self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1825
1826 def _cb(r):
1827 if listType == "FL":
1828 r = (r[0], userHandle, r[2]) # make sure we always get a userHandle
1829 l = self.factory.contacts
1830 c = l.getContact(r[1])
1831 if not c: return
1832 group = r[2]
1833 shouldRemove = 1
1834 if group: # they may not have been removed from the list
1835 c.groups.remove(group)
1836 if c.groups: shouldRemove = 0
1837 if shouldRemove:
1838 c.removeFromList(r[0])
1839 if c.lists == 0: l.remContact(c.userHandle)
1840 return r
1841 return d.addCallback(_cb)
1842
1843 def changeScreenName(self, newName):
1844 """
1845 Used to change your current screen name.
1846 A default callback is added to the returned
1847 Deferred which updates the screenName attribute
1848 of the factory and also updates the contact list
1849 version.
1850
1851 @param newName: the new screen name
1852
1853 @return: A Deferred, the callback for which will be called
1854 when the server acknowledges the change.
1855 The callback argument will be a tuple of 1 element,
1856 the new screen name.
1857 """
1858
1859 id, d = self._createIDMapping()
1860 self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
1861 def _cb(r):
1862 self.factory.screenName = r[0]
1863 return r
1864 return d.addCallback(_cb)
1865
1866 def changePersonalMessage(self, personal):
1867 """
1868 Used to change your personal message.
1869
1870 @param personal: the new screen name
1871
1872 @return: A Deferred, the callback for which will be called
1873 when the server acknowledges the change.
1874 The callback argument will be a tuple of 1 element,
1875 the personal message.
1876 """
1877
1878 id, d = self._createIDMapping()
1879 data = ""
1880 if personal:
1881 data = "<Data><PSM>" + personal + "</PSM><CurrentMedia></CurrentMedia></Data>"
1882 self.sendLine("UUX %s %s" % (id, len(data)))
1883 self.transport.write(data)
1884 def _cb(r):
1885 self.factory.personal = personal
1886 return (personal,)
1887 return d.addCallback(_cb)
1888
1889 def changeAvatar(self, imageData, push):
1890 """
1891 Used to change the avatar that other users see.
1892
1893 @param imageData: the PNG image data to set as the avatar
1894 @param push : whether to push the update to the server
1895 (it will otherwise be sent with the next
1896 changeStatus())
1897
1898 @return: If push==True, a Deferred, the callback for which
1899 will be called when the server acknowledges the change.
1900 The callback argument will be the same as for changeStatus.
1901 """
1902
1903 if self.msnobj and imageData == self.msnobj.imageData: return
1904 if imageData:
1905 self.msnobj.setData(self.factory.userHandle, imageData)
1906 else:
1907 self.msnobj.setNull()
1908 if push: return self.changeStatus(self.factory.status) # Push to server
1909
1910
1911 def requestSwitchboardServer(self):
1912 """
1913 Used to request a switchboard server to use for conversations.
1914
1915 @return: A Deferred, the callback for which will be called when
1916 the server responds with the switchboard information.
1917 The callback argument will be a tuple with 3 elements:
1918 the host of the switchboard server, the port and a key
1919 used for logging in.
1920 """
1921
1922 id, d = self._createIDMapping()
1923 self.sendLine("XFR %s SB" % id)
1924 return d
1925
1926 def logOut(self):
1927 """
1928 Used to log out of the notification server.
1929 After running the method the server is expected
1930 to close the connection.
1931 """
1932
1933 if self.pingCheckTask:
1934 self.pingCheckTask.stop()
1935 self.pingCheckTask = None
1936 self.sendLine("OUT")
1937
1938 class NotificationFactory(ClientFactory):
1939 """
1940 Factory for the NotificationClient protocol.
1941 This is basically responsible for keeping
1942 the state of the client and thus should be used
1943 in a 1:1 situation with clients.
1944
1945 @ivar contacts: An MSNContactList instance reflecting
1946 the current contact list -- this is
1947 generally kept up to date by the default
1948 command handlers.
1949 @ivar userHandle: The client's userHandle, this is expected
1950 to be set by the client and is used by the
1951 protocol (for logging in etc).
1952 @ivar screenName: The client's current screen-name -- this is
1953 generally kept up to date by the default
1954 command handlers.
1955 @ivar password: The client's password -- this is (obviously)
1956 expected to be set by the client.
1957 @ivar passportServer: This must point to an msn passport server
1958 (the whole URL is required)
1959 @ivar status: The status of the client -- this is generally kept
1960 up to date by the default command handlers
1961 """
1962
1963 contacts = None
1964 userHandle = ''
1965 screenName = ''
1966 password = ''
1967 passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1968 status = 'FLN'
1969 protocol = NotificationClient
1970
1971
1972 class SwitchboardClient(MSNEventBase):
1973 """
1974 This class provides support for clients connecting to a switchboard server.
1975
1976 Switchboard servers are used for conversations with other people
1977 on the MSN network. This means that the number of conversations at
1978 any given time will be directly proportional to the number of
1979 connections to varioius switchboard servers.
1980
1981 MSN makes no distinction between single and group conversations,
1982 so any number of users may be invited to join a specific conversation
1983 taking place on a switchboard server.
1984
1985 @ivar key: authorization key, obtained when receiving
1986 invitation / requesting switchboard server.
1987 @ivar userHandle: your user handle (passport)
1988 @ivar sessionID: unique session ID, used if you are replying
1989 to a switchboard invitation
1990 @ivar reply: set this to 1 in connectionMade or before to signifiy
1991 that you are replying to a switchboard invitation.
1992 @ivar msnobj: the MSNObject for the user. So that the switchboard can distribute it.
1993 """
1994
1995 key = 0
1996 userHandle = ""
1997 sessionID = ""
1998 reply = 0
1999 msnobj = None
2000
2001 _iCookie = 0
2002
2003 def __init__(self):
2004 MSNEventBase.__init__(self)
2005 self.pendingUsers = {}
2006 self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
2007 self.slpLinks = {}
2008
2009 def connectionMade(self):
2010 MSNEventBase.connectionMade(self)
2011 self._sendInit()
2012
2013 def connectionLost(self, reason):
2014 self.cookies['iCookies'] = {}
2015 MSNEventBase.connectionLost(self, reason)
2016
2017 def _sendInit(self):
2018 """
2019 send initial data based on whether we are replying to an invitation
2020 or starting one.
2021 """
2022 id = self._nextTransactionID()
2023 if not self.reply:
2024 self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
2025 else:
2026 self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
2027
2028 def _newInvitationCookie(self):
2029 self._iCookie += 1
2030 if self._iCookie > 1000: self._iCookie = 1
2031 return self._iCookie
2032
2033 def _checkTyping(self, message, cTypes):
2034 """ helper method for checkMessage """
2035 if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
2036 self.userTyping(message)
2037 return 1
2038
2039 def _checkFileInvitation(self, message, info):
2040 """ helper method for checkMessage """
2041 if not info.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID: return 0
2042 try:
2043 cookie = info['Invitation-Cookie']
2044 filename = info['Application-File']
2045 filesize = int(info['Application-FileSize'])
2046 connectivity = (info.get('Connectivity').lower() == 'y')
2047 except KeyError:
2048 log.msg('Received munged file transfer request ... ignoring.')
2049 return 0
2050 import msnft
2051 self.gotSendRequest(msnft.MSNFTP_Receive(filename, filesize, message.userHandle, cookie, connectivity, self))
2052 return 1
2053
2054 def _handleP2PMessage(self, message):
2055 """ helper method for msnslp messages (file transfer & avatars) """
2056 packet = message.message
2057 binaryFields = BinaryFields(packet=packet)
2058 if binaryFields[0] != 0:
2059 slpLink = self.slpLinks[binaryFields[0]]
2060 if slpLink.remoteUser == message.userHandle:
2061 slpLink.handlePacket(packet)
2062 elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
2063 pass # Ignore the ACKs
2064 else:
2065 slpMessage = MSNSLPMessage(packet)
2066 slpLink = None
2067 if slpMessage.method == "INVITE":
2068 if slpMessage.euf_guid == MSN_MSNFTP_GUID:
2069 slpLink = SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
2070 context = FileContext(slpMessage.context)
2071 fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro)
2072 elif slpMessage.euf_guid == MSN_AVATAR_GUID:
2073 # Check that we have an avatar to send
2074 if self.msnobj:
2075 slpLink = SLPLink_Send(slpMessage.fro, self, self.msnobj.size, slpMessage.sessionID, slpMessage.sessionGuid)
2076 self._sendMSNSLPResponse(slpLink, "200 OK")
2077 slpLink.write(self.msnobj.imageData)
2078 slpLink.finish()
2079 else:
2080 pass # FIXME, should send an error response
2081 if slpLink:
2082 self.slpLinks[slpMessage.sessionID] = slpLink
2083 else:
2084 if slpMessage.status != "200":
2085 for slpLink in self.slpLinks:
2086 if slpLink.sessionGuid == slpMessage.sessionGuid:
2087 del self.slpLinks[slpLink.sessionID]
2088 if slpMessage.method != "BYE":
2089 # Must be an error. If its a file transfer we need to signal that it failed
2090 slpLink.transferError()
2091 else:
2092 slpLink = self.slpLinks[slpMessage.sessionID]
2093 slpLink.transferReady()
2094 if slpLink:
2095 # Always need to ACK these packets if we can
2096 self._sendP2PACK(slpLink, binaryFields)
2097
2098
2099 def _sendP2PACK(self, slpLink, ackHeaders):
2100 binaryFields = BinaryFields()
2101 binaryFields[1] = slpLink.seqID.next()
2102 binaryFields[3] = ackHeaders[3]
2103 binaryFields[5] = BinaryFields.ACK
2104 binaryFields[6] = ackHeaders[1]
2105 binaryFields[7] = ackHeaders[6]
2106 binaryFields[8] = ackHeaders[3]
2107 self._sendP2PMessage(binaryFields, "", slpLink.remoteUser)
2108
2109 def _sendMSNSLPCommand(self, command, slpLink, guid, context):
2110 msg = MSNSLPMessage()
2111 msg.create(method=command, to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
2112 msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=b64enc(context + chr(0)))
2113 self._sendMSNSLPMessage(slpLink, msg)
2114
2115 def _sendMSNSLPResponse(self, slpLink, response):
2116 msg = MSNSLPMessage()
2117 msg.create(status=response, to=slpLink.remoteUser, fro=self.userHandle, cseq=1, sessionGuid=slpLink.sessionGuid)
2118 msg.setData(sessionID=slpLink.sessionID)
2119 self._sendMSNSLPMessage(slpLink, msg)
2120
2121 def _sendMSNSLPMessage(self, slpLink, msnSLPMessage):
2122 msgStr = str(msnSLPMessage)
2123 binaryFields = BinaryFields()
2124 binaryFields[1] = slpLink.seqID.next()
2125 binaryFields[3] = len(msgStr)
2126 binaryFields[4] = binaryFields[3]
2127 binaryFields[6] = random.randint(0, sys.maxint)
2128 self._sendP2PMessage(binaryFields, msgStr, msnSLPMessage.to)
2129
2130 def _sendP2PMessage(self, binaryFields, msgStr, to):
2131 packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
2132
2133 message = MSNMessage(message=packet)
2134 message.setHeader("Content-Type", "application/x-msnmsgrp2p")
2135 message.setHeader("P2P-Dest", to)
2136 message.ack = MSNMessage.MESSAGE_ACK_FAT
2137 self.sendMessage(message)
2138
2139
2140
2141 def checkMessage(self, message):
2142 """
2143 hook for detecting any notification type messages
2144 (e.g. file transfer)
2145 """
2146 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
2147 if self._checkTyping(message, cTypes): return 0
2148 if 'text/x-msmsgsinvite' in cTypes:
2149 # header like info is sent as part of the message body.
2150 info = {}
2151 for line in message.message.split('\r\n'):
2152 try:
2153 key, val = line.split(':')
2154 info[key] = val.lstrip()
2155 except ValueError: continue
2156 if self._checkFileInvitation(message, info): return 0
2157 elif 'application/x-msnmsgrp2p' in cTypes:
2158 self._handleP2PMessage(message)
2159 return 0
2160 return 1
2161
2162 # negotiation
2163 def handle_USR(self, params):
2164 checkParamLen(len(params), 4, 'USR')
2165 if params[1] == "OK":
2166 self.loggedIn()
2167
2168 # invite a user
2169 def handle_CAL(self, params):
2170 checkParamLen(len(params), 3, 'CAL')
2171 id = int(params[0])
2172 if params[1].upper() == "RINGING":
2173 self._fireCallback(id, int(params[2])) # session ID as parameter
2174
2175 # user joined
2176 def handle_JOI(self, params):
2177 checkParamLen(len(params), 2, 'JOI')
2178 self.userJoined(params[0], unquote(params[1]))
2179
2180 # users participating in the current chat
2181 def handle_IRO(self, params):
2182 checkParamLen(len(params), 5, 'IRO')
2183 self.pendingUsers[params[3]] = unquote(params[4])
2184 if params[1] == params[2]:
2185 self.gotChattingUsers(self.pendingUsers)
2186 self.pendingUsers = {}
2187
2188 # finished listing users
2189 def handle_ANS(self, params):
2190 checkParamLen(len(params), 2, 'ANS')
2191 if params[1] == "OK":
2192 self.loggedIn()
2193
2194 def handle_ACK(self, params):
2195 checkParamLen(len(params), 1, 'ACK')
2196 self._fireCallback(int(params[0]), None)
2197
2198 def handle_NAK(self, params):
2199 checkParamLen(len(params), 1, 'NAK')
2200 self._fireCallback(int(params[0]), None)
2201
2202 def handle_BYE(self, params):
2203 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2204 self.userLeft(params[0])
2205
2206 # callbacks
2207
2208 def loggedIn(self):
2209 """
2210 called when all login details have been negotiated.
2211 Messages can now be sent, or new users invited.
2212 """
2213 pass
2214
2215 def gotChattingUsers(self, users):
2216 """
2217 called after connecting to an existing chat session.
2218
2219 @param users: A dict mapping user handles to screen names
2220 (current users taking part in the conversation)
2221 """
2222 pass
2223
2224 def userJoined(self, userHandle, screenName):
2225 """
2226 called when a user has joined the conversation.
2227
2228 @param userHandle: the user handle (passport) of the user
2229 @param screenName: the screen name of the user
2230 """
2231 pass
2232
2233 def userLeft(self, userHandle):
2234 """
2235 called when a user has left the conversation.
2236
2237 @param userHandle: the user handle (passport) of the user.
2238 """
2239 pass
2240
2241 def gotMessage(self, message):
2242 """
2243 called when we receive a message.
2244
2245 @param message: the associated MSNMessage object
2246 """
2247 pass
2248
2249 def gotSendRequest(self, fileReceive):
2250 """
2251 called when we receive a file send request from a contact
2252
2253 @param fileReceive: msnft.MSNFTReceive_Base instance
2254 """
2255 pass
2256
2257 def userTyping(self, message):
2258 """
2259 called when we receive the special type of message notifying
2260 us that a user is typing a message.
2261
2262 @param message: the associated MSNMessage object
2263 """
2264 pass
2265
2266 # api calls
2267
2268 def inviteUser(self, userHandle):
2269 """
2270 used to invite a user to the current switchboard server.
2271
2272 @param userHandle: the user handle (passport) of the desired user.
2273
2274 @return: A Deferred, the callback for which will be called
2275 when the server notifies us that the user has indeed
2276 been invited. The callback argument will be a tuple
2277 with 1 element, the sessionID given to the invited user.
2278 I'm not sure if this is useful or not.
2279 """
2280
2281 id, d = self._createIDMapping()
2282 self.sendLine("CAL %s %s" % (id, userHandle))
2283 return d
2284
2285 def sendMessage(self, message):
2286 """
2287 used to send a message.
2288
2289 @param message: the corresponding MSNMessage object.
2290
2291 @return: Depending on the value of message.ack.
2292 If set to MSNMessage.MESSAGE_ACK or
2293 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2294 the callback for which will be fired when an ACK or
2295 NACK is received - the callback argument will be
2296 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2297 the return value is None.
2298 """
2299
2300 if message.ack not in ('A','N','D'): id, d = self._nextTransactionID(), None
2301 else: id, d = self._createIDMapping()
2302 if message.length == 0: message.length = message._calcMessageLen()
2303 self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
2304 # apparently order matters with at least MIME-Version and Content-Type
2305 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
2306 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
2307 # send the rest of the headers
2308 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
2309 self.sendLine("%s: %s" % (header[0], header[1]))
2310 self.transport.write(CR+LF)
2311 self.transport.write(message.message)
2312 if MESSAGEDEBUG: log.msg(message.message)
2313 return d
2314
2315 def sendAvatarRequest(self, msnContact):
2316 """
2317 used to request an avatar from a user in this switchboard
2318 session.
2319
2320 @param msnContact: the msnContact object to request an avatar for
2321
2322 @return: A Deferred, the callback for which will be called
2323 when the avatar transfer succeeds.
2324 The callback argument will be a tuple with one element,
2325 the PNG avatar data.
2326 """
2327 if not msnContact.msnobj: return
2328 d = Deferred()
2329 def bufferClosed(data):
2330 d.callback((data,))
2331 buffer = StringBuffer(bufferClosed)
2332 slpLink = SLPLink_Receive(msnContact.userHandle, self, buffer)
2333 slpLink.avatarDataBuffer = buffer
2334 self.slpLinks[slpLink.sessionID] = slpLink
2335 self._sendMSNSLPCommand("INVITE", slpLink, MSN_AVATAR_GUID, msnContact.msnobj.text)
2336 return d
2337
2338 def sendTypingNotification(self):
2339 """
2340 Used to send a typing notification. Upon receiving this
2341 message the official client will display a 'user is typing'
2342 message to all other users in the chat session for 10 seconds.
2343 You should send one of these every 5 seconds as long as the
2344 user is typing.
2345 """
2346 m = MSNMessage()
2347 m.ack = m.MESSAGE_ACK_NONE
2348 m.setHeader('Content-Type', 'text/x-msmsgscontrol')
2349 m.setHeader('TypingUser', self.userHandle)
2350 m.message = "\r\n"
2351 self.sendMessage(m)
2352
2353 def sendFileInvitation(self, fileName, fileSize):
2354 """
2355 send an notification that we want to send a file.
2356
2357 @param fileName: the file name
2358 @param fileSize: the file size
2359
2360 @return: A Deferred, the callback of which will be fired
2361 when the user responds to this invitation with an
2362 appropriate message. The callback argument will be
2363 a tuple with 3 elements, the first being 1 or 0
2364 depending on whether they accepted the transfer
2365 (1=yes, 0=no), the second being an invitation cookie
2366 to identify your follow-up responses and the third being
2367 the message 'info' which is a dict of information they
2368 sent in their reply (this doesn't really need to be used).
2369 If you wish to proceed with the transfer see the
2370 sendTransferInfo method.
2371 """
2372 cookie = self._newInvitationCookie()
2373 d = Deferred()
2374 m = MSNMessage()
2375 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2376 m.message += 'Application-Name: File Transfer\r\n'
2377 m.message += 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2378 m.message += 'Invitation-Command: INVITE\r\n'
2379 m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2380 m.message += 'Application-File: %s\r\n' % fileName
2381 m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2382 m.ack = m.MESSAGE_ACK_NONE
2383 self.sendMessage(m)
2384 self.cookies['iCookies'][cookie] = (d, m)
2385 return d
2386
2387 def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2388 """
2389 send information relating to a file transfer session.
2390
2391 @param accept: whether or not to go ahead with the transfer
2392 (1=yes, 0=no)
2393 @param iCookie: the invitation cookie of previous replies
2394 relating to this transfer
2395 @param authCookie: the authentication cookie obtained from
2396 an FileSend instance
2397 @param ip: your ip
2398 @param port: the port on which an FileSend protocol is listening.
2399 """
2400 m = MSNMessage()
2401 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2402 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2403 m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2404 m.message += 'IP-Address: %s\r\n' % ip
2405 m.message += 'Port: %s\r\n' % port
2406 m.message += 'AuthCookie: %s\r\n' % authCookie
2407 m.message += '\r\n'
2408 m.ack = m.MESSAGE_NACK
2409 self.sendMessage(m)
2410
2411
2412 class BinaryFields:
2413 """ Utility class for the binary header & footer in p2p messages """
2414 ACK = 0x02
2415 WAIT = 0x04
2416 ERR = 0x08
2417 DATA = 0x20
2418 BYEGOT = 0x40
2419 BYESENT = 0x80
2420 DATAFT = 0x1000030
2421
2422 def __init__(self, fields=None, packet=None):
2423 if fields:
2424 self.fields = fields
2425 else:
2426 self.fields = [0] * 10
2427 if packet:
2428 self.unpackFields(packet)
2429
2430 def __getitem__(self, key):
2431 return self.fields[key]
2432
2433 def __setitem__(self, key, value):
2434 self.fields[key] = value
2435
2436 def unpackFields(self, packet):
2437 self.fields = struct.unpack("<LLQQLLLLQ", packet[0:48])
2438 self.fields += struct.unpack(">L", packet[len(packet)-4:])
2439 if MSNP2PDEBUG:
2440 print "Unpacked fields:",
2441 for i in self.fields:
2442 print hex(i),
2443 print
2444
2445 def packHeaders(self):
2446 f = tuple(self.fields)
2447 if MSNP2PDEBUG:
2448 print "Packed fields:",
2449 for i in self.fields:
2450 print hex(i),
2451 print
2452 return struct.pack("<LLQQLLLLQ", f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8])
2453
2454 def packFooter(self):
2455 return struct.pack(">L", self.fields[9])
2456
2457
2458 class MSNSLPMessage:
2459 """ Representation of a single MSNSLP message """
2460 def __init__(self, packet=None):
2461 self.method = ""
2462 self.status = ""
2463 self.to = ""
2464 self.fro = ""
2465 self.cseq = 0
2466 self.sessionGuid = ""
2467 self.sessionID = None
2468 self.euf_guid = ""
2469 self.data = "\r\n" + chr(0)
2470 if packet:
2471 self.parse(packet)
2472
2473 def create(self, method=None, status=None, to=None, fro=None, cseq=0, sessionGuid=None, data=None):
2474 self.method = method
2475 self.status = status
2476 self.to = to
2477 self.fro = fro
2478 self.cseq = cseq
2479 self.sessionGuid = sessionGuid
2480 if data: self.data = data
2481
2482 def setData(self, sessionID=None, appID=None, guid=None, context=None):
2483 s = []
2484 if guid: s.append("EUF-GUID: %s\r\n" % guid)
2485 if sessionID: s.append("SessionID: %s\r\n" % sessionID)
2486 if appID: s.append("AppID: %s\r\n" % appID)
2487 if context: s.append("Context: %s\r\n\r\n" % context)
2488 s.append(chr(0))
2489
2490 self.data = "".join(s)
2491
2492 def parse(self, s):
2493 s = s[48:len(s)-4:]
2494 if s.find("MSNSLP/1.0") < 0: return
2495
2496 lines = s.split("\r\n")
2497
2498 # Get the MSNSLP method or status
2499 msnslp = lines[0].split(" ")
2500 if MSNP2PDEBUG: print "Parsing MSNSLPMessage", s, len(s)
2501 if msnslp[0] in ("INVITE", "BYE"):
2502 self.method = msnslp[0].strip()
2503 else:
2504 self.status = msnslp[1].strip()
2505
2506 lines.remove(lines[0])
2507
2508 for line in lines:
2509 line = line.split(":")
2510 if len(line) > 1:
2511 if len(line) > 2 and line[0] == "To":
2512 self.to = line[2][:line[2].find('>')]
2513 elif len(line) > 2 and line[0] == "From":
2514 self.fro = line[2][:line[2].find('>')]
2515 elif line[0] == "Call-ID":
2516 self.sessionGuid = line[1].strip()
2517 elif line[0] == "CSeq":
2518 self.cseq = int(line[1].strip())
2519 elif line[0] == "SessionID":
2520 self.sessionID = int(line[1].strip())
2521 elif line[0] == "EUF-GUID":
2522 self.euf_guid = line[1].strip()
2523
2524 def __str__(self):
2525 s = []
2526 if self.method:
2527 s.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self.method, self.to))
2528 else:
2529 s.append("MSNSLP/1.0 %s\r\n" % self.status)
2530 s.append("To: <msnmsgr:%s>\r\n" % self.to)
2531 s.append("From: <msnmsgr:%s>\r\n" % self.fro)
2532 s.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % random_guid())
2533 s.append("CSeq: %s \r\n" % str(self.cseq))
2534 s.append("Call-ID: %s\r\n" % self.sessionGuid)
2535 s.append("Max-Forwards: 0\r\n")
2536 if self.method == "BYE":
2537 s.append("Content-Type: application/x-msnmsgr-sessionclosebody\r\n")
2538 else:
2539 s.append("Content-Type: application/x-msnmsgr-sessionreqbody\r\n")
2540 s.append("Content-Length: %s\r\n\r\n" % len(self.data))
2541 s.append(self.data)
2542 return "".join(s)
2543
2544
2545 class SeqID:
2546 """ Utility for handling the weird sequence IDs in p2p messages """
2547 def __init__(self, baseID=None):
2548 if baseID:
2549 self.seqID = baseID
2550 else:
2551 self.seqID = random.randint(4, 2**30)
2552 self.pos = -1
2553
2554 def get(self):
2555 if self.pos == 0:
2556 return self.seqID
2557 else:
2558 return self.seqID + self.pos - 3
2559
2560 def next(self):
2561 self.pos += 1
2562 if self.pos == 3:
2563 self.pos += 1
2564 return self.get()
2565
2566 class StringBuffer(StringIO.StringIO):
2567 def __init__(self, notifyFunc=None):
2568 self.notifyFunc = notifyFunc
2569 StringIO.StringIO.__init__(self)
2570
2571 def finish(self):
2572 if self.notifyFunc:
2573 self.notifyFunc(self.getvalue())
2574 self.notifyFunc = None
2575 StringIO.StringIO.close(self)
2576
2577
2578 class SLPLink:
2579 def __init__(self, remoteUser, switchboard, sessionID, sessionGuid):
2580 if not sessionID:
2581 sessionID = random.randint(1000, sys.maxint)
2582 if not sessionGuid:
2583 sessionGuid = random_guid()
2584 self.remoteUser = remoteUser
2585 self.switchboard = switchboard
2586 self.sessionID = sessionID
2587 self.sessionGuid = sessionGuid
2588 self.seqID = SeqID()
2589
2590 def transferReady(self):
2591 pass
2592
2593 def setError(self, text):
2594 if MSNP2PDEBUG:
2595 print "ERROR in avatar transfer: ", self, text, "in state:", self.state
2596 self.state = self.ERROR
2597
2598 def warn(self, text):
2599 if MSNP2PDEBUG:
2600 print "Warning in avatar transfer: ", self, text, "in state:", self.state
2601
2602
2603 class SLPLink_Send(SLPLink):
2604 def __init__(self, remoteUser, switchboard, length, sessionID=None, sessionGuid=None):
2605 SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
2606 self.handlePacket = None
2607 self.offset = 0
2608 self.length = length
2609 self.ready = False # Have we received the go ahead to transmit yet?
2610 self.data = ""
2611 # Send dataprep
2612 self.send_dataprep()
2613
2614 def send_dataprep(self):
2615 binaryFields = BinaryFields()
2616 binaryFields[0] = self.sessionID
2617 binaryFields[1] = self.seqID.next()
2618 binaryFields[3] = 4
2619 binaryFields[4] = 4
2620 binaryFields[6] = random.randint(0, sys.maxint)
2621 binaryFields[9] = 1
2622
2623 self.switchboard._sendP2PMessage(binaryFields, chr(0) * 4, self.remoteUser)
2624
2625 def transferReady(self):
2626 self.ready = True
2627 if self.data:
2628 data = self.data
2629 self.data = ""
2630 self.write(data)
2631
2632 def write(self, data):
2633 if not self.ready:
2634 self.data += data
2635 return
2636
2637 i = 0
2638 length = len(data)
2639 while i < length:
2640 if i + 1202 < length:
2641 self._writeChunk(data[i:i+1202])
2642 i += 1202
2643 else:
2644 self.data += data[i:]
2645 if len(self.data) >= 1202:
2646 data = self.data
2647 self.data = ""
2648 self.write(data)
2649 return
2650
2651 def _writeChunk(self, chunk):
2652 binaryFields = BinaryFields()
2653 binaryFields[0] = self.sessionID
2654 binaryFields[1] = self.seqID.next()
2655 binaryFields[2] = self.offset
2656 binaryFields[3] = self.length
2657 binaryFields[4] = len(chunk)
2658 binaryFields[5] = BinaryFields.DATA
2659 binaryFields[6] = random.randint(0, 2**30)
2660 binaryFields[9] = 1
2661
2662 self.offset += len(chunk)
2663 self.switchboard._sendP2PMessage(binaryFields, chunk, self.remoteUser)
2664
2665 def finish(self):
2666 if self.data:
2667 self._writeChunk(self.data)
2668 self.switchboard = None
2669 # FIXME Now tell the switchboard
2670
2671 def error(self):
2672 pass
2673 # Same as above?
2674
2675
2676 class SLPLink_Receive(SLPLink):
2677 def __init__(self, remoteUser, switchboard, consumer, sessionID=None, sessionGuid=None):
2678 SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
2679 self.consumer = consumer
2680 self.pos = 0
2681
2682 self.handlePacket = self.wait_dataprep
2683
2684 def wait_dataprep(self, packet):
2685 binaryFields = BinaryFields()
2686 binaryFields.unpackFields(packet)
2687
2688 if binaryFields[0] != self.sessionID:
2689 self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
2690 return
2691 if binaryFields[3] != 4:
2692 self.setError("field3," + str(binaryFields[3]))
2693 return
2694 if binaryFields[4] != 4:
2695 self.setError("field4," + str(binaryFields[4]))
2696 return
2697 if binaryFields[9] != 1:
2698 self.warn("field9," + str(binaryFields[9]))
2699 # return
2700
2701 self.switchboard._sendP2PACK(self, binaryFields)
2702 self.handlePacket = self.wait_data
2703
2704 def wait_data(self, packet):
2705 binaryFields = BinaryFields()
2706 binaryFields.unpackFields(packet)
2707 if binaryFields[0] != self.sessionID:
2708 self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
2709 return
2710 if binaryFields[5] != BinaryFields.DATA:
2711 self.setError("field5," + str(binaryFields[5]))
2712 return
2713 if binaryFields[9] != 1:
2714 self.warn("field9," + str(binaryFields[9]))
2715 # return
2716 offset = binaryFields[2]
2717 total = binaryFields[3]
2718 length = binaryFields[4]
2719
2720 data = packet[48:-4]
2721 if offset != self.pos:
2722 self.setError("Received packet out of order")
2723 self.consumer.error()
2724 return
2725 if len(data) != length:
2726 self.setError("Received bad length of slp")
2727 self.consumer.error()
2728 return
2729
2730 self.pos += length
2731
2732 self.consumer.write(data)
2733
2734 if self.pos == total:
2735 self.switchboard._sendP2PACK(self, binaryFields)
2736 self.consumer.finish()
2737 self.handlePacket = None
2738 del self.switchboard.slpLinks[self.sessionID]
2739 self.switchboard = None
2740
2741
2742
2743
2744
2745
2746 # mapping of error codes to error messages
2747 errorCodes = {
2748
2749 200 : "Syntax error",
2750 201 : "Invalid parameter",
2751 205 : "Invalid user",
2752 206 : "Domain name missing",
2753 207 : "Already logged in",
2754 208 : "Invalid username",
2755 209 : "Invalid screen name",
2756 210 : "User list full",
2757 215 : "User already there",
2758 216 : "User already on list",
2759 217 : "User not online",
2760 218 : "Already in mode",
2761 219 : "User is in the opposite list",
2762 223 : "Too many groups",
2763 224 : "Invalid group",
2764 225 : "User not in group",
2765 229 : "Group name too long",
2766 230 : "Cannot remove group 0",
2767 231 : "Invalid group",
2768 280 : "Switchboard failed",
2769 281 : "Transfer to switchboard failed",
2770
2771 300 : "Required field missing",
2772 301 : "Too many FND responses",
2773 302 : "Not logged in",
2774
2775 402 : "Error accessing contact list",
2776 403 : "Error accessing contact list",
2777
2778 500 : "Internal server error",
2779 501 : "Database server error",
2780 502 : "Command disabled",
2781 510 : "File operation failed",
2782 520 : "Memory allocation failed",
2783 540 : "Wrong CHL value sent to server",
2784
2785 600 : "Server is busy",
2786 601 : "Server is unavaliable",
2787 602 : "Peer nameserver is down",
2788 603 : "Database connection failed",
2789 604 : "Server is going down",
2790 605 : "Server unavailable",
2791
2792 707 : "Could not create connection",
2793 710 : "Invalid CVR parameters",
2794 711 : "Write is blocking",
2795 712 : "Session is overloaded",
2796 713 : "Too many active users",
2797 714 : "Too many sessions",
2798 715 : "Not expected",
2799 717 : "Bad friend file",
2800 731 : "Not expected",
2801
2802 800 : "Requests too rapid",
2803
2804 910 : "Server too busy",
2805 911 : "Authentication failed",
2806 912 : "Server too busy",
2807 913 : "Not allowed when offline",
2808 914 : "Server too busy",
2809 915 : "Server too busy",
2810 916 : "Server too busy",
2811 917 : "Server too busy",
2812 918 : "Server too busy",
2813 919 : "Server too busy",
2814 920 : "Not accepting new users",
2815 921 : "Server too busy",
2816 922 : "Server too busy",
2817 923 : "No parent consent",
2818 924 : "Passport account not yet verified"
2819
2820 }
2821
2822 # mapping of status codes to readable status format
2823 statusCodes = {
2824
2825 STATUS_ONLINE : "Online",
2826 STATUS_OFFLINE : "Offline",
2827 STATUS_HIDDEN : "Appear Offline",
2828 STATUS_IDLE : "Idle",
2829 STATUS_AWAY : "Away",
2830 STATUS_BUSY : "Busy",
2831 STATUS_BRB : "Be Right Back",
2832 STATUS_PHONE : "On the Phone",
2833 STATUS_LUNCH : "Out to Lunch"
2834
2835 }
2836
2837 # mapping of list ids to list codes
2838 listIDToCode = {
2839
2840 FORWARD_LIST : 'fl',
2841 BLOCK_LIST : 'bl',
2842 ALLOW_LIST : 'al',
2843 REVERSE_LIST : 'rl',
2844 PENDING_LIST : 'pl'
2845
2846 }
2847
2848 # mapping of list codes to list ids
2849 listCodeToID = {}
2850 for id,code in listIDToCode.items():
2851 listCodeToID[code] = id
2852
2853 del id, code