]> code.delx.au - pymsnt/blob - src/tlib/msn/msn.py
Trunk is broken. Don't use!
[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 TODO
82 ====
83 - check message hooks with invalid x-msgsinvite messages.
84 - font handling
85 - switchboard factory
86
87 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
88 """
89
90 from __future__ import nested_scopes
91
92 # Sibling imports
93 from twisted.protocols.basic import LineReceiver
94 try:
95 from twisted.web.http import HTTPClient
96 except ImportError:
97 try:
98 from twisted.protocols.http import HTTPClient
99 except ImportError:
100 print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
101 raise
102 import msnp11chl
103 from msnp2p import random_guid
104 import msnp2p
105
106 # Twisted imports
107 from twisted.internet import reactor, task
108 from twisted.internet.defer import Deferred
109 from twisted.internet.protocol import ClientFactory
110 from twisted.internet.ssl import ClientContextFactory
111 from twisted.python import failure, log
112 from twisted.xish.domish import unescapeFromXml, SuxElementStream
113
114 # System imports
115 import types, operator, os, sys, base64, random
116 from urllib import quote, unquote
117
118 MSN_PROTOCOL_VERSION = "MSNP11 CVR0" # protocol version
119 MSN_PORT = 1863 # default dispatch server port
120 MSN_MAX_MESSAGE = 1664 # max message length
121 MSN_CVR_STR = "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
122 MSN_AVATAR_GUID = "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
123 MSN_MSNFTP_GUID = "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
124
125 # auth constants
126 LOGIN_SUCCESS = 1
127 LOGIN_FAILURE = 2
128 LOGIN_REDIRECT = 3
129
130 # list constants
131 FORWARD_LIST = 1
132 ALLOW_LIST = 2
133 BLOCK_LIST = 4
134 REVERSE_LIST = 8
135 PENDING_LIST = 16
136
137 # phone constants
138 HOME_PHONE = "PHH"
139 WORK_PHONE = "PHW"
140 MOBILE_PHONE = "PHM"
141 HAS_PAGER = "MOB"
142 HAS_BLOG = "HSB"
143
144 # status constants
145 STATUS_ONLINE = 'NLN'
146 STATUS_OFFLINE = 'FLN'
147 STATUS_HIDDEN = 'HDN'
148 STATUS_IDLE = 'IDL'
149 STATUS_AWAY = 'AWY'
150 STATUS_BUSY = 'BSY'
151 STATUS_BRB = 'BRB'
152 STATUS_PHONE = 'PHN'
153 STATUS_LUNCH = 'LUN'
154
155 CR = "\r"
156 LF = "\n"
157 PINGSPEED = 50.0
158
159 LINEDEBUG = True
160 MESSAGEDEBUG = True
161
162 def getVal(inp):
163 return inp.split('=')[1]
164
165 def checkParamLen(num, expected, cmd, error=None):
166 if error == None: error = "Invalid Number of Parameters for %s" % cmd
167 if num != expected: raise MSNProtocolError, error
168
169 def _parseHeader(h, v):
170 """
171 Split a certin number of known
172 header values with the format:
173 field1=val,field2=val,field3=val into
174 a dict mapping fields to values.
175 @param h: the header's key
176 @param v: the header's value as a string
177 """
178
179 if h in ('passporturls','authentication-info','www-authenticate'):
180 v = v.replace('Passport1.4','').lstrip()
181 fields = {}
182 for fieldPair in v.split(','):
183 try:
184 field,value = fieldPair.split('=',1)
185 fields[field.lower()] = value
186 except ValueError:
187 fields[field.lower()] = ''
188 return fields
189 else: return v
190
191 def _parsePrimitiveHost(host):
192 # Ho Ho Ho
193 h,p = host.replace('https://','').split('/',1)
194 p = '/' + p
195 return h,p
196
197 def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
198 """
199 This function is used internally and should not ever be called
200 directly.
201 """
202 cb = Deferred()
203 def _cb(server, auth):
204 loginFac = ClientFactory()
205 loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
206 reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
207
208 if cached:
209 _cb(nexusServer, authData)
210 else:
211 fac = ClientFactory()
212 d = Deferred()
213 d.addCallbacks(_cb, callbackArgs=(authData,))
214 d.addErrback(lambda f: cb.errback(f))
215 fac.protocol = lambda : PassportNexus(d, nexusServer)
216 reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
217 return cb
218
219
220 class PassportNexus(HTTPClient):
221
222 """
223 Used to obtain the URL of a valid passport
224 login HTTPS server.
225
226 This class is used internally and should
227 not be instantiated directly -- that is,
228 The passport logging in process is handled
229 transparantly by NotificationClient.
230 """
231
232 def __init__(self, deferred, host):
233 self.deferred = deferred
234 self.host, self.path = _parsePrimitiveHost(host)
235
236 def connectionMade(self):
237 HTTPClient.connectionMade(self)
238 self.sendCommand('GET', self.path)
239 self.sendHeader('Host', self.host)
240 self.endHeaders()
241 self.headers = {}
242
243 def handleHeader(self, header, value):
244 h = header.lower()
245 self.headers[h] = _parseHeader(h, value)
246
247 def handleEndHeaders(self):
248 if self.connected: self.transport.loseConnection()
249 if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
250 self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
251 else:
252 self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
253
254 def handleResponse(self, r): pass
255
256 class PassportLogin(HTTPClient):
257 """
258 This class is used internally to obtain
259 a login ticket from a passport HTTPS
260 server -- it should not be used directly.
261 """
262
263 _finished = 0
264
265 def __init__(self, deferred, userHandle, passwd, host, authData):
266 self.deferred = deferred
267 self.userHandle = userHandle
268 self.passwd = passwd
269 self.authData = authData
270 self.host, self.path = _parsePrimitiveHost(host)
271
272 def connectionMade(self):
273 self.sendCommand('GET', self.path)
274 self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
275 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
276 self.sendHeader('Host', self.host)
277 self.endHeaders()
278 self.headers = {}
279
280 def handleHeader(self, header, value):
281 h = header.lower()
282 self.headers[h] = _parseHeader(h, value)
283
284 def handleEndHeaders(self):
285 if self._finished: return
286 self._finished = 1 # I think we need this because of HTTPClient
287 if self.connected: self.transport.loseConnection()
288 authHeader = 'authentication-info'
289 _interHeader = 'www-authenticate'
290 if self.headers.has_key(_interHeader): authHeader = _interHeader
291 try:
292 info = self.headers[authHeader]
293 status = info['da-status']
294 handler = getattr(self, 'login_%s' % (status,), None)
295 if handler:
296 handler(info)
297 else: raise Exception()
298 except Exception, e:
299 self.deferred.errback(failure.Failure(e))
300
301 def handleResponse(self, r): pass
302
303 def login_success(self, info):
304 ticket = info['from-pp']
305 ticket = ticket[1:len(ticket)-1]
306 self.deferred.callback((LOGIN_SUCCESS, ticket))
307
308 def login_failed(self, info):
309 self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
310
311 def login_redir(self, info):
312 self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
313
314 class MSNProtocolError(Exception):
315 """
316 This Exception is basically used for debugging
317 purposes, as the official MSN server should never
318 send anything _wrong_ and nobody in their right
319 mind would run their B{own} MSN server.
320 If it is raised by default command handlers
321 (handle_BLAH) the error will be logged.
322 """
323 pass
324
325 class MSNMessage:
326
327 """
328 I am the class used to represent an 'instant' message.
329
330 @ivar userHandle: The user handle (passport) of the sender
331 (this is only used when receiving a message)
332 @ivar screenName: The screen name of the sender (this is only used
333 when receiving a message)
334 @ivar message: The message
335 @ivar headers: The message headers
336 @type headers: dict
337 @ivar length: The message length (including headers and line endings)
338 @ivar ack: This variable is used to tell the server how to respond
339 once the message has been sent. If set to MESSAGE_ACK
340 (default) the server will respond with an ACK upon receiving
341 the message, if set to MESSAGE_NACK the server will respond
342 with a NACK upon failure to receive the message.
343 If set to MESSAGE_ACK_NONE the server will do nothing.
344 This is relevant for the return value of
345 SwitchboardClient.sendMessage (which will return
346 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
347 and will fire when the respective ACK or NACK is received).
348 If set to MESSAGE_ACK_NONE sendMessage will return None.
349 """
350 MESSAGE_ACK = 'A'
351 MESSAGE_ACK_FAT = 'D'
352 MESSAGE_NACK = 'N'
353 MESSAGE_ACK_NONE = 'U'
354
355 ack = MESSAGE_ACK
356
357 def __init__(self, length=0, userHandle="", screenName="", message="", specialMessage=False):
358 self.userHandle = userHandle
359 self.screenName = screenName
360 self.specialMessage = specialMessage
361 self.message = message
362 self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
363 self.length = length
364 self.readPos = 0
365
366 def _calcMessageLen(self):
367 """
368 used to calculte the number to send
369 as the message length when sending a message.
370 """
371 return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
372
373 def setHeader(self, header, value):
374 """ set the desired header """
375 self.headers[header] = value
376
377 def getHeader(self, header):
378 """
379 get the desired header value
380 @raise KeyError: if no such header exists.
381 """
382 return self.headers[header]
383
384 def hasHeader(self, header):
385 """ check to see if the desired header exists """
386 return self.headers.has_key(header)
387
388 def getMessage(self):
389 """ return the message - not including headers """
390 return self.message
391
392 def setMessage(self, message):
393 """ set the message text """
394 self.message = message
395
396 class MSNContact:
397
398 """
399 This class represents a contact (user).
400
401 @ivar userGuid: The contact's user guid (unique string)
402 @ivar userHandle: The contact's user handle (passport).
403 @ivar screenName: The contact's screen name.
404 @ivar groups: A list of all the group IDs which this
405 contact belongs to.
406 @ivar lists: An integer representing the sum of all lists
407 that this contact belongs to.
408 @ivar caps: int, The capabilities of this client
409 @ivar msnobj: msnp2p.MSNOBJ, The MSNObject representing the contact's avatar
410 @ivar status: The contact's status code.
411 @type status: str if contact's status is known, None otherwise.
412 @ivar personal: The contact's personal message .
413 @type personal: str if contact's personal message is known, None otherwise.
414
415 @ivar homePhone: The contact's home phone number.
416 @type homePhone: str if known, otherwise None.
417 @ivar workPhone: The contact's work phone number.
418 @type workPhone: str if known, otherwise None.
419 @ivar mobilePhone: The contact's mobile phone number.
420 @type mobilePhone: str if known, otherwise None.
421 @ivar hasPager: Whether or not this user has a mobile pager
422 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
423 (true=yes, false=no)
424 """
425 MSNC1 = 0x10000000
426 MSNC2 = 0x20000000
427 MSNC3 = 0x30000000
428 MSNC4 = 0x40000000
429
430 def __init__(self, userGuid="", userHandle="", screenName="", lists=0, caps=0, msnobj=None, groups={}, status=None, personal=""):
431 self.userGuid = userGuid
432 self.userHandle = userHandle
433 self.screenName = screenName
434 self.lists = lists
435 self.caps = caps
436 self.msnobj = msnobj
437 self.msnobjGot = True
438 self.groups = [] # if applicable
439 self.status = status # current status
440 self.personal = personal
441
442 # phone details
443 self.homePhone = None
444 self.workPhone = None
445 self.mobilePhone = None
446 self.hasPager = None
447 self.hasBlog = None
448
449 def setPhone(self, phoneType, value):
450 """
451 set phone numbers/values for this specific user.
452 for phoneType check the *_PHONE constants and HAS_PAGER
453 """
454
455 t = phoneType.upper()
456 if t == HOME_PHONE: self.homePhone = value
457 elif t == WORK_PHONE: self.workPhone = value
458 elif t == MOBILE_PHONE: self.mobilePhone = value
459 elif t == HAS_PAGER: self.hasPager = value
460 elif t == HAS_BLOG: self.hasBlog = value
461 #else: raise ValueError, "Invalid Phone Type: " + t
462
463 def addToList(self, listType):
464 """
465 Update the lists attribute to
466 reflect being part of the
467 given list.
468 """
469 self.lists |= listType
470
471 def removeFromList(self, listType):
472 """
473 Update the lists attribute to
474 reflect being removed from the
475 given list.
476 """
477 self.lists ^= listType
478
479 class MSNContactList:
480 """
481 This class represents a basic MSN contact list.
482
483 @ivar contacts: All contacts on my various lists
484 @type contacts: dict (mapping user handles to MSNContact objects)
485 @ivar groups: a mapping of group ids to group names
486 (groups can only exist on the forward list)
487 @type groups: dict
488
489 B{Note}:
490 This is used only for storage and doesn't effect the
491 server's contact list.
492 """
493
494 def __init__(self):
495 self.contacts = {}
496 self.groups = {}
497 self.autoAdd = 0
498 self.privacy = 0
499
500 def _getContactsFromList(self, listType):
501 """
502 Obtain all contacts which belong
503 to the given list type.
504 """
505 return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
506
507 def addContact(self, contact):
508 """
509 Add a contact
510 """
511 self.contacts[contact.userHandle] = contact
512
513 def remContact(self, userHandle):
514 """
515 Remove a contact
516 """
517 try:
518 del self.contacts[userHandle]
519 except KeyError: pass
520
521 def getContact(self, userHandle):
522 """
523 Obtain the MSNContact object
524 associated with the given
525 userHandle.
526 @return: the MSNContact object if
527 the user exists, or None.
528 """
529 try:
530 return self.contacts[userHandle]
531 except KeyError:
532 return None
533
534 def getBlockedContacts(self):
535 """
536 Obtain all the contacts on my block list
537 """
538 return self._getContactsFromList(BLOCK_LIST)
539
540 def getAuthorizedContacts(self):
541 """
542 Obtain all the contacts on my auth list.
543 (These are contacts which I have verified
544 can view my state changes).
545 """
546 return self._getContactsFromList(ALLOW_LIST)
547
548 def getReverseContacts(self):
549 """
550 Get all contacts on my reverse list.
551 (These are contacts which have added me
552 to their forward list).
553 """
554 return self._getContactsFromList(REVERSE_LIST)
555
556 def getContacts(self):
557 """
558 Get all contacts on my forward list.
559 (These are the contacts which I have added
560 to my list).
561 """
562 return self._getContactsFromList(FORWARD_LIST)
563
564 def setGroup(self, id, name):
565 """
566 Keep a mapping from the given id
567 to the given name.
568 """
569 self.groups[id] = name
570
571 def remGroup(self, id):
572 """
573 Removed the stored group
574 mapping for the given id.
575 """
576 try:
577 del self.groups[id]
578 except KeyError: pass
579 for c in self.contacts:
580 if id in c.groups: c.groups.remove(id)
581
582
583 class MSNEventBase(LineReceiver):
584 """
585 This class provides support for handling / dispatching events and is the
586 base class of the three main client protocols (DispatchClient,
587 NotificationClient, SwitchboardClient)
588 """
589
590 def __init__(self):
591 self.ids = {} # mapping of ids to Deferreds
592 self.currentID = 0
593 self.connected = 0
594 self.setLineMode()
595 self.currentMessage = None
596
597 def connectionLost(self, reason):
598 self.ids = {}
599 self.connected = 0
600
601 def connectionMade(self):
602 self.connected = 1
603
604 def _fireCallback(self, id, *args):
605 """
606 Fire the callback for the given id
607 if one exists and return 1, else return false
608 """
609 if self.ids.has_key(id):
610 self.ids[id][0].callback(args)
611 del self.ids[id]
612 return 1
613 return 0
614
615 def _nextTransactionID(self):
616 """ return a usable transaction ID """
617 self.currentID += 1
618 if self.currentID > 1000: self.currentID = 1
619 return self.currentID
620
621 def _createIDMapping(self, data=None):
622 """
623 return a unique transaction ID that is mapped internally to a
624 deferred .. also store arbitrary data if it is needed
625 """
626 id = self._nextTransactionID()
627 d = Deferred()
628 self.ids[id] = (d, data)
629 return (id, d)
630
631 def checkMessage(self, message):
632 """
633 process received messages to check for file invitations and
634 typing notifications and other control type messages
635 """
636 raise NotImplementedError
637
638 def sendLine(self, line):
639 if LINEDEBUG: log.msg(">> " + line)
640 LineReceiver.sendLine(self, line)
641
642 def lineReceived(self, line):
643 if LINEDEBUG: log.msg("<< " + line)
644 if self.currentMessage:
645 self.currentMessage.readPos += len(line+CR+LF)
646 try:
647 header, value = line.split(':')
648 self.currentMessage.setHeader(header, unquote(value).lstrip())
649 return
650 except ValueError:
651 #raise MSNProtocolError, "Invalid Message Header"
652 line = ""
653 if line == "" or self.currentMessage.specialMessage:
654 self.setRawMode()
655 if self.currentMessage.readPos == self.currentMessage.length: self.rawDataReceived("") # :(
656 return
657 try:
658 cmd, params = line.split(' ', 1)
659 except ValueError:
660 raise MSNProtocolError, "Invalid Message, %s" % repr(line)
661
662 if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
663 if cmd.isdigit():
664 if self.ids.has_key(params.split(' ')[0]):
665 self.ids[id].errback(int(cmd))
666 del self.ids[id]
667 return
668 else: # we received an error which doesn't map to a sent command
669 self.gotError(int(cmd))
670 return
671
672 handler = getattr(self, "handle_%s" % cmd.upper(), None)
673 if handler:
674 try: handler(params.split(' '))
675 except MSNProtocolError, why: self.gotBadLine(line, why)
676 else:
677 self.handle_UNKNOWN(cmd, params.split(' '))
678
679 def rawDataReceived(self, data):
680 extra = ""
681 self.currentMessage.readPos += len(data)
682 diff = self.currentMessage.readPos - self.currentMessage.length
683 if diff > 0:
684 self.currentMessage.message += data[:-diff]
685 extra = data[-diff:]
686 elif diff == 0:
687 self.currentMessage.message += data
688 else:
689 self.currentMessage.message += data
690 return
691 del self.currentMessage.readPos
692 m = self.currentMessage
693 self.currentMessage = None
694 if MESSAGEDEBUG: log.msg(m.message)
695 if not self.checkMessage(m):
696 self.setLineMode(extra)
697 return
698 self.gotMessage(m)
699 self.setLineMode(extra)
700
701 ### protocol command handlers - no need to override these.
702
703 def handle_MSG(self, params):
704 checkParamLen(len(params), 3, 'MSG')
705 try:
706 messageLen = int(params[2])
707 except ValueError: raise MSNProtocolError, "Invalid Parameter for MSG length argument"
708 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
709
710 def handle_UNKNOWN(self, cmd, params):
711 """ implement me in subclasses if you want to handle unknown events """
712 log.msg("Received unknown command (%s), params: %s" % (cmd, params))
713
714 ### callbacks
715
716 def gotBadLine(self, line, why):
717 """ called when a handler notifies me that this line is broken """
718 log.msg('Error in line: %s (%s)' % (line, why))
719
720 def gotError(self, errorCode):
721 """
722 called when the server sends an error which is not in
723 response to a sent command (ie. it has no matching transaction ID)
724 """
725 log.msg('Error %s' % (errorCodes[errorCode]))
726
727 class DispatchClient(MSNEventBase):
728 """
729 This class provides support for clients connecting to the dispatch server
730 @ivar userHandle: your user handle (passport) needed before connecting.
731 """
732
733 # eventually this may become an attribute of the
734 # factory.
735 userHandle = ""
736
737 def connectionMade(self):
738 MSNEventBase.connectionMade(self)
739 self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
740
741 ### protocol command handlers ( there is no need to override these )
742
743 def handle_VER(self, params):
744 versions = params[1:]
745 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
746 self.transport.loseConnection()
747 raise MSNProtocolError, "Invalid version response"
748 id = self._nextTransactionID()
749 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
750
751 def handle_CVR(self, params):
752 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
753
754 def handle_XFR(self, params):
755 if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
756 id, refType, addr = params[:3]
757 # was addr a host:port pair?
758 try:
759 host, port = addr.split(':')
760 except ValueError:
761 host = addr
762 port = MSN_PORT
763 if refType == "NS":
764 self.gotNotificationReferral(host, int(port))
765
766 ### callbacks
767
768 def gotNotificationReferral(self, host, port):
769 """
770 called when we get a referral to the notification server.
771
772 @param host: the notification server's hostname
773 @param port: the port to connect to
774 """
775 pass
776
777
778 class NotificationClient(MSNEventBase):
779 """
780 This class provides support for clients connecting
781 to the notification server.
782 """
783
784 factory = None # sssh pychecker
785
786 def __init__(self, currentID=0):
787 MSNEventBase.__init__(self)
788 self.currentID = currentID
789 self._state = ['DISCONNECTED', {}]
790 self.pingCounter = 0
791 self.pingCheckTask = None
792 self.msnobj = msnp2p.MSNOBJ()
793
794 def _setState(self, state):
795 self._state[0] = state
796
797 def _getState(self):
798 return self._state[0]
799
800 def _getStateData(self, key):
801 return self._state[1][key]
802
803 def _setStateData(self, key, value):
804 self._state[1][key] = value
805
806 def _remStateData(self, *args):
807 for key in args: del self._state[1][key]
808
809 def connectionMade(self):
810 MSNEventBase.connectionMade(self)
811 self._setState('CONNECTED')
812 self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
813
814 def connectionLost(self, reason):
815 self._setState('DISCONNECTED')
816 self._state[1] = {}
817 if self.pingCheckTask:
818 self.pingCheckTask.stop()
819 self.pingCheckTask = None
820 MSNEventBase.connectionLost(self, reason)
821
822 def _getEmailFields(self, message):
823 fields = message.getMessage().strip().split('\n')
824 values = {}
825 for i in fields:
826 a = i.split(':')
827 if len(a) != 2: continue
828 f, v = a
829 f = f.strip()
830 v = v.strip()
831 values[f] = v
832 return values
833
834 def _gotInitialEmailNotification(self, message):
835 values = self._getEmailFields(message)
836 try:
837 inboxunread = int(values["Inbox-Unread"])
838 foldersunread = int(values["Folders-Unread"])
839 except KeyError:
840 return
841 if foldersunread + inboxunread > 0: # For some reason MSN sends notifications about empty inboxes sometimes?
842 self.gotInitialEmailNotification(inboxunread, foldersunread)
843
844 def _gotEmailNotification(self, message):
845 values = self._getEmailFields(message)
846 try:
847 mailfrom = values["From"]
848 fromaddr = values["From-Addr"]
849 subject = values["Subject"]
850 junkbeginning = "=?\"us-ascii\"?Q?"
851 junkend = "?="
852 subject = subject.replace(junkbeginning, "").replace(junkend, "").replace("_", " ")
853 except KeyError:
854 # If any of the fields weren't found then it's not a big problem. We just ignore the message
855 return
856 self.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
857
858 def _gotMSNAlert(self, message):
859 notification = utils.parseText(message.message, beExtremelyLenient=True)
860 siteurl = notification.getAttribute("siteurl")
861 notid = notification.getAttribute("id")
862
863 msg = None
864 for e in notification.elements():
865 if e.name == "MSG":
866 msg = e
867 break
868 else: return
869
870 msgid = msg.getAttribute("id")
871
872 action = None
873 subscr = None
874 bodytext = None
875 for e in msg.elements():
876 if e.name == "ACTION":
877 action = e.getAttribute("url")
878 if e.name == "SUBSCR":
879 subscr = e.getAttribute("url")
880 if e.name == "BODY":
881 for e2 in e.elements():
882 if e2.name == "TEXT":
883 bodytext = e2.__str__()
884 if not (action and subscr and bodytext): return
885
886 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
887 subscrurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (subscr, notid, msgid)
888
889 self.gotMSNAlert(bodytext, actionurl, subscrurl)
890
891 def _gotUBX(self, message):
892 lm = message.message.lower()
893 p1 = lm.find("<psm>") + 5
894 p2 = lm.find("</psm>")
895 if p1 >= 0 and p2 >= 0:
896 personal = unescapeFromXml(message.message[p1:p2])
897 msnContact = self.factory.contacts.getContact(userHandle)
898 if not msnContact: return
899 msnContact.personal = personal
900 self.contactPersonalChanged(message.userHandle, personal)
901
902 def checkMessage(self, message):
903 """ hook used for detecting specific notification messages """
904 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
905 if 'text/x-msmsgsprofile' in cTypes:
906 self.gotProfile(message)
907 return 0
908 elif "text/x-msmsgsinitialemailnotification" in cTypes:
909 self._gotInitialEmailNotification(message)
910 return 0
911 elif "text/x-msmsgsemailnotification" in cTypes:
912 self._gotEmailNotification(message)
913 return 0
914 elif "NOTIFICATION" == message.userHandle and message.specialMessage == True:
915 self._gotMSNAlert(message)
916 return 0
917 elif "UBX" == message.screenName and message.specialMessage == True:
918 self._gotUBX(message)
919 return 0
920 return 1
921
922 ### protocol command handlers - no need to override these
923
924 def handle_VER(self, params):
925 versions = params[1:]
926 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
927 self.transport.loseConnection()
928 raise MSNProtocolError, "Invalid version response"
929 self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
930
931 def handle_CVR(self, params):
932 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
933
934 def handle_USR(self, params):
935 if not (4 <= len(params) <= 6):
936 raise MSNProtocolError, "Invalid Number of Parameters for USR"
937
938 mechanism = params[1]
939 if mechanism == "OK":
940 self.loggedIn(params[2], int(params[3]))
941 elif params[2].upper() == "S":
942 # we need to obtain auth from a passport server
943 f = self.factory
944 d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
945 d.addCallback(self._passportLogin)
946 d.addErrback(self._passportError)
947
948 def _passportLogin(self, result):
949 if result[0] == LOGIN_REDIRECT:
950 d = _login(self.factory.userHandle, self.factory.password,
951 result[1], cached=1, authData=result[2])
952 d.addCallback(self._passportLogin)
953 d.addErrback(self._passportError)
954 elif result[0] == LOGIN_SUCCESS:
955 self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
956 elif result[0] == LOGIN_FAILURE:
957 self.loginFailure(result[1])
958
959 def _passportError(self, failure):
960 self.loginFailure("Exception while authenticating: %s" % failure)
961
962 def handle_CHG(self, params):
963 id = int(params[0])
964 if not self._fireCallback(id, params[1]):
965 self.factory.status = statusCode
966 self.statusChanged(params[1])
967
968 def handle_ILN(self, params):
969 #checkParamLen(len(params), 6, 'ILN')
970 msnContact = self.factory.contacts.getContact(params[2])
971 if not msnContact: return
972 msnContact.status = params[1]
973 msnContact.screenName = unquote(params[3])
974 if len(params) > 4: msnContact.caps = int(params[4])
975 if len(params) > 5: self.handleAvatarHelper(msnContact, params[5])
976 self.contactStatusChanged(params[1], params[2], unquote(params[3]))
977
978 def handleAvatarHelper(self, msnContact, msnobjStr):
979 s = unquote(msnobjStr)
980 msnobj = msnp2p.MSNOBJ()
981 msnobj.parse(s)
982 if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
983 if msnp2p.MSNP2P_DEBUG: log.msg("Updated MSNOBJ received!" + msnobjStr)
984 msnContact.msnobj = msnobj
985 msnContact.msnobjGot = False
986 self.avatarHashChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
987
988 def handle_CHL(self, params):
989 checkParamLen(len(params), 2, 'CHL')
990 response = msnp11chl.doChallenge(params[1])
991 self.sendLine("QRY %s %s %s" % (self._nextTransactionID(), msnp11chl.MSNP11_PRODUCT_ID, len(response)))
992 self.transport.write(response)
993
994 def handle_QRY(self, params):
995 pass
996
997 def handle_NLN(self, params):
998 if not self.factory: return
999 msnContact = self.factory.contacts.getContact(params[1])
1000 if not msnContact: return
1001 msnContact.status = params[0]
1002 msnContact.screenName = unquote(params[2])
1003 if len(params) > 3: msnContact.caps = int(params[3])
1004 if len(params) > 4: self.handleAvatarHelper(msnContact, params[4])
1005 self.contactStatusChanged(params[0], params[1], unquote(params[2]))
1006
1007 def handle_FLN(self, params):
1008 checkParamLen(len(params), 1, 'FLN')
1009 msnContact = self.factory.contacts.getContact(userHandle)
1010 if msnContact:
1011 msnContact.status = STATUS_OFFLINE
1012 self.contactOffline(params[0])
1013
1014 def handle_LST(self, params):
1015 if self._getState() != 'SYNC': return
1016 userHandle = ""
1017 screenName = ""
1018 userGuid = ""
1019 lists = -1
1020 groups = []
1021 for p in params:
1022 if p[0] == 'N':
1023 userHandle = getVal(p)
1024 elif p[0] == 'F':
1025 screenName = getVal(p)
1026 elif p[0] == 'C':
1027 userGuid = getVal(p)
1028 elif p.isdigit():
1029 lists = int(p)
1030 else: # Must be the groups
1031 try:
1032 groups = p.split(',')
1033 except:
1034 raise MSNProtocolError, "Unknown LST " + str(params) # debug
1035
1036 if not userHandle or lists < 1:
1037 raise MSNProtocolError, "Unknown LST " + str(params) # debug
1038 contact = MSNContact(userGuid, userHandle, screenName, lists)
1039 if contact.lists & FORWARD_LIST:
1040 contact.groups.extend(map(str, groups))
1041 self._getStateData('list').addContact(contact)
1042 self._setStateData('last_contact', contact)
1043 sofar = self._getStateData('lst_sofar') + 1
1044 if sofar == self._getStateData('lst_reply'):
1045 # this is the best place to determine that
1046 # a syn realy has finished - msn _may_ send
1047 # BPR information for the last contact
1048 # which is unfortunate because it means
1049 # that the real end of a syn is non-deterministic.
1050 # to handle this we'll keep 'last_contact' hanging
1051 # around in the state data and update it if we need
1052 # to later.
1053 self._setState('SESSION')
1054 contacts = self._getStateData('list')
1055 phone = self._getStateData('phone')
1056 id = self._getStateData('synid')
1057 self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1058 self._fireCallback(id, contacts, phone)
1059 else:
1060 self._setStateData('lst_sofar',sofar)
1061
1062 def handle_BLP(self, params):
1063 # check to see if this is in response to a SYN
1064 if self._getState() == 'SYNC':
1065 self._getStateData('list').privacy = listCodeToID[params[0].lower()]
1066 else:
1067 id = int(params[0])
1068 self._fireCallback(id, listCodeToID[params[1].lower()])
1069
1070 def handle_GTC(self, params):
1071 # check to see if this is in response to a SYN
1072 if self._getState() == 'SYNC':
1073 if params[0].lower() == "a": self._getStateData('list').autoAdd = 0
1074 elif params[0].lower() == "n": self._getStateData('list').autoAdd = 1
1075 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1076 else:
1077 id = int(params[0])
1078 if params[1].lower() == "a": self._fireCallback(id, 0)
1079 elif params[1].lower() == "n": self._fireCallback(id, 1)
1080 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1081
1082 def handle_SYN(self, params):
1083 id = int(params[0])
1084 self._setStateData('phone', []) # Always needs to be set
1085 if params[3] == 0: # No LST will be received. New account?
1086 self._setState('SESSION')
1087 self._fireCallback(id, None, None)
1088 else:
1089 contacts = MSNContactList()
1090 self._setStateData('list', contacts)
1091 self._setStateData('lst_reply', int(params[3]))
1092 self._setStateData('lsg_reply', int(params[4]))
1093 self._setStateData('lst_sofar', 0)
1094
1095 def handle_LSG(self, params):
1096 if self._getState() == 'SYNC':
1097 self._getStateData('list').groups[params[1]] = unquote(params[0])
1098
1099 def handle_PRP(self, params):
1100 if params[1] == "MFN":
1101 self._fireCallback(int(params[0]), unquote(params[2]))
1102 elif self._getState() == 'SYNC':
1103 self._getStateData('phone').append((params[0], unquote(params[1])))
1104 else:
1105 self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
1106
1107 def handle_BPR(self, params):
1108 if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
1109 numParams = len(params)
1110 if numParams == 2: # part of a syn
1111 self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
1112 elif numParams == 4:
1113 self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1114 self.gotPhoneNumber(params[1], params[2], unquote(params[3]))
1115
1116
1117 def handle_ADG(self, params):
1118 checkParamLen(len(params), 5, 'ADG')
1119 id = int(params[0])
1120 if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
1121 raise MSNProtocolError, "ADG response does not match up to a request" # debug
1122
1123 def handle_RMG(self, params):
1124 checkParamLen(len(params), 3, 'RMG')
1125 id = int(params[0])
1126 if not self._fireCallback(id, int(params[1]), int(params[2])):
1127 raise MSNProtocolError, "RMG response does not match up to a request" # debug
1128
1129 def handle_REG(self, params):
1130 checkParamLen(len(params), 5, 'REG')
1131 id = int(params[0])
1132 if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
1133 raise MSNProtocolError, "REG response does not match up to a request" # debug
1134
1135 def handle_ADC(self, params):
1136 if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list"
1137 numParams = len(params)
1138 if numParams < 4 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
1139 raise MSNProtocolError, "Invalid Paramaters for ADC" # debug
1140 id = int(params[0])
1141 listType = params[1].lower()
1142 userHandle = getVal(params[2])
1143 screenName = unquote(getVal(params[3]))
1144 userGuid = None
1145 if len(params) >= 5: userGuid = getVal(params[4])
1146 # if numParams == 6: # they sent a group id
1147 # if params[1].upper() != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
1148 # groupID = int(params[5])
1149 if not self._fireCallback(id, listCodeToID[listType], userGuid, userHandle, screenName):
1150 c = self.factory.contacts.getContact(userHandle)
1151 if not c:
1152 c = MSNContact(userGuid=userGuid, userHandle=userHandle, screenName=screenName)
1153 self.factory.contacts.addContact(c)
1154 c.addToList(PENDING_LIST)
1155 self.userAddedMe(userGuid, userHandle, screenName)
1156
1157 def handle_REM(self, params):
1158 if not self.factory.contacts: raise MSNProtocolError, "handle_REM called with no contact list available!"
1159 numParams = len(params)
1160 if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
1161 raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1162 id = int(params[0])
1163 listType = params[1].lower()
1164 userHandle = params[3]
1165 groupID = None
1166 if numParams == 5:
1167 if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
1168 groupID = int(params[4])
1169 if not self._fireCallback(id, listCodeToID[listType], userHandle, groupID):
1170 if listType.upper() != "RL": return
1171 c = self.factory.contacts.getContact(userHandle)
1172 if not c: return
1173 c.removeFromList(REVERSE_LIST)
1174 if c.lists == 0: self.factory.contacts.remContact(c.userHandle)
1175 self.userRemovedMe(userHandle)
1176
1177 def handle_XFR(self, params):
1178 checkParamLen(len(params), 5, 'XFR')
1179 id = int(params[0])
1180 # check to see if they sent a host/port pair
1181 try:
1182 host, port = params[2].split(':')
1183 except ValueError:
1184 host = params[2]
1185 port = MSN_PORT
1186
1187 if not self._fireCallback(id, host, int(port), params[4]):
1188 raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1189
1190 def handle_RNG(self, params):
1191 checkParamLen(len(params), 6, 'RNG')
1192 # check for host:port pair
1193 try:
1194 host, port = params[1].split(":")
1195 port = int(port)
1196 except ValueError:
1197 host = params[1]
1198 port = MSN_PORT
1199 self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1200 unquote(params[5]))
1201
1202 def handle_NOT(self, params):
1203 checkParamLen(len(params), 1, 'NOT')
1204 try:
1205 messageLen = int(params[0])
1206 except ValueError: raise MSNProtocolError, "Invalid Parameter for NOT length argument"
1207 self.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", specialMessage=True)
1208 self.setRawMode()
1209
1210 def handle_UBX(self, params):
1211 checkParamLen(len(params), 2, 'UBX')
1212 try:
1213 messageLen = int(params[1])
1214 except ValueError: raise MSNProtocolError, "Invalid Parameter for UBX length argument"
1215 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
1216 self.setRawMode()
1217
1218 def handle_UUX(self, params):
1219 checkParamLen(len(params), 2, 'UUX')
1220 if params[1] != '0': return
1221 id = int(params[0])
1222 self._fireCallback(id)
1223
1224 def handle_OUT(self, params):
1225 checkParamLen(len(params), 1, 'OUT')
1226 if params[0] == "OTH": self.multipleLogin()
1227 elif params[0] == "SSD": self.serverGoingDown()
1228 else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1229
1230 def handle_QNG(self, params):
1231 self.pingCounter = 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1232
1233 # callbacks
1234
1235 def pingChecker(self):
1236 if self.pingCounter > 5:
1237 # The server has ignored 5 pings, lets kill the connection
1238 self.transport.loseConnection()
1239 else:
1240 self.sendLine("PNG")
1241 self.pingCounter += 1
1242
1243 def pingCheckerStart(self, *args):
1244 self.pingCheckTask = task.LoopingCall(self.pingChecker)
1245 self.pingCheckTask.start(PINGSPEED)
1246
1247 def loggedIn(self, userHandle, verified):
1248 """
1249 Called when the client has logged in.
1250 The default behaviour of this method is to
1251 update the factory with our screenName and
1252 to sync the contact list (factory.contacts).
1253 When this is complete self.listSynchronized
1254 will be called.
1255
1256 @param userHandle: our userHandle
1257 @param verified: 1 if our passport has been (verified), 0 if not.
1258 (i'm not sure of the significace of this)
1259 @type verified: int
1260 """
1261 d = self.syncList()
1262 d.addCallback(self.listSynchronized)
1263 d.addCallback(self.pingCheckerStart)
1264
1265 def loginFailure(self, message):
1266 """
1267 Called when the client fails to login.
1268
1269 @param message: a message indicating the problem that was encountered
1270 """
1271 pass
1272
1273 def gotProfile(self, message):
1274 """
1275 Called after logging in when the server sends an initial
1276 message with MSN/passport specific profile information
1277 such as country, number of kids, etc.
1278 Check the message headers for the specific values.
1279
1280 @param message: The profile message
1281 """
1282 pass
1283
1284 def listSynchronized(self, *args):
1285 """
1286 Lists are now synchronized by default upon logging in, this
1287 method is called after the synchronization has finished
1288 and the factory now has the up-to-date contacts.
1289 """
1290 pass
1291
1292 def avatarHashChanged(self, userHandle, hash):
1293 """
1294 Called when we receive the first, or a new <msnobj/> from a
1295 contact.
1296
1297 @param userHandle: contact who's msnobj has been changed
1298 @param hash: sha1 hash of their avatar
1299 """
1300
1301 def statusChanged(self, statusCode):
1302 """
1303 Called when our status changes and it isn't in response to
1304 a client command.
1305
1306 @param statusCode: 3-letter status code
1307 """
1308 pass
1309
1310 def contactStatusChanged(self, statusCode, userHandle, screenName):
1311 """
1312 Called when we're notified that a contact's status has changed.
1313 By default we will update the status attribute and screenName
1314 of the contact stored on the factory.
1315
1316 @param statusCode: 3-letter status code
1317 @param userHandle: the contact's user handle (passport)
1318 @param screenName: the contact's screen name
1319 """
1320 pass
1321
1322 def contactPersonalChanged(self, userHandle, personal):
1323 """
1324 Called when a contact's personal message changes.
1325
1326 @param userHandle: the contact who changed their personal message
1327 @param personal : the new personal message
1328 """
1329 pass
1330
1331 def contactOffline(self, userHandle):
1332 """
1333 Called when a contact goes offline.
1334
1335 @param userHandle: the contact's user handle
1336 """
1337 pass
1338
1339 def gotMessage(self, message):
1340 """
1341 Called when there is a message from the notification server
1342 that is not understood by default.
1343
1344 @param message: the MSNMessage.
1345 """
1346 pass
1347
1348 def gotMSNAlert(self, body, action, subscr):
1349 """
1350 Called when the server sends an MSN Alert (http://alerts.msn.com)
1351
1352 @param body : the alert text
1353 @param action: a URL with more information for the user to view
1354 @param subscr: a URL the user can use to modify their alert subscription
1355 """
1356 pass
1357
1358 def gotInitialEmailNotification(self, inboxunread, foldersunread):
1359 """
1360 Called when the server sends you details about your hotmail
1361 inbox. This is only ever called once, on login.
1362
1363 @param inboxunread : the number of unread items in your inbox
1364 @param foldersunread: the number of unread items in other folders
1365 """
1366 pass
1367
1368 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
1369 """
1370 Called when the server sends us realtime email
1371 notification. This means that you have received
1372 a new email in your hotmail inbox.
1373
1374 @param mailfrom: the sender of the email
1375 @param fromaddr: the sender of the email (I don't know :P)
1376 @param subject : the email subject
1377 """
1378 pass
1379
1380 def gotPhoneNumber(self, userHandle, phoneType, number):
1381 """
1382 Called when the server sends us phone details about
1383 a specific user (for example after a user is added
1384 the server will send their status, phone details etc.
1385
1386 @param userHandle: the contact's user handle (passport)
1387 @param phoneType: the specific phoneType
1388 (*_PHONE constants or HAS_PAGER)
1389 @param number: the value/phone number.
1390 """
1391 pass
1392
1393 def userAddedMe(self, userGuid, userHandle, screenName):
1394 """
1395 Called when a user adds me to their list. (ie. they have been added to
1396 the reverse list.
1397
1398 @param userHandle: the userHandle of the user
1399 @param screenName: the screen name of the user
1400 """
1401 pass
1402
1403 def userRemovedMe(self, userHandle):
1404 """
1405 Called when a user removes us from their contact list
1406 (they are no longer on our reverseContacts list.
1407
1408 @param userHandle: the contact's user handle (passport)
1409 """
1410 pass
1411
1412 def gotSwitchboardInvitation(self, sessionID, host, port,
1413 key, userHandle, screenName):
1414 """
1415 Called when we get an invitation to a switchboard server.
1416 This happens when a user requests a chat session with us.
1417
1418 @param sessionID: session ID number, must be remembered for logging in
1419 @param host: the hostname of the switchboard server
1420 @param port: the port to connect to
1421 @param key: used for authorization when connecting
1422 @param userHandle: the user handle of the person who invited us
1423 @param screenName: the screen name of the person who invited us
1424 """
1425 pass
1426
1427 def multipleLogin(self):
1428 """
1429 Called when the server says there has been another login
1430 under our account, the server should disconnect us right away.
1431 """
1432 pass
1433
1434 def serverGoingDown(self):
1435 """
1436 Called when the server has notified us that it is going down for
1437 maintenance.
1438 """
1439 pass
1440
1441 # api calls
1442
1443 def changeStatus(self, status):
1444 """
1445 Change my current status. This method will add
1446 a default callback to the returned Deferred
1447 which will update the status attribute of the
1448 factory.
1449
1450 @param status: 3-letter status code (as defined by
1451 the STATUS_* constants)
1452 @return: A Deferred, the callback of which will be
1453 fired when the server confirms the change
1454 of status. The callback argument will be
1455 a tuple with the new status code as the
1456 only element.
1457 """
1458
1459 id, d = self._createIDMapping()
1460 self.sendLine("CHG %s %s %s %s" % (id, status, str(MSNContact.MSNC1 | MSNContact.MSNC2 | MSNContact.MSNC3 | MSNContact.MSNC4), quote(self.msnobj.text)))
1461 def _cb(r):
1462 if self.factory: self.factory.status = r[0]
1463 return r
1464 return d.addCallback(_cb)
1465
1466 def setPrivacyMode(self, privLevel):
1467 """
1468 Set my privacy mode on the server.
1469
1470 B{Note}:
1471 This only keeps the current privacy setting on
1472 the server for later retrieval, it does not
1473 effect the way the server works at all.
1474
1475 @param privLevel: This parameter can be true, in which
1476 case the server will keep the state as
1477 'al' which the official client interprets
1478 as -> allow messages from only users on
1479 the allow list. Alternatively it can be
1480 false, in which case the server will keep
1481 the state as 'bl' which the official client
1482 interprets as -> allow messages from all
1483 users except those on the block list.
1484
1485 @return: A Deferred, the callback of which will be fired when
1486 the server replies with the new privacy setting.
1487 The callback argument will be a tuple, the 2 elements
1488 of which being the list version and either 'al'
1489 or 'bl' (the new privacy setting).
1490 """
1491
1492 id, d = self._createIDMapping()
1493 if privLevel: self.sendLine("BLP %s AL" % id)
1494 else: self.sendLine("BLP %s BL" % id)
1495 return d
1496
1497 def syncList(self):
1498 """
1499 Used for keeping an up-to-date contact list.
1500 A callback is added to the returned Deferred
1501 that updates the contact list on the factory
1502 and also sets my state to STATUS_ONLINE.
1503
1504 B{Note}:
1505 This is called automatically upon signing
1506 in using the version attribute of
1507 factory.contacts, so you may want to persist
1508 this object accordingly. Because of this there
1509 is no real need to ever call this method
1510 directly.
1511
1512 @return: A Deferred, the callback of which will be
1513 fired when the server sends an adequate reply.
1514 The callback argument will be a tuple with two
1515 elements, the new list (MSNContactList) and
1516 your current state (a dictionary). If the version
1517 you sent _was_ the latest list version, both elements
1518 will be None. To just request the list send a version of 0.
1519 """
1520
1521 self._setState('SYNC')
1522 id, d = self._createIDMapping(data=None)
1523 self._setStateData('synid',id)
1524 self.sendLine("SYN %s %s %s" % (id, 0, 0))
1525 def _cb(r):
1526 self.changeStatus(STATUS_ONLINE)
1527 if r[0] is not None:
1528 self.factory.contacts = r[0]
1529 return r
1530 return d.addCallback(_cb)
1531
1532 def setPhoneDetails(self, phoneType, value):
1533 """
1534 Set/change my phone numbers stored on the server.
1535
1536 @param phoneType: phoneType can be one of the following
1537 constants - HOME_PHONE, WORK_PHONE,
1538 MOBILE_PHONE, HAS_PAGER.
1539 These are pretty self-explanatory, except
1540 maybe HAS_PAGER which refers to whether or
1541 not you have a pager.
1542 @param value: for all of the *_PHONE constants the value is a
1543 phone number (str), for HAS_PAGER accepted values
1544 are 'Y' (for yes) and 'N' (for no).
1545
1546 @return: A Deferred, the callback for which will be fired when
1547 the server confirms the change has been made. The
1548 callback argument will be a tuple with 2 elements, the
1549 first being the new list version (int) and the second
1550 being the new phone number value (str).
1551 """
1552 raise "ProbablyDoesntWork"
1553 # XXX: Add a default callback which updates
1554 # factory.contacts.version and the relevant phone
1555 # number
1556 id, d = self._createIDMapping()
1557 self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1558 return d
1559
1560 def addListGroup(self, name):
1561 """
1562 Used to create a new list group.
1563 A default callback is added to the
1564 returned Deferred which updates the
1565 contacts attribute of the factory.
1566
1567 @param name: The desired name of the new group.
1568
1569 @return: A Deferred, the callbacck for which will be called
1570 when the server clarifies that the new group has been
1571 created. The callback argument will be a tuple with 3
1572 elements: the new list version (int), the new group name
1573 (str) and the new group ID (int).
1574 """
1575
1576 raise "ProbablyDoesntWork"
1577 id, d = self._createIDMapping()
1578 self.sendLine("ADG %s %s 0" % (id, quote(name)))
1579 def _cb(r):
1580 if self.factory.contacts:
1581 self.factory.contacts.version = r[0]
1582 self.factory.contacts.setGroup(r[1], r[2])
1583 return r
1584 return d.addCallback(_cb)
1585
1586 def remListGroup(self, groupID):
1587 """
1588 Used to remove a list group.
1589 A default callback is added to the
1590 returned Deferred which updates the
1591 contacts attribute of the factory.
1592
1593 @param groupID: the ID of the desired group to be removed.
1594
1595 @return: A Deferred, the callback for which will be called when
1596 the server clarifies the deletion of the group.
1597 The callback argument will be a tuple with 2 elements:
1598 the new list version (int) and the group ID (int) of
1599 the removed group.
1600 """
1601
1602 raise "ProbablyDoesntWork"
1603 id, d = self._createIDMapping()
1604 self.sendLine("RMG %s %s" % (id, groupID))
1605 def _cb(r):
1606 self.factory.contacts.version = r[0]
1607 self.factory.contacts.remGroup(r[1])
1608 return r
1609 return d.addCallback(_cb)
1610
1611 def renameListGroup(self, groupID, newName):
1612 """
1613 Used to rename an existing list group.
1614 A default callback is added to the returned
1615 Deferred which updates the contacts attribute
1616 of the factory.
1617
1618 @param groupID: the ID of the desired group to rename.
1619 @param newName: the desired new name for the group.
1620
1621 @return: A Deferred, the callback for which will be called
1622 when the server clarifies the renaming.
1623 The callback argument will be a tuple of 3 elements,
1624 the new list version (int), the group id (int) and
1625 the new group name (str).
1626 """
1627
1628 raise "ProbablyDoesntWork"
1629 id, d = self._createIDMapping()
1630 self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1631 def _cb(r):
1632 self.factory.contacts.version = r[0]
1633 self.factory.contacts.setGroup(r[1], r[2])
1634 return r
1635 return d.addCallback(_cb)
1636
1637 def addContact(self, listType, userHandle, groupID=""):
1638 """
1639 Used to add a contact to the desired list.
1640 A default callback is added to the returned
1641 Deferred which updates the contacts attribute of
1642 the factory with the new contact information.
1643 If you are adding a contact to the forward list
1644 and you want to associate this contact with multiple
1645 groups then you will need to call this method for each
1646 group you would like to add them to, changing the groupID
1647 parameter. The default callback will take care of updating
1648 the group information on the factory's contact list.
1649
1650 @param listType: (as defined by the *_LIST constants)
1651 @param userHandle: the user handle (passport) of the contact
1652 that is being added
1653 @param groupID: the group ID for which to associate this contact
1654 with. (default 0 - default group). Groups are only
1655 valid for FORWARD_LIST.
1656
1657 @return: A Deferred, the callback for which will be called when
1658 the server has clarified that the user has been added.
1659 The callback argument will be a tuple with 4 elements:
1660 the list type, the contact's user handle, the new list
1661 version, and the group id (if relevant, otherwise it
1662 will be None)
1663 """
1664
1665 id, d = self._createIDMapping()
1666 try: # Make sure the contact isn't actually on the list
1667 if self.factory.contacts.getContact(userHandle).lists & listType: return
1668 except AttributeError: pass
1669 listType = listIDToCode[listType].upper()
1670 if listType == "FL":
1671 self.sendLine("ADC %s %s N=%s F=%s %s" % (id, listType, userHandle, userHandle, groupID))
1672 else:
1673 self.sendLine("ADC %s %s N=%s" % (id, listType, userHandle))
1674
1675 def _cb(r):
1676 if not self.factory: return
1677 c = self.factory.contacts.getContact(r[2])
1678 if not c:
1679 c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
1680 #if r[3]: c.groups.append(r[3])
1681 c.addToList(r[0])
1682 return r
1683 return d.addCallback(_cb)
1684
1685 def remContact(self, listType, userHandle):
1686 """
1687 Used to remove a contact from the desired list.
1688 A default callback is added to the returned deferred
1689 which updates the contacts attribute of the factory
1690 to reflect the new contact information.
1691
1692 @param listType: (as defined by the *_LIST constants)
1693 @param userHandle: the user handle (passport) of the
1694 contact being removed
1695
1696 @return: A Deferred, the callback for which will be called when
1697 the server has clarified that the user has been removed.
1698 The callback argument will be a tuple of 4 elements:
1699 the list type, the contact's user handle, the new list
1700 version, and the group id (if relevant, otherwise it will
1701 be None)
1702 """
1703
1704 id, d = self._createIDMapping()
1705 try: # Make sure the contact is actually on this list
1706 if not (self.factory.contacts.getContact(userHandle).lists & listType): return
1707 except AttributeError: return
1708 listType = listIDToCode[listType].upper()
1709 if listType == "FL":
1710 try:
1711 c = self.factory.contacts.getContact(userHandle)
1712 userGuid = c.userGuid
1713 except AttributeError: return
1714 self.sendLine("REM %s FL %s" % (id, userGuid))
1715 else:
1716 self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1717
1718 def _cb(r):
1719 l = self.factory.contacts
1720 l.version = r[2]
1721 c = l.getContact(r[1])
1722 if not c: return
1723 group = r[3]
1724 shouldRemove = 1
1725 if group: # they may not have been removed from the list
1726 c.groups.remove(group)
1727 if c.groups: shouldRemove = 0
1728 if shouldRemove:
1729 c.removeFromList(r[0])
1730 if c.lists == 0: l.remContact(c.userHandle)
1731 return r
1732 return d.addCallback(_cb)
1733
1734 def changeScreenName(self, newName):
1735 """
1736 Used to change your current screen name.
1737 A default callback is added to the returned
1738 Deferred which updates the screenName attribute
1739 of the factory and also updates the contact list
1740 version.
1741
1742 @param newName: the new screen name
1743
1744 @return: A Deferred, the callback for which will be called
1745 when the server sends an adequate reply.
1746 The callback argument will be a tuple of 2 elements:
1747 the new list version and the new screen name.
1748 """
1749
1750 id, d = self._createIDMapping()
1751 self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
1752 def _cb(r):
1753 self.factory.screenName = r[0]
1754 return r
1755 return d.addCallback(_cb)
1756
1757 def changePersonalMessage(self, personal):
1758 id, d = self._createIDMapping()
1759 data = ""
1760 if personal:
1761 data = "<Data><PSM>" + personal + "</PSM><CurrentMedia></CurrentMedia></Data>"
1762 self.sendLine("UUX %s %s" % (id, len(data)))
1763 self.transport.write(data)
1764 def _cb(r):
1765 self.factory.personal = personal
1766 return d.addCallback(_cb)
1767
1768 def changeAvatar(self, imageData, push):
1769 if self.msnobj and imageData == self.msnobj.imageData: return
1770 if imageData:
1771 self.msnobj.setData(self.factory.userHandle, imageData)
1772 else:
1773 self.msnobj.setNull()
1774 if push: self.changeStatus(self.factory.status) # Push to server
1775
1776
1777 def requestSwitchboardServer(self):
1778 """
1779 Used to request a switchboard server to use for conversations.
1780
1781 @return: A Deferred, the callback for which will be called when
1782 the server responds with the switchboard information.
1783 The callback argument will be a tuple with 3 elements:
1784 the host of the switchboard server, the port and a key
1785 used for logging in.
1786 """
1787
1788 id, d = self._createIDMapping()
1789 self.sendLine("XFR %s SB" % id)
1790 return d
1791
1792 def logOut(self):
1793 """
1794 Used to log out of the notification server.
1795 After running the method the server is expected
1796 to close the connection.
1797 """
1798
1799 if self.pingCheckTask:
1800 self.pingCheckTask.stop()
1801 self.pingCheckTask = None
1802 self.sendLine("OUT")
1803
1804 class NotificationFactory(ClientFactory):
1805 """
1806 Factory for the NotificationClient protocol.
1807 This is basically responsible for keeping
1808 the state of the client and thus should be used
1809 in a 1:1 situation with clients.
1810
1811 @ivar contacts: An MSNContactList instance reflecting
1812 the current contact list -- this is
1813 generally kept up to date by the default
1814 command handlers.
1815 @ivar userHandle: The client's userHandle, this is expected
1816 to be set by the client and is used by the
1817 protocol (for logging in etc).
1818 @ivar screenName: The client's current screen-name -- this is
1819 generally kept up to date by the default
1820 command handlers.
1821 @ivar password: The client's password -- this is (obviously)
1822 expected to be set by the client.
1823 @ivar passportServer: This must point to an msn passport server
1824 (the whole URL is required)
1825 @ivar status: The status of the client -- this is generally kept
1826 up to date by the default command handlers
1827 """
1828
1829 contacts = None
1830 userHandle = ''
1831 screenName = ''
1832 password = ''
1833 passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1834 status = 'FLN'
1835 protocol = NotificationClient
1836
1837
1838 # XXX: A lot of the state currently kept in
1839 # instances of SwitchboardClient is likely to
1840 # be moved into a factory at some stage in the
1841 # future
1842
1843 class SwitchboardClient(MSNEventBase):
1844 """
1845 This class provides support for clients connecting to a switchboard server.
1846
1847 Switchboard servers are used for conversations with other people
1848 on the MSN network. This means that the number of conversations at
1849 any given time will be directly proportional to the number of
1850 connections to varioius switchboard servers.
1851
1852 MSN makes no distinction between single and group conversations,
1853 so any number of users may be invited to join a specific conversation
1854 taking place on a switchboard server.
1855
1856 @ivar key: authorization key, obtained when receiving
1857 invitation / requesting switchboard server.
1858 @ivar userHandle: your user handle (passport)
1859 @ivar sessionID: unique session ID, used if you are replying
1860 to a switchboard invitation
1861 @ivar reply: set this to 1 in connectionMade or before to signifiy
1862 that you are replying to a switchboard invitation.
1863 """
1864
1865 key = 0
1866 userHandle = ""
1867 sessionID = ""
1868 reply = 0
1869
1870 _iCookie = 0
1871
1872 def __init__(self, msnobj=None):
1873 MSNEventBase.__init__(self)
1874 self.pendingUsers = {}
1875 self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
1876 self.slpLinks = {}
1877 self.msnobj = msnobj
1878
1879 def connectionMade(self):
1880 MSNEventBase.connectionMade(self)
1881 self._sendInit()
1882
1883 def connectionLost(self, reason):
1884 self.cookies['iCookies'] = {}
1885 MSNEventBase.connectionLost(self, reason)
1886
1887 def _sendInit(self):
1888 """
1889 send initial data based on whether we are replying to an invitation
1890 or starting one.
1891 """
1892 id = self._nextTransactionID()
1893 if not self.reply:
1894 self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
1895 else:
1896 self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
1897
1898 def _newInvitationCookie(self):
1899 self._iCookie += 1
1900 if self._iCookie > 1000: self._iCookie = 1
1901 return self._iCookie
1902
1903 def _checkTyping(self, message, cTypes):
1904 """ helper method for checkMessage """
1905 if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
1906 self.userTyping(message)
1907 return 1
1908
1909 def _checkFileInvitation(self, message, info):
1910 """ helper method for checkMessage """
1911 if not info.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID: return 0
1912 try:
1913 cookie = info['Invitation-Cookie']
1914 filename = info['Application-File']
1915 filesize = int(info['Application-FileSize'])
1916 connectivity = (info.get('Connectivity').lower() == 'y')
1917 except KeyError:
1918 log.msg('Received munged file transfer request ... ignoring.')
1919 return 0
1920 import msnft
1921 self.gotSendRequest(msnft.MSNFTP_Receive(filename, filesize, message.userHandle, cookie, connectivity, self))
1922 return 1
1923
1924 def _checkP2PMessage(self, message, cTypes):
1925 """ helper method for msnslp messages (file transfer & avatars) """
1926 packet = message.message
1927 binaryFields = msnp2p.BinaryFields(packet=packet)
1928 if binaryFields[0] != 0:
1929 slpLink = self.slpLinks[binaryFields[0]]
1930 if slpLink.remoteUser == message.userHandle:
1931 slpLink.handlePacket(packet)
1932 elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
1933 pass # Ignore the ACKs
1934 else:
1935 slpMessage = msnp2p.MSNSLPMessage(packet)
1936 slpLink = None
1937 if slpMessage.method == "INVITE":
1938 if slpMessage.euf_guid == MSN_MSNFTP_GUID:
1939 slpLink = msnp2p.SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
1940 context = msnp2p.FileContext(slpMessage.context)
1941 fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro, self)
1942 elif slpMessage.euf_guid == MSN_AVATAR_GUID:
1943 slpLink = msnp2p.SLPLink_Send(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
1944 self._sendMSNSLPResponse(slpLink, "200 OK")
1945 if slpLink:
1946 self.slpLinks[slpMessage.sessionID] = slpLink
1947 else:
1948 if slpMessage.status != "200":
1949 for slpLink in self.slpLinks:
1950 if slpLink.sessionGuid == slpMessage.sessionGuid:
1951 del self.slpLinks[slpLink.sessionID]
1952 if slpMessage.method != "BYE":
1953 # Must be an error. If its a file transfer we need to signal that it failed
1954 slpLink.transferError()
1955 else:
1956 slpLink = self.slpLinks[slpMessage.sessionID]
1957 slpLink.transferReady()
1958 if slpLink:
1959 # Always need to ACK these packets if we can
1960 self._sendP2PACK(self, slpLink, binaryHeaders)
1961
1962 return 1
1963
1964 def _sendP2PACK(self, slpLink, ackHeaders):
1965 binaryFields = msnp2p.BinaryFields()
1966 binaryFields[1] = slpLink.nextBaseID()
1967 binaryFields[3] = ackHeaders[3]
1968 binaryFields[5] = BinaryFields.ACK
1969 binaryFields[6] = ackHeaders[1]
1970 binaryFields[7] = ackHeaders[6]
1971 binaryFields[8] = ackHeaders[3]
1972 self._sendP2PMessage(binaryFields, "")
1973
1974 def _sendMSNSLPInvite(self, slpLink, guid, context):
1975 msg = msnp2p.MSNSLP_Message()
1976 msg.create(method="INVITE", to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
1977 msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=msnp2p.b64enc(context))
1978 self._sendMSNSLPMessage(slpLink, msg)
1979
1980 def _sendMSNSLPResponse(self, slpLink, response):
1981 msg = msnp2p.MSNSLPMessage()
1982 msg.create(status=response, to=slpLink.remoteUser, fro=self.userHandle, cseq=1, sessionGuid=slpLink.sessionGuid)
1983 msg.setData(sessionID=slpLink.sessionID)
1984 self._sendMSNSLPMessage(slpLink, msg)
1985
1986 def _sendMSNSLPMessage(self, slpLink, msnSlpMessage):
1987 msgStr = str(msg)
1988 binaryFields = msnp2p.BinaryFields()
1989 binaryFields[1] = slpLink.nextBaseID()
1990 binaryFields[3] = len(msgStr)
1991 binaryFields[4] = binaryFields[3]
1992 binaryFields[6] = random.randint(0, sys.maxint)
1993 self._sendP2PMessage(binaryFields, msgStr)
1994
1995 def _sendP2PMessage(self, binaryFields, msgStr):
1996 packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
1997
1998 message = MSNMessage(message=packet)
1999 message.setHeader("Content-Type", "application/x-msnmsgrp2p")
2000 message.setHeader("P2P-Dest", handler.to)
2001 message.ack = MSNMessage.MESSAGE_ACK_FAT
2002 self.sendMessage(message)
2003
2004 def checkMessage(self, message):
2005 """
2006 hook for detecting any notification type messages
2007 (e.g. file transfer)
2008 """
2009 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
2010 if self._checkTyping(message, cTypes): return 0
2011 if 'text/x-msmsgsinvite' in cTypes:
2012 # header like info is sent as part of the message body.
2013 info = {}
2014 for line in message.message.split('\r\n'):
2015 try:
2016 key, val = line.split(':')
2017 info[key] = val.lstrip()
2018 except ValueError: continue
2019 if self._checkFileInvitation(message, info): return 0
2020 if 'application/x-msnmsgrp2p' in cTypes:
2021 if self._checkP2PMessage(message, cTypes): return 0
2022 return 1
2023
2024 # negotiation
2025 def handle_USR(self, params):
2026 checkParamLen(len(params), 4, 'USR')
2027 if params[1] == "OK":
2028 self.loggedIn()
2029
2030 # invite a user
2031 def handle_CAL(self, params):
2032 checkParamLen(len(params), 3, 'CAL')
2033 id = int(params[0])
2034 if params[1].upper() == "RINGING":
2035 self._fireCallback(id, int(params[2])) # session ID as parameter
2036
2037 # user joined
2038 def handle_JOI(self, params):
2039 checkParamLen(len(params), 2, 'JOI')
2040 self.userJoined(params[0], unquote(params[1]))
2041
2042 # users participating in the current chat
2043 def handle_IRO(self, params):
2044 checkParamLen(len(params), 5, 'IRO')
2045 self.pendingUsers[params[3]] = unquote(params[4])
2046 if params[1] == params[2]:
2047 self.gotChattingUsers(self.pendingUsers)
2048 self.pendingUsers = {}
2049
2050 # finished listing users
2051 def handle_ANS(self, params):
2052 checkParamLen(len(params), 2, 'ANS')
2053 if params[1] == "OK":
2054 self.loggedIn()
2055
2056 def handle_ACK(self, params):
2057 checkParamLen(len(params), 1, 'ACK')
2058 self._fireCallback(int(params[0]), None)
2059
2060 def handle_NAK(self, params):
2061 checkParamLen(len(params), 1, 'NAK')
2062 self._fireCallback(int(params[0]), None)
2063
2064 def handle_BYE(self, params):
2065 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2066 self.userLeft(params[0])
2067
2068 # callbacks
2069
2070 def loggedIn(self):
2071 """
2072 called when all login details have been negotiated.
2073 Messages can now be sent, or new users invited.
2074 """
2075 pass
2076
2077 def gotChattingUsers(self, users):
2078 """
2079 called after connecting to an existing chat session.
2080
2081 @param users: A dict mapping user handles to screen names
2082 (current users taking part in the conversation)
2083 """
2084 pass
2085
2086 def userJoined(self, userHandle, screenName):
2087 """
2088 called when a user has joined the conversation.
2089
2090 @param userHandle: the user handle (passport) of the user
2091 @param screenName: the screen name of the user
2092 """
2093 pass
2094
2095 def userLeft(self, userHandle):
2096 """
2097 called when a user has left the conversation.
2098
2099 @param userHandle: the user handle (passport) of the user.
2100 """
2101 pass
2102
2103 def gotMessage(self, message):
2104 """
2105 called when we receive a message.
2106
2107 @param message: the associated MSNMessage object
2108 """
2109 pass
2110
2111 def gotAvatarImage(self, userHandle, image):
2112 """
2113 called when we receive an avatar from a contact
2114
2115 @param userHandle: the person who's avatar we have got
2116 @param image: the avatar image
2117 """
2118 pass
2119
2120 def gotSendRequest(self, fileReceive):
2121 """
2122 called when we receive a file send request from a contact
2123
2124 @param fileReceive: msnft.MSNFTReceive_Base instance
2125 """
2126 pass
2127
2128 def userTyping(self, message):
2129 """
2130 called when we receive the special type of message notifying
2131 us that a user is typing a message.
2132
2133 @param message: the associated MSNMessage object
2134 """
2135 pass
2136
2137 # api calls
2138
2139 def inviteUser(self, userHandle):
2140 """
2141 used to invite a user to the current switchboard server.
2142
2143 @param userHandle: the user handle (passport) of the desired user.
2144
2145 @return: A Deferred, the callback for which will be called
2146 when the server notifies us that the user has indeed
2147 been invited. The callback argument will be a tuple
2148 with 1 element, the sessionID given to the invited user.
2149 I'm not sure if this is useful or not.
2150 """
2151
2152 id, d = self._createIDMapping()
2153 self.sendLine("CAL %s %s" % (id, userHandle))
2154 return d
2155
2156 def sendMessage(self, message):
2157 """
2158 used to send a message.
2159
2160 @param message: the corresponding MSNMessage object.
2161
2162 @return: Depending on the value of message.ack.
2163 If set to MSNMessage.MESSAGE_ACK or
2164 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2165 the callback for which will be fired when an ACK or
2166 NACK is received - the callback argument will be
2167 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2168 the return value is None.
2169 """
2170
2171 if message.ack not in ('A','N','D'): id, d = self._nextTransactionID(), None
2172 else: id, d = self._createIDMapping()
2173 if message.length == 0: message.length = message._calcMessageLen()
2174 self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
2175 # apparently order matters with at least MIME-Version and Content-Type
2176 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
2177 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
2178 # send the rest of the headers
2179 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
2180 self.sendLine("%s: %s" % (header[0], header[1]))
2181 self.transport.write(CR+LF)
2182 self.transport.write(message.message)
2183 if MESSAGEDEBUG: log.msg(message.message)
2184 return d
2185
2186 def sendAvatarRequest(self, userHandle, msnobj):
2187 pass
2188
2189 def sendTypingNotification(self):
2190 """
2191 used to send a typing notification. Upon receiving this
2192 message the official client will display a 'user is typing'
2193 message to all other users in the chat session for 10 seconds.
2194 The official client sends one of these every 5 seconds (I think)
2195 as long as you continue to type.
2196 """
2197 m = MSNMessage()
2198 m.ack = m.MESSAGE_ACK_NONE
2199 m.setHeader('Content-Type', 'text/x-msmsgscontrol')
2200 m.setHeader('TypingUser', self.userHandle)
2201 m.message = "\r\n"
2202 self.sendMessage(m)
2203
2204 def sendFileInvitation(self, fileName, fileSize):
2205 """
2206 send an notification that we want to send a file.
2207
2208 @param fileName: the file name
2209 @param fileSize: the file size
2210
2211 @return: A Deferred, the callback of which will be fired
2212 when the user responds to this invitation with an
2213 appropriate message. The callback argument will be
2214 a tuple with 3 elements, the first being 1 or 0
2215 depending on whether they accepted the transfer
2216 (1=yes, 0=no), the second being an invitation cookie
2217 to identify your follow-up responses and the third being
2218 the message 'info' which is a dict of information they
2219 sent in their reply (this doesn't really need to be used).
2220 If you wish to proceed with the transfer see the
2221 sendTransferInfo method.
2222 """
2223 cookie = self._newInvitationCookie()
2224 d = Deferred()
2225 m = MSNMessage()
2226 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2227 m.message += 'Application-Name: File Transfer\r\n'
2228 m.message += 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2229 m.message += 'Invitation-Command: INVITE\r\n'
2230 m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2231 m.message += 'Application-File: %s\r\n' % fileName
2232 m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2233 m.ack = m.MESSAGE_ACK_NONE
2234 self.sendMessage(m)
2235 self.cookies['iCookies'][cookie] = (d, m)
2236 return d
2237
2238 def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2239 """
2240 send information relating to a file transfer session.
2241
2242 @param accept: whether or not to go ahead with the transfer
2243 (1=yes, 0=no)
2244 @param iCookie: the invitation cookie of previous replies
2245 relating to this transfer
2246 @param authCookie: the authentication cookie obtained from
2247 an FileSend instance
2248 @param ip: your ip
2249 @param port: the port on which an FileSend protocol is listening.
2250 """
2251 m = MSNMessage()
2252 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2253 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2254 m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2255 m.message += 'IP-Address: %s\r\n' % ip
2256 m.message += 'Port: %s\r\n' % port
2257 m.message += 'AuthCookie: %s\r\n' % authCookie
2258 m.message += '\r\n'
2259 m.ack = m.MESSAGE_NACK
2260 self.sendMessage(m)
2261
2262 class FileSend(LineReceiver):
2263 """
2264 This class provides support for sending files to other contacts.
2265
2266 @ivar bytesSent: the number of bytes that have currently been sent.
2267 @ivar completed: true if the send has completed.
2268 @ivar connected: true if a connection has been established.
2269 @ivar targetUser: the target user (contact).
2270 @ivar segmentSize: the segment (block) size.
2271 @ivar auth: the auth cookie (number) to use when sending the
2272 transfer invitation
2273 """
2274
2275 def __init__(self, file):
2276 """
2277 @param file: A string or file object represnting the file to send.
2278 """
2279
2280 if isinstance(file, types.StringType):
2281 self.file = open(file, 'rb')
2282 else:
2283 self.file = file
2284
2285 self.fileSize = 0
2286 self.bytesSent = 0
2287 self.completed = 0
2288 self.connected = 0
2289 self.targetUser = None
2290 self.segmentSize = 2045
2291 self.auth = random.randint(0, 2**30)
2292 self._pendingSend = None # :(
2293
2294 def connectionMade(self):
2295 self.connected = 1
2296
2297 def connectionLost(self, reason):
2298 if self._pendingSend:
2299 self._pendingSend.cancel()
2300 self._pendingSend = None
2301 self.connected = 0
2302 self.file.close()
2303
2304 def lineReceived(self, line):
2305 temp = line.split(' ')
2306 if len(temp) == 1: params = []
2307 else: params = temp[1:]
2308 cmd = temp[0]
2309 handler = getattr(self, "handle_%s" % cmd.upper(), None)
2310 if handler: handler(params)
2311 else: self.handle_UNKNOWN(cmd, params)
2312
2313 def handle_VER(self, params):
2314 checkParamLen(len(params), 1, 'VER')
2315 if params[0].upper() == "MSNFTP":
2316 self.sendLine("VER MSNFTP")
2317 else: # they sent some weird version during negotiation, i'm quitting.
2318 self.transport.loseConnection()
2319
2320 def handle_USR(self, params):
2321 checkParamLen(len(params), 2, 'USR')
2322 self.targetUser = params[0]
2323 if self.auth == int(params[1]):
2324 self.sendLine("FIL %s" % (self.fileSize))
2325 else: # they failed the auth test, disconnecting.
2326 self.transport.loseConnection()
2327
2328 def handle_TFR(self, params):
2329 checkParamLen(len(params), 0, 'TFR')
2330 # they are ready for me to start sending
2331 self.sendPart()
2332
2333 def handle_BYE(self, params):
2334 self.completed = (self.bytesSent == self.fileSize)
2335 self.transport.loseConnection()
2336
2337 def handle_CCL(self, params):
2338 self.completed = (self.bytesSent == self.fileSize)
2339 self.transport.loseConnection()
2340
2341 def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params))
2342
2343 def makeHeader(self, size):
2344 """ make the appropriate header given a specific segment size. """
2345 quotient, remainder = divmod(size, 256)
2346 return chr(0) + chr(remainder) + chr(quotient)
2347
2348 def sendPart(self):
2349 """ send a segment of data """
2350 if not self.connected:
2351 self._pendingSend = None
2352 return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
2353 data = self.file.read(self.segmentSize)
2354 if data:
2355 dataSize = len(data)
2356 header = self.makeHeader(dataSize)
2357 self.transport.write(header + data)
2358 self.bytesSent += dataSize
2359 self._pendingSend = reactor.callLater(0, self.sendPart)
2360 else:
2361 self._pendingSend = None
2362 self.completed = 1
2363
2364 # mapping of error codes to error messages
2365 errorCodes = {
2366
2367 200 : "Syntax error",
2368 201 : "Invalid parameter",
2369 205 : "Invalid user",
2370 206 : "Domain name missing",
2371 207 : "Already logged in",
2372 208 : "Invalid username",
2373 209 : "Invalid screen name",
2374 210 : "User list full",
2375 215 : "User already there",
2376 216 : "User already on list",
2377 217 : "User not online",
2378 218 : "Already in mode",
2379 219 : "User is in the opposite list",
2380 223 : "Too many groups",
2381 224 : "Invalid group",
2382 225 : "User not in group",
2383 229 : "Group name too long",
2384 230 : "Cannot remove group 0",
2385 231 : "Invalid group",
2386 280 : "Switchboard failed",
2387 281 : "Transfer to switchboard failed",
2388
2389 300 : "Required field missing",
2390 301 : "Too many FND responses",
2391 302 : "Not logged in",
2392
2393 402 : "Error accessing contact list",
2394 403 : "Error accessing contact list",
2395
2396 500 : "Internal server error",
2397 501 : "Database server error",
2398 502 : "Command disabled",
2399 510 : "File operation failed",
2400 520 : "Memory allocation failed",
2401 540 : "Wrong CHL value sent to server",
2402
2403 600 : "Server is busy",
2404 601 : "Server is unavaliable",
2405 602 : "Peer nameserver is down",
2406 603 : "Database connection failed",
2407 604 : "Server is going down",
2408 605 : "Server unavailable",
2409
2410 707 : "Could not create connection",
2411 710 : "Invalid CVR parameters",
2412 711 : "Write is blocking",
2413 712 : "Session is overloaded",
2414 713 : "Too many active users",
2415 714 : "Too many sessions",
2416 715 : "Not expected",
2417 717 : "Bad friend file",
2418 731 : "Not expected",
2419
2420 800 : "Requests too rapid",
2421
2422 910 : "Server too busy",
2423 911 : "Authentication failed",
2424 912 : "Server too busy",
2425 913 : "Not allowed when offline",
2426 914 : "Server too busy",
2427 915 : "Server too busy",
2428 916 : "Server too busy",
2429 917 : "Server too busy",
2430 918 : "Server too busy",
2431 919 : "Server too busy",
2432 920 : "Not accepting new users",
2433 921 : "Server too busy",
2434 922 : "Server too busy",
2435 923 : "No parent consent",
2436 924 : "Passport account not yet verified"
2437
2438 }
2439
2440 # mapping of status codes to readable status format
2441 statusCodes = {
2442
2443 STATUS_ONLINE : "Online",
2444 STATUS_OFFLINE : "Offline",
2445 STATUS_HIDDEN : "Appear Offline",
2446 STATUS_IDLE : "Idle",
2447 STATUS_AWAY : "Away",
2448 STATUS_BUSY : "Busy",
2449 STATUS_BRB : "Be Right Back",
2450 STATUS_PHONE : "On the Phone",
2451 STATUS_LUNCH : "Out to Lunch"
2452
2453 }
2454
2455 # mapping of list ids to list codes
2456 listIDToCode = {
2457
2458 FORWARD_LIST : 'fl',
2459 BLOCK_LIST : 'bl',
2460 ALLOW_LIST : 'al',
2461 REVERSE_LIST : 'rl',
2462 PENDING_LIST : 'pl'
2463
2464 }
2465
2466 # mapping of list codes to list ids
2467 listCodeToID = {}
2468 for id,code in listIDToCode.items():
2469 listCodeToID[code] = id
2470
2471 del id, code