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