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