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