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