]> code.delx.au - pymsnt/blob - src/tlib/msn/msnw.py
Fixed getAllAvatars, changeCount race condition and intern->internJID
[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 del changeCount
196 LogEvent(INFO, self.ident)
197 self.notificationClient.changeStatus(statusCode.encode("utf-8")).addCallback(cb)
198 self.notificationClient.changeScreenName(screenName.encode("utf-8")).addCallback(cb)
199 if not personal: personal = ""
200 self.notificationClient.changePersonalMessage(personal.encode("utf-8")).addCallback(cb)
201 else:
202 self.savedEvents.statusCode = statusCode
203 self.savedEvents.screenName = screenName
204 self.savedEvents.personal = personal
205
206 def addContact(self, listType, userHandle):
207 """ See msn.NotificationClient.addContact """
208 if self.notificationClient:
209 return self.notificationClient.addContact(listType, str(userHandle))
210 else:
211 self.savedEvents.addContacts.append((listType, str(userHandle)))
212
213 def remContact(self, listType, userHandle):
214 """ See msn.NotificationClient.remContact """
215 if self.notificationClient:
216 return self.notificationClient.remContact(listType, str(userHandle))
217 else:
218 self.savedEvents.remContacts.append((listType, str(userHandle)))
219
220 def logOut(self):
221 """ Shuts down the whole connection. Don't try to call any
222 other methods after this one. """
223 if self.notificationClient:
224 self.notificationClient.logOut()
225 for c in self.connectors:
226 c.disconnect()
227 if self.notificationFactory:
228 self.notificationFactory.msncon = None
229 self.connectors = []
230 self.switchboardSessions = {}
231 LogEvent(INFO, self.ident)
232
233
234 # Reimplement these!
235 def connectionFailed(self, reason=''):
236 """ Called when the connection to the server failed. """
237
238 def loginFailed(self, reason=''):
239 """ Called when the account could not be logged in. """
240
241 def connectionLost(self, reason=''):
242 """ Called when we are disconnected. """
243
244 def multipleLogin(self):
245 """ Called when the server says there has been another login
246 for this account. """
247
248 def serverGoingDown(self):
249 """ Called when the server says that it will be going down. """
250
251 def accountNotVerified(self):
252 """ Called if this passport has not been verified. Certain
253 functions are not available. """
254
255 def userMapping(self, passport, jid):
256 """ Called when it is brought to our attention that one of the
257 MSN contacts has a Jabber ID. You should communicate with Jabber. """
258
259 def loggedIn(self):
260 """ Called when we have authenticated, but before we receive
261 the contact list. """
262
263 def listSynchronized(self):
264 """ Called when we have received the contact list. All methods
265 in this class are now valid. """
266
267 def ourStatusChanged(self, statusCode, screenName, personal):
268 """ Called when the user's status has changed. """
269
270 def gotMessage(self, userHandle, text):
271 """ Called when a contact sends us a message """
272
273 def gotGroupchat(self, msnGroupchat, userHandle):
274 """ Called when a conversation with more than one contact begins.
275 userHandle is the person who invited us.
276 The overriding method is expected to set msnGroupchat.groupchat to an object
277 that implements the following methods:
278 contactJoined(userHandle)
279 contactLeft(userHandle)
280 gotMessage(userHandle, text)
281
282 The object received as 'msnGroupchat' is an instance of MultiSwitchboardSession.
283 """
284
285 def gotContactTyping(self, userHandle):
286 """ Called when a contact sends typing notification.
287 Will be called once every 5 seconds. """
288
289 def failedMessage(self, userHandle, text):
290 """ Called when a message we sent has been bounced back. """
291
292 def contactAvatarChanged(self, userHandle, hash):
293 """ Called when we receive a changed avatar hash for a contact.
294 You should call sendAvatarRequest(). """
295
296 def contactStatusChanged(self, userHandle):
297 """ Called when we receive status information for a contact. """
298
299 def gotFileReceive(self, fileReceive):
300 """ Called when a contact sends the user a file.
301 Call accept(fileHandle) or reject() on the object. """
302
303 def contactAddedMe(self, userHandle):
304 """ Called when a contact adds the user to their list. """
305
306 def contactRemovedMe(self, userHandle):
307 """ Called when a contact removes the user from their list. """
308
309 def gotInitialEmailNotification(self, inboxunread, foldersunread):
310 """ Received at login to tell about the user's Hotmail status """
311
312 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
313 """ Received in realtime whenever an email comes into the hotmail account """
314
315 def gotMSNAlert(self, body, action, subscr):
316 """ An MSN Alert (http://alerts.msn.com) was received. Body is the
317 text of the alert. 'action' is a url for more information,
318 'subscr' is a url to modify your your alerts subscriptions. """
319
320 def gotAvatarImageData(self, userHandle, imageData):
321 """ An contact's avatar has been received because a switchboard
322 session with them was started. """
323
324
325 class SavedEvents:
326 def __init__(self):
327 self.nickname = ""
328 self.statusCode = ""
329 self.personal = ""
330 self.avatarImageData = ""
331 self.addContacts = []
332 self.remContacts = []
333
334 def send(self, msncon):
335 if self.avatarImageData:
336 msncon.notificationClient.changeAvatar(self.avatarImageData, push=False)
337 if self.nickname or self.statusCode or self.personal:
338 msncon.changeStatus(self.statusCode, self.nickname, self.personal)
339 for listType, userHandle in self.addContacts:
340 msncon.addContact(listType, userHandle)
341 for listType, userHandle in self.remContacts:
342 msncon.remContact(listType, userHandle)
343
344
345
346 class DispatchClient(msn.DispatchClient):
347 def gotNotificationReferral(self, host, port):
348 if self.factory.d.called: return # Too slow! We've already timed out
349 self.factory.d.callback((host, port))
350
351
352 class NotificationClient(msn.NotificationClient):
353 def loginFailure(self, message):
354 self.factory.msncon.loginFailed(message)
355
356 def loggedIn(self, userHandle, verified):
357 LogEvent(INFO, self.factory.msncon.ident)
358 msn.NotificationClient.loggedIn(self, userHandle, verified)
359 self.factory.msncon._notificationClientReady(self)
360 self.factory.msncon.loggedIn()
361 if not verified:
362 self.factory.msncon.accountNotVerified()
363
364 def logOut(self):
365 msn.NotificationClient.logOut(self)
366
367 def connectionLost(self, reason):
368 if not self.factory.msncon: return # If we called logOut
369 def wait():
370 LogEvent(INFO, self.factory.msncon.ident)
371 msn.NotificationClient.connectionLost(self, reason)
372 self.factory.msncon.connectionLost(reason)
373 # Make sure this event is handled after any others
374 reactor.callLater(0, wait)
375
376 def gotMSNAlert(self, body, action, subscr):
377 LogEvent(INFO, self.factory.msncon.ident)
378 self.factory.msncon.gotMSNAlert(body, action, subscr)
379
380 def gotInitialEmailNotification(self, inboxunread, foldersunread):
381 LogEvent(INFO, self.factory.msncon.ident)
382 self.factory.msncon.gotInitialEmailNotification(inboxunread, foldersunread)
383
384 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
385 LogEvent(INFO, self.factory.msncon.ident)
386 self.factory.msncon.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
387
388 def userAddedMe(self, userGuid, userHandle, screenName):
389 LogEvent(INFO, self.factory.msncon.ident)
390 self.factory.msncon.contactAddedMe(userHandle)
391
392 def userRemovedMe(self, userHandle):
393 LogEvent(INFO, self.factory.msncon.ident)
394 self.factory.msncon.contactRemovedMe(userHandle)
395
396 def listSynchronized(self, *args):
397 LogEvent(INFO, self.factory.msncon.ident)
398 self.factory.msncon._sendSavedEvents()
399 self.factory.msncon.listSynchronized()
400
401 def contactAvatarChanged(self, userHandle, hash):
402 LogEvent(INFO, self.factory.msncon.ident)
403 self.factory.msncon.contactAvatarChanged(userHandle, hash)
404
405 def gotContactStatus(self, userHandle, statusCode, screenName):
406 LogEvent(INFO, self.factory.msncon.ident)
407 self.factory.msncon.contactStatusChanged(userHandle)
408
409 def contactStatusChanged(self, userHandle, statusCode, screenName):
410 LogEvent(INFO, self.factory.msncon.ident)
411 self.factory.msncon.contactStatusChanged(userHandle)
412
413 def contactPersonalChanged(self, userHandle, personal):
414 LogEvent(INFO, self.factory.msncon.ident)
415 self.factory.msncon.contactStatusChanged(userHandle)
416
417 def contactOffline(self, userHandle):
418 LogEvent(INFO, self.factory.msncon.ident)
419 self.factory.msncon.contactStatusChanged(userHandle)
420
421 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
422 LogEvent(INFO, self.factory.msncon.ident)
423 sb = self.factory.msncon.switchboardSessions.get(userHandle)
424 if sb and sb.transport:
425 sb.transport.loseConnection()
426 else:
427 sb = OneSwitchboardSession(self.factory.msncon, userHandle)
428 self.factory.msncon.switchboardSessions[userHandle] = sb
429 sb.connectReply(host, port, key, sessionID)
430
431 def multipleLogin(self):
432 LogEvent(INFO, self.factory.msncon.ident)
433 self.factory.msncon.multipleLogin()
434
435 def serverGoingDown(self):
436 LogEvent(INFO, self.factory.msncon.ident)
437 self.factory.msncon.serverGoingDown()
438
439
440
441 class SwitchboardSessionBase(msn.SwitchboardClient):
442 def __init__(self, msncon):
443 msn.SwitchboardClient.__init__(self)
444 self.msncon = msncon
445 self.msnobj = msncon.notificationClient.msnobj
446 self.userHandle = msncon.username
447 self.ident = (msncon.ident, "INVALID!!")
448 self.messageBuffer = []
449 self.funcBuffer = []
450 self.ready = False
451
452 def __del__(self):
453 LogEvent(INFO, self.ident)
454 del self.msncon
455 self.transport.disconnect()
456
457 def loggedIn(self):
458 LogEvent(INFO, self.ident)
459 self.ready = True
460 self.flushBuffer()
461
462 def connect(self):
463 LogEvent(INFO, self.ident)
464 self.ready = False
465 def sbRequestAccepted((host, port, key)):
466 LogEvent(INFO, self.ident)
467 self.key = key
468 self.reply = 0
469 factory = ClientFactory()
470 factory.buildProtocol = lambda addr: self
471 reactor.connectTCP(host, port, factory)
472 def sbRequestFailed(ignored=None):
473 LogEvent(INFO, self.ident)
474 del self.msncon.switchboardSessions[self.remoteUser]
475 d = self.msncon.notificationClient.requestSwitchboardServer()
476 d.addCallbacks(sbRequestAccepted, sbRequestFailed)
477
478 def connectReply(self, host, port, key, sessionID):
479 LogEvent(INFO, self.ident)
480 self.ready = False
481 self.key = key
482 self.sessionID = sessionID
483 self.reply = 1
484 factory = ClientFactory()
485 factory.buildProtocol = lambda addr: self
486 reactor.connectTCP(host, port, factory)
487
488 def flushBuffer(self):
489 for message, noerror in self.messageBuffer[:]:
490 self.messageBuffer.remove((message, noerror))
491 self.sendMessage(message, noerror)
492 for f in self.funcBuffer[:]:
493 self.funcBuffer.remove(f)
494 f()
495
496 def failedMessage(self, *ignored):
497 raise NotImplementedError
498
499 def sendClientCaps(self):
500 message = msn.MSNMessage()
501 message.setHeader("Content-Type", "text/x-clientcaps")
502 message.setHeader("Client-Name", "PyMSNt")
503 if hasattr(self.msncon, "jabberID"):
504 message.setHeader("JabberID", str(self.msncon.jabberID))
505 self.sendMessage(message)
506
507 def sendMessage(self, message, noerror=False):
508 # Check to make sure that clientcaps only gets sent after
509 # the first text type message.
510 if isinstance(message, msn.MSNMessage) and message.getHeader("Content-Type").startswith("text"):
511 self.sendMessage = self.sendMessageReal
512 self.sendClientCaps()
513 return self.sendMessage(message, noerror)
514 else:
515 return self.sendMessageReal(message, noerror)
516
517 def sendMessageReal(self, text, noerror=False):
518 if not isinstance(text, basestring):
519 msn.SwitchboardClient.sendMessage(self, text)
520 return
521 if not self.ready:
522 self.messageBuffer.append((text, noerror))
523 else:
524 LogEvent(INFO, self.ident)
525 def failedMessage(ignored):
526 if not noerror:
527 self.failedMessage(text)
528
529 if len(text) < MAXMESSAGESIZE:
530 message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
531 message.setHeader("Content-Type", "text/plain; charset=UTF-8")
532 message.ack = msn.MSNMessage.MESSAGE_NACK
533
534 d = msn.SwitchboardClient.sendMessage(self, message)
535 if not noerror:
536 d.addCallback(failedMessage)
537
538 else:
539 chunks = int(math.ceil(len(text) / float(MAXMESSAGESIZE)))
540 chunk = 0
541 guid = msn.random_guid()
542 while chunk < chunks:
543 offset = chunk * MAXMESSAGESIZE
544 text = message[offset : offset + MAXMESSAGESIZE]
545 message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
546 message.ack = msn.MSNMessage.MESSAGE_NACK
547 if chunk == 0:
548 message.setHeader("Content-Type", "text/plain; charset=UTF-8")
549 message.setHeader("Chunks", str(chunks))
550 else:
551 message.setHeader("Chunk", str(chunk))
552
553 d = msn.SwitchboardClient.sendMessage(self, message)
554 if not noerror:
555 d.addCallback(failedMessage)
556
557 chunk += 1
558
559
560 class MultiSwitchboardSession(SwitchboardSessionBase):
561 """ Create one of me to chat to multiple contacts """
562
563 def __init__(self, msncon):
564 """ Automatically creates a new switchboard connection to the server """
565 SwitchboardSessionBase.__init__(self, msncon)
566 self.ident = (self.msncon.ident, self)
567 self.contactCount = 0
568 self.groupchat = None
569 self.connect()
570
571 def failedMessage(self, text):
572 self.groupchat.gotMessage("BOUNCE", text)
573
574 def sendMessage(self, text, noerror=False):
575 """ Used to send a mesage to the groupchat. Can be called immediately
576 after instantiation. """
577 if self.contactCount > 0:
578 SwitchboardSessionBase.sendMessage(self, text, noerror)
579 else:
580 #self.messageBuffer.append((message, noerror))
581 pass # They're sending messages to an empty room. Ignore.
582
583 def inviteUser(self, userHandle):
584 """ Used to invite a contact to the groupchat. Can be called immediately
585 after instantiation. """
586 userHandle = str(userHandle)
587 if self.ready:
588 LogEvent(INFO, self.ident, "immediate")
589 msn.SwitchboardClient.inviteUser(self, userHandle)
590 else:
591 LogEvent(INFO, self.ident, "pending")
592 self.funcBuffer.append(lambda: msn.SwitchboardClient.inviteUser(self, userHandle))
593
594 def gotMessage(self, message):
595 self.groupchat.gotMessage(message.userHandle, message.getMessage())
596
597 def userJoined(self, userHandle, screenName=''):
598 LogEvent(INFO, self.ident)
599 self.contactCount += 1
600 self.groupchat.contactJoined(userHandle)
601
602 def userLeft(self, userHandle):
603 LogEvent(INFO, self.ident)
604 self.contactCount -= 1
605 self.groupchat.contactLeft(userHandle)
606
607
608
609 class OneSwitchboardSession(SwitchboardSessionBase):
610 def __init__(self, msncon, remoteUser):
611 SwitchboardSessionBase.__init__(self, msncon)
612 self.remoteUser = str(remoteUser)
613 self.ident = (self.msncon.ident, self.remoteUser)
614 self.chattingUsers = []
615 self.timeout = None
616
617 def __del__(self):
618 for message, noerror in self.messageBuffer:
619 if not noerror:
620 self.failedMessage(self.remoteUser, message)
621
622 def _ready(self):
623 LogEvent(INFO, self.ident)
624 self.ready = True
625 for user in self.chattingUsers:
626 self.userJoined(user)
627 if self.timeout:
628 self.timeout.cancel()
629 self.timeout = None
630 self.flushBuffer()
631
632 def _switchToMulti(self, userHandle):
633 LogEvent(INFO, self.ident)
634 del self.msncon.switchboardSessions[self.remoteUser]
635 self.__class__ = MultiSwitchboardSession
636 del self.remoteUser
637 self.contactCount = 0
638 self.msncon.gotGroupchat(self, userHandle)
639 if not self.groupchat:
640 LogEvent(ERROR, self.ident)
641 raise Exception("YouNeedAGroupchat-WeHaveAProblemError") # FIXME
642
643 def failedMessage(self, text):
644 self.msncon.failedMessage(self.remoteUser, text)
645
646 # Callbacks
647 def loggedIn(self):
648 LogEvent(INFO, self.ident)
649 if not self.reply:
650 def failCB(arg=None):
651 LogEvent(INFO, self.ident, "User has not joined after 30 seconds.")
652 del self.msncon.switchboardSessions[self.remoteUser]
653 d = self.inviteUser(self.remoteUser)
654 d.addErrback(failCB)
655 self.timeout = reactor.callLater(30.0, failCB)
656 else:
657 self._ready()
658
659 def gotChattingUsers(self, users):
660 for userHandle in users.keys():
661 self.chattingUsers.append(userHandle)
662
663 def userJoined(self, userHandle, screenName=''):
664 LogEvent(INFO, self.ident)
665 if not self.reply:
666 self._ready()
667 if userHandle != self.remoteUser:
668 # Another user has joined, so we now have three participants.
669 remoteUser = self.remoteUser
670 self._switchToMulti(remoteUser)
671 self.userJoined(remoteUser)
672 self.userJoined(userHandle)
673 else:
674 def updateAvatarCB((imageData, )):
675 if self.msncon:
676 self.msncon.gotAvatarImageData(self.remoteUser, imageData)
677 d = self.sendAvatarRequest()
678 if d:
679 d.addCallback(updateAvatarCB)
680
681 def userLeft(self, userHandle):
682 def wait():
683 if userHandle == self.remoteUser:
684 del self.msncon.switchboardSessions[self.remoteUser]
685 reactor.callLater(0, wait) # Make sure this is handled after everything else
686
687 def gotMessage(self, message):
688 LogEvent(INFO, self.ident)
689 cTypes = [s.strip() for s in message.getHeader("Content-Type").split(';')]
690 if "text/plain" == cTypes[0]:
691 try:
692 if len(cTypes) > 1 and cTypes[1].lower().find("utf-8") >= 0:
693 text = message.getMessage().decode("utf-8")
694 else:
695 text = message.getMessage()
696 self.msncon.gotMessage(self.remoteUser, text)
697 except:
698 self.msncon.gotMessage("A message was lost.")
699 raise
700 elif "text/x-clientcaps" == cTypes[0]:
701 if message.hasHeader("JabberID"):
702 jid = message.getHeader("JabberID")
703 self.switchboardSession.msncon.userMapping(message.userHandle, jid)
704 else:
705 LogEvent(INFO, self.ident, "Discarding unknown message type.")
706
707 def gotFileReceive(self, fileReceive):
708 LogEvent(INFO, self.ident)
709 self.msncon.gotFileReceive(fileReceive)
710
711 def gotContactTyping(self, message):
712 LogEvent(INFO, self.ident)
713 self.msncon.gotContactTyping(message.userHandle)
714
715 def sendTypingNotification(self):
716 LogEvent(INFO, self.ident)
717 if self.ready:
718 msn.SwitchboardClient.sendTypingNotification(self)
719
720 CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
721 def sendAvatarRequest(self):
722 if not self.ready: return
723 msnContacts = self.msncon.getContacts()
724 if not msnContacts: return
725 msnContact = msnContacts.getContact(self.remoteUser)
726 if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
727 if msnContact.msnobjGot: return
728 msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
729 return msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
730
731 def sendFile(self, msnContact, filename, filesize):
732 def doSendFile(ignored=None):
733 d.callback(msn.SwitchboardClient.sendFile(self, msnContact, filename, filesize))
734 d = Deferred()
735 if self.ready:
736 reactor.callLater(0, doSendFile)
737 else:
738 self.funcBuffer.append(doSendFile)
739 return d
740