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