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