]>
code.delx.au - pymsnt/blob - 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
5 from twisted
.internet
import reactor
6 from twisted
.internet
.defer
import Deferred
7 from twisted
.internet
.protocol
import ClientFactory
10 import math
, base64
, binascii
, math
13 from debug
import LogEvent
, INFO
, WARN
, ERROR
14 from tlib
.msn
import msn
18 SWITCHBOARDTIMEOUT
= 30.0*60.0
23 All interaction should be with the MSNConnection class.
24 You should not directly instantiate any objects of other classes.
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.
35 self
.username
= username
36 self
.password
= password
39 self
.notificationClient
= None
41 LogEvent(INFO
, self
.ident
)
44 """ Automatically called by the constructor """
46 self
.switchboardSessions
= {}
47 self
.savedEvents
= SavedEvents() # Save any events that occur before connect
48 self
._getNotificationReferral
()
50 def _getNotificationReferral(self
):
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
59 d
.addCallbacks(self
._gotNotificationReferral
, self
.connectionFailed
)
60 self
.connectors
.append(reactor
.connectTCP("messenger.hotmail.com", 1863, dispatchFactory
))
61 LogEvent(INFO
, self
.ident
)
63 def _gotNotificationReferral(self
, (host
, port
)):
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
)
74 def _sendSavedEvents(self
):
75 self
.savedEvents
.send(self
)
76 self
.savedEvents
= None
78 def _notificationClientReady(self
, notificationClient
):
79 self
.notificationClient
= notificationClient
81 def _ensureSwitchboardSession(self
, userHandle
):
82 if not self
.switchboardSessions
.has_key(userHandle
):
83 sb
= OneSwitchboardSession(self
, userHandle
)
85 self
.switchboardSessions
[userHandle
] = sb
90 def getContacts(self
):
91 """ Gets the contact list.
93 @return an instance of MSNContactList (do not modify) if connected,
96 if self
.notificationFactory
:
97 return self
.notificationFactory
.contacts
101 def sendMessage(self
, userHandle
, text
, noerror
=False):
103 Sends a message to a contact. Can only be called after listSynchronized().
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.
109 LogEvent(INFO
, self
.ident
)
110 if self
.notificationClient
:
111 self
._ensureSwitchboardSession
(userHandle
)
112 self
.switchboardSessions
[userHandle
].sendMessage(text
, noerror
)
114 self
.failedMessage(userHandle
, text
)
116 def sendAvatarRequest(self
, userHandle
):
118 Requests the avatar of a contact.
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
127 LogEvent(INFO
, self
.ident
)
128 if not self
.notificationClient
: return
130 self
._ensureSwitchboardSession
(userHandle
)
131 sb
= self
.switchboardSessions
.get(userHandle
)
132 if sb
: return sb
.sendAvatarRequest()
134 def sendFile(self
, userHandle
, filename
, filesize
):
136 Used to send a file to a contact.
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.
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
152 msnContact
= self
.getContacts().getContact(userHandle
)
154 raise ValueError, "Contact not found"
155 self
._ensureSwitchboardSession
(userHandle
)
156 return self
.switchboardSessions
[userHandle
].sendFile(msnContact
, filename
, filesize
)
158 def sendTypingToContact(self
, userHandle
):
160 Sends typing notification to a contact.
161 @param userHandle: the contact to notify of our typing.
164 sb
= self
.switchboardSessions
.get(userHandle
)
165 if sb
: return sb
.sendTypingNotification()
167 def changeAvatar(self
, imageData
):
169 Changes the user's avatar.
170 @param imageData: the new PNG avatar image data.
172 if self
.notificationClient
:
173 LogEvent(INFO
, self
.ident
)
174 self
.notificationClient
.changeAvatar(imageData
, push
=True)
176 self
.savedEvents
.avatarImageData
= imageData
178 def changeStatus(self
, statusCode
, screenName
, personal
):
180 Changes your status details. All details must be given with
181 each call. This can be called before connection if you wish.
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.
188 if self
.notificationClient
:
190 def cb(ignored
=None):
192 self
.ourStatusChanged(statusCode
, screenName
, personal
)
193 LogEvent(INFO
, self
.ident
)
194 self
.notificationClient
.changeStatus(statusCode
.encode("utf-8")).addCallback(cb
)
195 self
.notificationClient
.changeScreenName(screenName
.encode("utf-8")).addCallback(cb
)
196 if not personal
: personal
= ""
197 self
.notificationClient
.changePersonalMessage(personal
.encode("utf-8"))
199 self
.savedEvents
.statusCode
= statusCode
200 self
.savedEvents
.screenName
= screenName
201 self
.savedEvents
.personal
= personal
203 def addContact(self
, listType
, userHandle
):
204 """ See msn.NotificationClient.addContact """
205 if self
.notificationClient
:
206 return self
.notificationClient
.addContact(listType
, str(userHandle
))
208 self
.savedEvents
.addContacts
.append((listType
, str(userHandle
)))
210 def remContact(self
, listType
, userHandle
):
211 """ See msn.NotificationClient.remContact """
212 if self
.notificationClient
:
213 return self
.notificationClient
.remContact(listType
, str(userHandle
))
215 self
.savedEvents
.remContacts
.append((listType
, str(userHandle
)))
218 """ Shuts down the whole connection. Don't try to call any
219 other methods after this one. """
220 if self
.notificationClient
:
221 self
.notificationClient
.logOut()
222 for c
in self
.connectors
:
224 if self
.notificationFactory
:
225 self
.notificationFactory
.msncon
= None
227 self
.switchboardSessions
= {}
228 LogEvent(INFO
, self
.ident
)
232 def connectionFailed(self
, reason
=''):
233 """ Called when the connection to the server failed. """
235 def connectionLost(self
, reason
=''):
236 """ Called when we are disconnected. """
238 def multipleLogin(self
):
239 """ Called when the server says there has been another login
240 for this account. """
242 def serverGoingDown(self
):
243 """ Called when the server says that it will be going down. """
245 def accountNotVerified(self
):
246 """ Called if this passport has not been verified. Certain
247 functions are not available. """
249 def userMapping(self
, passport
, jid
):
250 """ Called when it is brought to our attention that one of the
251 MSN contacts has a Jabber ID. You should communicate with Jabber. """
254 """ Called when we have authenticated, but before we receive
255 the contact list. """
257 def listSynchronized(self
):
258 """ Called when we have received the contact list. All methods
259 in this class are now valid. """
261 def ourStatusChanged(self
, statusCode
, screenName
, personal
):
262 """ Called when the user's status has changed. """
264 def gotMessage(self
, userHandle
, text
):
265 """ Called when a contact sends us a message """
267 def gotContactTyping(self
, userHandle
):
268 """ Called when a contact sends typing notification """
270 def failedMessage(self
, userHandle
, text
):
271 """ Called when a message we sent has been bounced back. """
273 def contactAvatarChanged(self
, userHandle
, hash):
274 """ Called when we receive a changed avatar hash for a contact.
275 You should call sendAvatarRequest(). """
277 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
278 """ Called when we receive status information for a contact. """
280 def contactPersonalChanged(self
, personal
):
281 """ Called when we receive a new personal message for a contact. """
283 def gotFileReceive(self
, fileReceive
):
284 """ Called when a contact sends the user a file.
285 Call accept(fileHandle) or reject() on the object. """
287 def contactAddedMe(self
, userHandle
):
288 """ Called when a contact adds the user to their list. """
290 def contactRemovedMe(self
, userHandle
):
291 """ Called when a contact removes the user from their list. """
299 self
.avatarImageData
= ""
300 self
.addContacts
= []
301 self
.remContacts
= []
303 def send(self
, msncon
):
304 if self
.avatarImageData
:
305 msncon
.notificationClient
.changeAvatar(self
.avatarImageData
, push
=False)
306 if self
.nickname
or self
.statusCode
or self
.personal
:
307 msncon
.changeStatus(self
.statusCode
, self
.nickname
, self
.personal
)
308 for listType
, userHandle
in self
.addContacts
:
309 msncon
.addContact(listType
, userHandle
)
310 for listType
, userHandle
in self
.remContacts
:
311 msncon
.remContact(listType
, userHandle
)
315 class DispatchClient(msn
.DispatchClient
):
316 def gotNotificationReferral(self
, host
, port
):
317 if self
.factory
.d
.called
: return # Too slow! We've already timed out
318 self
.factory
.d
.callback((host
, port
))
321 class NotificationClient(msn
.NotificationClient
):
322 def loginFailure(self
, message
):
323 self
.factory
.msncon
.connectionFailed(message
)
325 def loggedIn(self
, userHandle
, verified
):
326 LogEvent(INFO
, self
.factory
.msncon
.ident
)
327 msn
.NotificationClient
.loggedIn(self
, userHandle
, verified
)
328 self
.factory
.msncon
._notificationClientReady
(self
)
329 self
.factory
.msncon
.loggedIn()
331 self
.factory
.msncon
.accountNotVerified()
334 msn
.NotificationClient
.logOut(self
)
336 def connectionLost(self
, reason
):
337 if not self
.factory
.msncon
: return # If we called logOut
339 LogEvent(INFO
, self
.factory
.msncon
.ident
)
340 msn
.NotificationClient
.connectionLost(self
, reason
)
341 self
.factory
.msncon
.connectionLost(reason
)
342 # Make sure this event is handled after any others
343 reactor
.callLater(0, wait
)
345 def gotMSNAlert(self
, body
, action
, subscr
):
346 LogEvent(INFO
, self
.factory
.msncon
.ident
)
347 self
.factory
.msncon
.gotMSNAlert(body
, action
, subscr
)
349 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
350 LogEvent(INFO
, self
.factory
.msncon
.ident
)
351 self
.factory
.msncon
.gotInitialEmailNotification(inboxunread
, foldersunread
)
353 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
354 LogEvent(INFO
, self
.factory
.msncon
.ident
)
355 self
.factory
.msncon
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
357 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
358 LogEvent(INFO
, self
.factory
.msncon
.ident
)
359 self
.factory
.msncon
.contactAddedMe(userHandle
)
361 def userRemovedMe(self
, userHandle
):
362 LogEvent(INFO
, self
.factory
.msncon
.ident
)
363 self
.factory
.msncon
.contactRemovedMe(userHandle
)
365 def listSynchronized(self
, *args
):
366 LogEvent(INFO
, self
.factory
.msncon
.ident
)
367 self
.factory
.msncon
._sendSavedEvents
()
368 self
.factory
.msncon
.listSynchronized()
370 def contactAvatarChanged(self
, userHandle
, hash):
371 LogEvent(INFO
, self
.factory
.msncon
.ident
)
372 self
.factory
.msncon
.contactAvatarChanged(userHandle
, hash)
374 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
375 LogEvent(INFO
, self
.factory
.msncon
.ident
)
376 self
.factory
.msncon
.contactStatusChanged(userHandle
, statusCode
, screenName
)
378 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
379 LogEvent(INFO
, self
.factory
.msncon
.ident
)
380 self
.factory
.msncon
.contactStatusChanged(userHandle
, statusCode
, screenName
)
382 def contactOffline(self
, userHandle
):
383 LogEvent(INFO
, self
.factory
.msncon
.ident
)
384 self
.factory
.msncon
.contactStatusChanged(userHandle
, msn
.STATUS_OFFLINE
, "")
386 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
387 LogEvent(INFO
, self
.factory
.msncon
.ident
)
388 sb
= self
.factory
.msncon
.switchboardSessions
.get(userHandle
)
390 sb
.transport
.loseConnection()
392 sb
= OneSwitchboardSession(self
.factory
.msncon
, userHandle
)
393 self
.factory
.msncon
.switchboardSessions
[userHandle
] = sb
394 sb
.connectReply(host
, port
, key
, sessionID
)
396 def multipleLogin(self
):
397 LogEvent(INFO
, self
.factory
.msncon
.ident
)
398 self
.factory
.msncon
.multipleLogin()
400 def serverGoingDown(self
):
401 LogEvent(INFO
, self
.factory
.msncon
.ident
)
402 self
.factory
.msncon
.serverGoingDown()
406 def switchToGroupchat(*args
):
407 raise NotImplementedError
410 class SwitchboardSessionBase
:
411 def __init__(self
, msncon
):
413 self
.userHandle
= msncon
.username
414 self
.ident
= (msncon
.ident
, "INVALID!!")
415 self
.messageBuffer
= []
420 LogEvent(INFO
, self
.ident
)
422 self
.transport
.disconnect()
424 for message
, noerror
in self
.messageBuffer
:
426 self
.msncon
.failedMessage(self
.remoteUser
, message
)
429 LogEvent(INFO
, self
.ident
)
431 def sbRequestAccepted((host
, port
, key
)):
432 LogEvent(INFO
, self
.ident
)
435 factory
= ClientFactory()
436 factory
.buildProtocol
= lambda addr
: self
437 reactor
.connectTCP(host
, port
, factory
)
438 def sbRequestFailed(ignored
=None):
439 LogEvent(INFO
, self
.ident
)
440 del self
.msncon
.switchboardSessions
[self
.remoteUser
]
441 d
= self
.msncon
.notificationClient
.requestSwitchboardServer()
442 d
.addCallbacks(sbRequestAccepted
, sbRequestFailed
)
444 def connectReply(self
, host
, port
, key
, sessionID
):
445 LogEvent(INFO
, self
.ident
)
448 self
.sessionID
= sessionID
450 factory
= ClientFactory()
451 factory
.buildProtocol
= lambda addr
: self
452 reactor
.connectTCP(host
, port
, factory
)
454 def flushBuffer(self
):
455 for message
, noerror
in self
.messageBuffer
[:]:
456 self
.messageBuffer
.remove((message
, noerror
))
457 self
.sendMessage(message
, noerror
)
458 for f
in self
.funcBuffer
[:]:
459 self
.funcBuffer
.remove(f
)
462 def failedMessage(self
, ignored
):
463 raise NotImplementedError
465 def sendMessage(self
, text
, noerror
=False):
466 if not isinstance(text
, (str, unicode)):
467 msn
.SwitchboardClient
.sendMessage(self
, text
)
470 self
.messageBuffer
.append((text
, noerror
))
472 LogEvent(INFO
, self
.ident
)
473 def failedMessage(ignored
):
475 self
.failedMessage(text
)
477 if len(text
) < MAXMESSAGESIZE
:
478 message
= msn
.MSNMessage(message
=str(text
.replace("\n", "\r\n").encode("utf-8")))
479 message
.setHeader("Content-Type", "text/plain; charset=UTF-8")
480 message
.ack
= msn
.MSNMessage
.MESSAGE_NACK
482 d
= msn
.SwitchboardClient
.sendMessage(self
, message
)
484 d
.addCallback(failedMessage
)
487 chunks
= int(math
.ceil(len(text
) / float(MAXMESSAGESIZE
)))
489 guid
= msn
.random_guid()
490 while chunk
< chunks
:
491 offset
= chunk
* MAXMESSAGESIZE
492 text
= message
[offset
: offset
+ MAXMESSAGESIZE
]
493 message
= msn
.MSNMessage(message
=str(text
.replace("\n", "\r\n").encode("utf-8")))
494 message
.ack
= msn
.MSNMessage
.MESSAGE_NACK
496 message
.setHeader("Content-Type", "text/plain; charset=UTF-8")
497 message
.setHeader("Chunks", str(chunks
))
499 message
.setHeader("Chunk", str(chunk
))
501 d
= msn
.SwitchboardClient
.sendMessage(self
, message
)
503 d
.addCallback(failedMessage
)
509 class OneSwitchboardSession(SwitchboardSessionBase
, msn
.SwitchboardClient
):
510 def __init__(self
, msncon
, remoteUser
):
511 SwitchboardSessionBase
.__init
__(self
, msncon
)
512 msn
.SwitchboardClient
.__init
__(self
)
513 self
.remoteUser
= str(remoteUser
)
514 self
.ident
= (self
.msncon
, self
.remoteUser
)
515 self
.chattingUsers
= []
519 LogEvent(INFO
, self
.ident
)
521 for user
in self
.chattingUsers
:
522 self
.userJoined(user
)
524 self
.timeout
.cancel()
528 def failedMessage(self
, text
):
529 self
.msncon
.failedMessage(self
.remoteUser
, text
)
533 LogEvent(INFO
, self
.ident
)
535 def failCB(arg
=None):
536 LogEvent(INFO
, ident
, "User has not joined after 30 seconds.")
537 del self
.msncon
.switchboardSessions
[self
.remoteUser
]
538 d
= self
.inviteUser(self
.remoteUser
)
540 self
.timeout
= reactor
.callLater(30.0, failCB
)
544 def gotChattingUsers(self
, users
):
545 for userHandle
in users
.keys():
546 self
.chattingUsers
.append(userHandle
)
548 def userJoined(self
, userHandle
, screenName
=''):
549 LogEvent(INFO
, self
.ident
)
552 if userHandle
!= self
.remoteUser
:
553 # Another user has joined, so we now have three participants.
554 switchToGroupchat(self
, self
.remoteUser
, userHandle
)
556 def userLeft(self
, userHandle
):
558 if userHandle
== self
.remoteUser
:
559 del self
.msncon
.switchboardSessions
[self
.remoteUser
]
560 reactor
.callLater(0, wait
) # Make sure this is handled after everything else
562 def gotMessage(self
, message
):
563 LogEvent(INFO
, self
.ident
)
564 self
.msncon
.gotMessage(self
.remoteUser
, message
.getMessage())
566 def gotFileReceive(self
, fileReceive
):
567 LogEvent(INFO
, self
.ident
)
568 self
.msncon
.gotFileReceive(fileReceive
)
570 def gotContactTyping(self
, message
):
571 LogEvent(INFO
, self
.ident
)
572 self
.msncon
.gotContactTyping(message
.userHandle
)
574 def sendTypingNotification(self
):
575 LogEvent(INFO
, self
.ident
)
577 msn
.SwitchboardClient
.sendTypingNotification(self
)
579 CAPS
= msn
.MSNContact
.MSNC1 | msn
.MSNContact
.MSNC2 | msn
.MSNContact
.MSNC3 | msn
.MSNContact
.MSNC4
580 def sendAvatarRequest(self
):
581 if not self
.ready
: return
582 msnContacts
= self
.msncon
.getContacts()
583 if not msnContacts
: return
584 msnContact
= msnContacts
.getContact(self
.remoteUser
)
585 if not (msnContact
and msnContact
.caps
& self
.CAPS
and msnContact
.msnobj
): return
586 if msnContact
.msnobjGot
: return
587 msnContact
.msnobjGot
= True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
588 msn
.SwitchboardClient
.sendAvatarRequest(self
, msnContact
)
590 def sendFile(self
, msnContact
, filename
, filesize
):
591 def doSendFile(ignored
=None):
592 d
.callback(msn
.SwitchboardClient
.sendFile(self
, msnContact
, filename
, filesize
))
595 reactor
.callLater(0, doSendFile
)
597 self
.funcBuffer
.append(doSendFile
)