]> code.delx.au - pymsnt/blob - src/tlib/msn/msnw.py
Weirdness with closures.
[pymsnt] / src / tlib / msn / msnw.py
1 # Copyright 2004-2005 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 # Twisted imports
5 from twisted.internet import reactor
6 from twisted.internet.defer import Deferred
7 from twisted.internet.protocol import ClientFactory
8
9 # System imports
10 import math, base64, binascii, math
11
12 # Local imports
13 from debug import LogEvent, INFO, WARN, ERROR
14 from tlib.msn import msn
15
16
17 MAXMESSAGESIZE = 1400
18 SWITCHBOARDTIMEOUT = 30.0*60.0
19 GETALLAVATARS = False
20
21
22 """
23 All interaction should be with the MSNConnection and MultiSwitchboardSession classes.
24 You should not directly instantiate any objects of other classes.
25 """
26
27 class MSNConnection:
28 """ Manages all the Twisted factories, etc """
29 def __init__(self, username, password, ident):
30 """ Connects to the MSN servers.
31 @param username: the MSN passport to connect with.
32 @param password: the password for this account.
33 @param ident: a unique identifier to use in logging.
34 """
35 self.username = username
36 self.password = password
37 self.ident = ident
38 self.timeout = None
39 self.notificationClient = None
40 self.connect()
41 LogEvent(INFO, self.ident)
42
43 def connect(self):
44 """ Automatically called by the constructor """
45 self.connectors = []
46 self.switchboardSessions = {}
47 self.savedEvents = SavedEvents() # Save any events that occur before connect
48 self._getNotificationReferral()
49
50 def _getNotificationReferral(self):
51 def timeout():
52 if not d.called: d.errback()
53 self.timeout = reactor.callLater(30, timeout)
54 dispatchFactory = msn.DispatchFactory()
55 dispatchFactory.userHandle = self.username
56 dispatchFactory.protocol = DispatchClient
57 d = Deferred()
58 dispatchFactory.d = d
59 d.addCallbacks(self._gotNotificationReferral, self.connectionFailed)
60 self.connectors.append(reactor.connectTCP("messenger.hotmail.com", 1863, dispatchFactory))
61 LogEvent(INFO, self.ident)
62
63 def _gotNotificationReferral(self, (host, port)):
64 self.timeout.cancel()
65 # Create the NotificationClient
66 self.notificationFactory = msn.NotificationFactory()
67 self.notificationFactory.userHandle = self.username
68 self.notificationFactory.password = self.password
69 self.notificationFactory.msncon = self
70 self.notificationFactory.protocol = NotificationClient
71 self.connectors.append(reactor.connectTCP(host, port, self.notificationFactory))
72 LogEvent(INFO, self.ident)
73
74 def _sendSavedEvents(self):
75 self.savedEvents.send(self)
76 self.savedEvents = None
77
78 def _notificationClientReady(self, notificationClient):
79 self.notificationClient = notificationClient
80
81 def _ensureSwitchboardSession(self, userHandle):
82 if not self.switchboardSessions.has_key(userHandle):
83 sb = OneSwitchboardSession(self, userHandle)
84 sb.connect()
85 self.switchboardSessions[userHandle] = sb
86
87
88
89 # API calls
90 def getContacts(self):
91 """ Gets the contact list.
92
93 @return an instance of MSNContactList (do not modify) if connected,
94 or None if not.
95 """
96 if self.notificationFactory:
97 return self.notificationFactory.contacts
98 else:
99 return None
100
101 def sendMessage(self, userHandle, text, noerror=False):
102 """
103 Sends a message to a contact. Can only be called after listSynchronized().
104
105 @param userHandle: the contact's MSN passport.
106 @param text: the text to send.
107 @param noerror: Set this to True if you don't want failed messages to bounce.
108 """
109 LogEvent(INFO, self.ident)
110 if self.notificationClient:
111 self._ensureSwitchboardSession(userHandle)
112 self.switchboardSessions[userHandle].sendMessage(text, noerror)
113 elif not noerror:
114 self.failedMessage(userHandle, text)
115
116 def sendAvatarRequest(self, userHandle):
117 """
118 Requests the avatar of a contact.
119
120 @param userHandle: the contact to request an avatar from.
121 @return: a Deferred() if the avatar can be fetched at this time.
122 This will fire with an argument of a tuple with the PNG
123 image data as the only element.
124 Otherwise returns None
125 """
126
127 LogEvent(INFO, self.ident)
128 if not self.notificationClient: return
129 if GETALLAVATARS:
130 self._ensureSwitchboardSession(userHandle)
131 sb = self.switchboardSessions.get(userHandle)
132 if sb: return sb.sendAvatarRequest()
133
134 def sendFile(self, userHandle, filename, filesize):
135 """
136 Used to send a file to a contact.
137
138 @param username: the passport of the contact to send a file to.
139 @param filename: the name of the file to send.
140 @param filesize: the size of the file to send.
141
142 @return: A Deferred, which will fire with an argument of:
143 (fileSend, d) A FileSend object and a Deferred.
144 The Deferred will pass one argument in a tuple,
145 whether or not the transfer is accepted. If you
146 receive a True, then you can call write() on the
147 fileSend object to send your file. Call close()
148 when the file is done.
149 NOTE: You MUST write() exactly as much as you
150 declare in filesize.
151 """
152 msnContact = self.getContacts().getContact(userHandle)
153 if not msnContact:
154 raise ValueError, "Contact not found"
155 self._ensureSwitchboardSession(userHandle)
156 return self.switchboardSessions[userHandle].sendFile(msnContact, filename, filesize)
157
158 def sendTypingToContact(self, userHandle):
159 """
160 Sends typing notification to a contact. Should send every 5secs.
161 @param userHandle: the contact to notify of our typing.
162 """
163
164 sb = self.switchboardSessions.get(userHandle)
165 if sb: return sb.sendTypingNotification()
166
167 def changeAvatar(self, imageData):
168 """
169 Changes the user's avatar.
170 @param imageData: the new PNG avatar image data.
171 """
172 if self.notificationClient:
173 LogEvent(INFO, self.ident)
174 self.notificationClient.changeAvatar(imageData, push=True)
175 else:
176 self.savedEvents.avatarImageData = imageData
177
178 def changeStatus(self, statusCode, screenName, personal):
179 """
180 Changes your status details. All details must be given with
181 each call. This can be called before connection if you wish.
182
183 @param statusCode: the user's new status (look in msn.statusCodes).
184 @param screenName: the user's new screenName (up to 127 characters).
185 @param personal: the user's new personal message.
186 """
187
188 if not screenName: screenName = self.username
189 if self.notificationClient:
190 changeCount = [0] # Hack
191 def cb(ignored=None):
192 changeCount[0] += 1
193 if changeCount[0] == 3:
194 self.ourStatusChanged(statusCode, screenName, personal)
195 LogEvent(INFO, self.ident)
196 self.notificationClient.changeStatus(statusCode.encode("utf-8")).addCallback(cb)
197 self.notificationClient.changeScreenName(screenName.encode("utf-8")).addCallback(cb)
198 if not personal: personal = ""
199 self.notificationClient.changePersonalMessage(personal.encode("utf-8")).addCallback(cb)
200 else:
201 self.savedEvents.statusCode = statusCode
202 self.savedEvents.screenName = screenName
203 self.savedEvents.personal = personal
204
205 def addContact(self, listType, userHandle):
206 """ See msn.NotificationClient.addContact """
207 if self.notificationClient:
208 return self.notificationClient.addContact(listType, str(userHandle))
209 else:
210 self.savedEvents.addContacts.append((listType, str(userHandle)))
211
212 def remContact(self, listType, userHandle):
213 """ See msn.NotificationClient.remContact """
214 if self.notificationClient:
215 return self.notificationClient.remContact(listType, str(userHandle))
216 else:
217 self.savedEvents.remContacts.append((listType, str(userHandle)))
218
219 def logOut(self):
220 """ Shuts down the whole connection. Don't try to call any
221 other methods after this one. """
222 if self.notificationClient:
223 self.notificationClient.logOut()
224 for c in self.connectors:
225 c.disconnect()
226 if self.notificationFactory:
227 self.notificationFactory.msncon = None
228 self.connectors = []
229 self.switchboardSessions = {}
230 LogEvent(INFO, self.ident)
231
232
233 # Reimplement these!
234 def connectionFailed(self, reason=''):
235 """ Called when the connection to the server failed. """
236
237 def loginFailed(self, reason=''):
238 """ Called when the account could not be logged in. """
239
240 def connectionLost(self, reason=''):
241 """ Called when we are disconnected. """
242
243 def multipleLogin(self):
244 """ Called when the server says there has been another login
245 for this account. """
246
247 def serverGoingDown(self):
248 """ Called when the server says that it will be going down. """
249
250 def accountNotVerified(self):
251 """ Called if this passport has not been verified. Certain
252 functions are not available. """
253
254 def userMapping(self, passport, jid):
255 """ Called when it is brought to our attention that one of the
256 MSN contacts has a Jabber ID. You should communicate with Jabber. """
257
258 def loggedIn(self):
259 """ Called when we have authenticated, but before we receive
260 the contact list. """
261
262 def listSynchronized(self):
263 """ Called when we have received the contact list. All methods
264 in this class are now valid. """
265
266 def ourStatusChanged(self, statusCode, screenName, personal):
267 """ Called when the user's status has changed. """
268
269 def gotMessage(self, userHandle, text):
270 """ Called when a contact sends us a message """
271
272 def gotGroupchat(self, msnGroupchat, userHandle):
273 """ Called when a conversation with more than one contact begins.
274 userHandle is the person who invited us.
275 The overriding method is expected to set msnGroupchat.groupchat to an object
276 that implements the following methods:
277 contactJoined(userHandle)
278 contactLeft(userHandle)
279 gotMessage(userHandle, text)
280
281 The object received as 'msnGroupchat' is an instance of MultiSwitchboardSession.
282 """
283
284 def gotContactTyping(self, userHandle):
285 """ Called when a contact sends typing notification.
286 Will be called once every 5 seconds. """
287
288 def failedMessage(self, userHandle, text):
289 """ Called when a message we sent has been bounced back. """
290
291 def contactAvatarChanged(self, userHandle, hash):
292 """ Called when we receive a changed avatar hash for a contact.
293 You should call sendAvatarRequest(). """
294
295 def contactStatusChanged(self, userHandle):
296 """ Called when we receive status information for a contact. """
297
298 def gotFileReceive(self, fileReceive):
299 """ Called when a contact sends the user a file.
300 Call accept(fileHandle) or reject() on the object. """
301
302 def contactAddedMe(self, userHandle):
303 """ Called when a contact adds the user to their list. """
304
305 def contactRemovedMe(self, userHandle):
306 """ Called when a contact removes the user from their list. """
307
308 def gotInitialEmailNotification(self, inboxunread, foldersunread):
309 """ Received at login to tell about the user's Hotmail status """
310
311 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
312 """ Received in realtime whenever an email comes into the hotmail account """
313
314 def gotMSNAlert(self, body, action, subscr):
315 """ An MSN Alert (http://alerts.msn.com) was received. Body is the
316 text of the alert. 'action' is a url for more information,
317 'subscr' is a url to modify your your alerts subscriptions. """
318
319 def gotAvatarImageData(self, userHandle, imageData):
320 """ An contact's avatar has been received because a switchboard
321 session with them was started. """
322
323
324 class SavedEvents:
325 def __init__(self):
326 self.nickname = ""
327 self.statusCode = ""
328 self.personal = ""
329 self.avatarImageData = ""
330 self.addContacts = []
331 self.remContacts = []
332
333 def send(self, msncon):
334 if self.avatarImageData:
335 msncon.notificationClient.changeAvatar(self.avatarImageData, push=False)
336 if self.nickname or self.statusCode or self.personal:
337 msncon.changeStatus(self.statusCode, self.nickname, self.personal)
338 for listType, userHandle in self.addContacts:
339 msncon.addContact(listType, userHandle)
340 for listType, userHandle in self.remContacts:
341 msncon.remContact(listType, userHandle)
342
343
344
345 class DispatchClient(msn.DispatchClient):
346 def gotNotificationReferral(self, host, port):
347 if self.factory.d.called: return # Too slow! We've already timed out
348 self.factory.d.callback((host, port))
349
350
351 class NotificationClient(msn.NotificationClient):
352 def loginFailure(self, message):
353 self.factory.msncon.loginFailed(message)
354
355 def loggedIn(self, userHandle, verified):
356 LogEvent(INFO, self.factory.msncon.ident)
357 msn.NotificationClient.loggedIn(self, userHandle, verified)
358 self.factory.msncon._notificationClientReady(self)
359 self.factory.msncon.loggedIn()
360 if not verified:
361 self.factory.msncon.accountNotVerified()
362
363 def logOut(self):
364 msn.NotificationClient.logOut(self)
365
366 def connectionLost(self, reason):
367 if not self.factory.msncon: return # If we called logOut
368 def wait():
369 LogEvent(INFO, self.factory.msncon.ident)
370 msn.NotificationClient.connectionLost(self, reason)
371 self.factory.msncon.connectionLost(reason)
372 # Make sure this event is handled after any others
373 reactor.callLater(0, wait)
374
375 def gotMSNAlert(self, body, action, subscr):
376 LogEvent(INFO, self.factory.msncon.ident)
377 self.factory.msncon.gotMSNAlert(body, action, subscr)
378
379 def gotInitialEmailNotification(self, inboxunread, foldersunread):
380 LogEvent(INFO, self.factory.msncon.ident)
381 self.factory.msncon.gotInitialEmailNotification(inboxunread, foldersunread)
382
383 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
384 LogEvent(INFO, self.factory.msncon.ident)
385 self.factory.msncon.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
386
387 def userAddedMe(self, userGuid, userHandle, screenName):
388 LogEvent(INFO, self.factory.msncon.ident)
389 self.factory.msncon.contactAddedMe(userHandle)
390
391 def userRemovedMe(self, userHandle):
392 LogEvent(INFO, self.factory.msncon.ident)
393 self.factory.msncon.contactRemovedMe(userHandle)
394
395 def listSynchronized(self, *args):
396 LogEvent(INFO, self.factory.msncon.ident)
397 self.factory.msncon._sendSavedEvents()
398 self.factory.msncon.listSynchronized()
399
400 def contactAvatarChanged(self, userHandle, hash):
401 LogEvent(INFO, self.factory.msncon.ident)
402 self.factory.msncon.contactAvatarChanged(userHandle, hash)
403
404 def gotContactStatus(self, userHandle, statusCode, screenName):
405 LogEvent(INFO, self.factory.msncon.ident)
406 self.factory.msncon.contactStatusChanged(userHandle)
407
408 def contactStatusChanged(self, userHandle, statusCode, screenName):
409 LogEvent(INFO, self.factory.msncon.ident)
410 self.factory.msncon.contactStatusChanged(userHandle)
411
412 def contactPersonalChanged(self, userHandle, personal):
413 LogEvent(INFO, self.factory.msncon.ident)
414 self.factory.msncon.contactStatusChanged(userHandle)
415
416 def contactOffline(self, userHandle):
417 LogEvent(INFO, self.factory.msncon.ident)
418 self.factory.msncon.contactStatusChanged(userHandle)
419
420 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
421 LogEvent(INFO, self.factory.msncon.ident)
422 sb = self.factory.msncon.switchboardSessions.get(userHandle)
423 if sb and sb.transport:
424 sb.transport.loseConnection()
425 else:
426 sb = OneSwitchboardSession(self.factory.msncon, userHandle)
427 self.factory.msncon.switchboardSessions[userHandle] = sb
428 sb.connectReply(host, port, key, sessionID)
429
430 def multipleLogin(self):
431 LogEvent(INFO, self.factory.msncon.ident)
432 self.factory.msncon.multipleLogin()
433
434 def serverGoingDown(self):
435 LogEvent(INFO, self.factory.msncon.ident)
436 self.factory.msncon.serverGoingDown()
437
438
439
440 class SwitchboardSessionBase(msn.SwitchboardClient):
441 def __init__(self, msncon):
442 msn.SwitchboardClient.__init__(self)
443 self.msncon = msncon
444 self.msnobj = msncon.notificationClient.msnobj
445 self.userHandle = msncon.username
446 self.ident = (msncon.ident, "INVALID!!")
447 self.messageBuffer = []
448 self.funcBuffer = []
449 self.ready = False
450
451 def __del__(self):
452 LogEvent(INFO, self.ident)
453 del self.msncon
454 self.transport.disconnect()
455
456 def loggedIn(self):
457 LogEvent(INFO, self.ident)
458 self.ready = True
459 self.flushBuffer()
460
461 def connect(self):
462 LogEvent(INFO, self.ident)
463 self.ready = False
464 def sbRequestAccepted((host, port, key)):
465 LogEvent(INFO, self.ident)
466 self.key = key
467 self.reply = 0
468 factory = ClientFactory()
469 factory.buildProtocol = lambda addr: self
470 reactor.connectTCP(host, port, factory)
471 def sbRequestFailed(ignored=None):
472 LogEvent(INFO, self.ident)
473 del self.msncon.switchboardSessions[self.remoteUser]
474 d = self.msncon.notificationClient.requestSwitchboardServer()
475 d.addCallbacks(sbRequestAccepted, sbRequestFailed)
476
477 def connectReply(self, host, port, key, sessionID):
478 LogEvent(INFO, self.ident)
479 self.ready = False
480 self.key = key
481 self.sessionID = sessionID
482 self.reply = 1
483 factory = ClientFactory()
484 factory.buildProtocol = lambda addr: self
485 reactor.connectTCP(host, port, factory)
486
487 def flushBuffer(self):
488 for message, noerror in self.messageBuffer[:]:
489 self.messageBuffer.remove((message, noerror))
490 self.sendMessage(message, noerror)
491 for f in self.funcBuffer[:]:
492 self.funcBuffer.remove(f)
493 f()
494
495 def failedMessage(self, *ignored):
496 raise NotImplementedError
497
498 def sendClientCaps(self):
499 message = msn.MSNMessage()
500 message.setHeader("Content-Type", "text/x-clientcaps")
501 message.setHeader("Client-Name", "PyMSNt")
502 if hasattr(self.msncon, "jabberID"):
503 message.setHeader("JabberID", str(self.msncon.jabberID))
504 self.sendMessage(message)
505
506 def sendMessage(self, message, noerror=False):
507 # Check to make sure that clientcaps only gets sent after
508 # the first text type message.
509 if isinstance(message, msn.MSNMessage) and message.getHeader("Content-Type").startswith("text"):
510 self.sendMessage = self.sendMessageReal
511 self.sendClientCaps()
512 return self.sendMessage(message, noerror)
513 else:
514 return self.sendMessageReal(message, noerror)
515
516 def sendMessageReal(self, text, noerror=False):
517 if not isinstance(text, basestring):
518 msn.SwitchboardClient.sendMessage(self, text)
519 return
520 if not self.ready:
521 self.messageBuffer.append((text, noerror))
522 else:
523 LogEvent(INFO, self.ident)
524 def failedMessage(ignored):
525 if not noerror:
526 self.failedMessage(text)
527
528 if len(text) < MAXMESSAGESIZE:
529 message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
530 message.setHeader("Content-Type", "text/plain; charset=UTF-8")
531 message.ack = msn.MSNMessage.MESSAGE_NACK
532
533 d = msn.SwitchboardClient.sendMessage(self, message)
534 if not noerror:
535 d.addCallback(failedMessage)
536
537 else:
538 chunks = int(math.ceil(len(text) / float(MAXMESSAGESIZE)))
539 chunk = 0
540 guid = msn.random_guid()
541 while chunk < chunks:
542 offset = chunk * MAXMESSAGESIZE
543 text = message[offset : offset + MAXMESSAGESIZE]
544 message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
545 message.ack = msn.MSNMessage.MESSAGE_NACK
546 if chunk == 0:
547 message.setHeader("Content-Type", "text/plain; charset=UTF-8")
548 message.setHeader("Chunks", str(chunks))
549 else:
550 message.setHeader("Chunk", str(chunk))
551
552 d = msn.SwitchboardClient.sendMessage(self, message)
553 if not noerror:
554 d.addCallback(failedMessage)
555
556 chunk += 1
557
558
559 class MultiSwitchboardSession(SwitchboardSessionBase):
560 """ Create one of me to chat to multiple contacts """
561
562 def __init__(self, msncon):
563 """ Automatically creates a new switchboard connection to the server """
564 SwitchboardSessionBase.__init__(self, msncon)
565 self.ident = (self.msncon.ident, self)
566 self.contactCount = 0
567 self.groupchat = None
568 self.connect()
569
570 def failedMessage(self, text):
571 self.groupchat.gotMessage("BOUNCE", text)
572
573 def sendMessage(self, text, noerror=False):
574 """ Used to send a mesage to the groupchat. Can be called immediately
575 after instantiation. """
576 if self.contactCount > 0:
577 SwitchboardSessionBase.sendMessage(self, text, noerror)
578 else:
579 #self.messageBuffer.append((message, noerror))
580 pass # They're sending messages to an empty room. Ignore.
581
582 def inviteUser(self, userHandle):
583 """ Used to invite a contact to the groupchat. Can be called immediately
584 after instantiation. """
585 userHandle = str(userHandle)
586 if self.ready:
587 LogEvent(INFO, self.ident, "immediate")
588 msn.SwitchboardClient.inviteUser(self, userHandle)
589 else:
590 LogEvent(INFO, self.ident, "pending")
591 self.funcBuffer.append(lambda: msn.SwitchboardClient.inviteUser(self, userHandle))
592
593 def gotMessage(self, message):
594 self.groupchat.gotMessage(message.userHandle, message.getMessage())
595
596 def userJoined(self, userHandle, screenName=''):
597 LogEvent(INFO, self.ident)
598 self.contactCount += 1
599 self.groupchat.contactJoined(userHandle)
600
601 def userLeft(self, userHandle):
602 LogEvent(INFO, self.ident)
603 self.contactCount -= 1
604 self.groupchat.contactLeft(userHandle)
605
606
607
608 class OneSwitchboardSession(SwitchboardSessionBase):
609 def __init__(self, msncon, remoteUser):
610 SwitchboardSessionBase.__init__(self, msncon)
611 self.remoteUser = str(remoteUser)
612 self.ident = (self.msncon.ident, self.remoteUser)
613 self.chattingUsers = []
614 self.timeout = None
615
616 def __del__(self):
617 for message, noerror in self.messageBuffer:
618 if not noerror:
619 self.failedMessage(self.remoteUser, message)
620
621 def _ready(self):
622 LogEvent(INFO, self.ident)
623 self.ready = True
624 for user in self.chattingUsers:
625 self.userJoined(user)
626 if self.timeout:
627 self.timeout.cancel()
628 self.timeout = None
629 self.flushBuffer()
630
631 def _switchToMulti(self, userHandle):
632 LogEvent(INFO, self.ident)
633 del self.msncon.switchboardSessions[self.remoteUser]
634 self.__class__ = MultiSwitchboardSession
635 del self.remoteUser
636 self.contactCount = 0
637 self.msncon.gotGroupchat(self, userHandle)
638 if not self.groupchat:
639 LogEvent(ERROR, self.ident)
640 raise Exception("YouNeedAGroupchat-WeHaveAProblemError") # FIXME
641
642 def failedMessage(self, text):
643 self.msncon.failedMessage(self.remoteUser, text)
644
645 # Callbacks
646 def loggedIn(self):
647 LogEvent(INFO, self.ident)
648 if not self.reply:
649 def failCB(arg=None):
650 LogEvent(INFO, self.ident, "User has not joined after 30 seconds.")
651 del self.msncon.switchboardSessions[self.remoteUser]
652 d = self.inviteUser(self.remoteUser)
653 d.addErrback(failCB)
654 self.timeout = reactor.callLater(30.0, failCB)
655 else:
656 self._ready()
657
658 def gotChattingUsers(self, users):
659 for userHandle in users.keys():
660 self.chattingUsers.append(userHandle)
661
662 def userJoined(self, userHandle, screenName=''):
663 LogEvent(INFO, self.ident)
664 if not self.reply:
665 self._ready()
666 if userHandle != self.remoteUser:
667 # Another user has joined, so we now have three participants.
668 remoteUser = self.remoteUser
669 self._switchToMulti(remoteUser)
670 self.userJoined(remoteUser)
671 self.userJoined(userHandle)
672 else:
673 def updateAvatarCB((imageData, )):
674 if self.msncon:
675 self.msncon.gotAvatarImageData(self.remoteUser, imageData)
676 d = self.sendAvatarRequest()
677 if d:
678 d.addCallback(updateAvatarCB)
679
680 def userLeft(self, userHandle):
681 def wait():
682 if userHandle == self.remoteUser:
683 del self.msncon.switchboardSessions[self.remoteUser]
684 reactor.callLater(0, wait) # Make sure this is handled after everything else
685
686 def gotMessage(self, message):
687 LogEvent(INFO, self.ident)
688 cTypes = [s.strip() for s in message.getHeader("Content-Type").split(';')]
689 if "text/plain" == cTypes[0]:
690 try:
691 if len(cTypes) > 1 and cTypes[1].lower().find("utf-8") >= 0:
692 text = message.getMessage().decode("utf-8")
693 else:
694 text = message.getMessage()
695 self.msncon.gotMessage(self.remoteUser, text)
696 except:
697 self.msncon.gotMessage("A message was lost.")
698 raise
699 elif "text/x-clientcaps" == cTypes[0]:
700 if message.hasHeader("JabberID"):
701 jid = message.getHeader("JabberID")
702 self.switchboardSession.msncon.userMapping(message.userHandle, jid)
703 else:
704 LogEvent(INFO, self.ident, "Discarding unknown message type.")
705
706 def gotFileReceive(self, fileReceive):
707 LogEvent(INFO, self.ident)
708 self.msncon.gotFileReceive(fileReceive)
709
710 def gotContactTyping(self, message):
711 LogEvent(INFO, self.ident)
712 self.msncon.gotContactTyping(message.userHandle)
713
714 def sendTypingNotification(self):
715 LogEvent(INFO, self.ident)
716 if self.ready:
717 msn.SwitchboardClient.sendTypingNotification(self)
718
719 CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
720 def sendAvatarRequest(self):
721 if not self.ready: return
722 msnContacts = self.msncon.getContacts()
723 if not msnContacts: return
724 msnContact = msnContacts.getContact(self.remoteUser)
725 if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
726 if msnContact.msnobjGot: return
727 msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
728 return msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
729
730 def sendFile(self, msnContact, filename, filesize):
731 def doSendFile(ignored=None):
732 d.callback(msn.SwitchboardClient.sendFile(self, msnContact, filename, filesize))
733 d = Deferred()
734 if self.ready:
735 reactor.callLater(0, doSendFile)
736 else:
737 self.funcBuffer.append(doSendFile)
738 return d
739