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