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