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