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