]>
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
17 from msn
import FORWARD_LIST
, ALLOW_LIST
, BLOCK_LIST
, REVERSE_LIST
, PENDING_LIST
18 from msn
import STATUS_ONLINE
, STATUS_OFFLINE
, STATUS_HIDDEN
, STATUS_IDLE
, STATUS_AWAY
, STATUS_BUSY
, STATUS_BRB
, STATUS_PHONE
, STATUS_LUNCH
21 SWITCHBOARDTIMEOUT
= 30.0*60.0
26 All interaction should be with the MSNConnection class.
27 You should not directly instantiate any objects of other classes.
31 """ Manages all the Twisted factories, etc """
32 def __init__(self
, username
, password
, ident
):
33 """ Connects to the MSN servers.
34 @param username: the MSN passport to connect with.
35 @param password: the password for this account.
36 @param ident: a unique identifier to use in logging.
38 self
.username
= username
39 self
.password
= password
43 LogEvent(INFO
, self
.ident
)
46 """ Automatically called by the constructor """
48 self
.switchboardSessions
= {}
49 self
.savedEvents
= SavedEvents() # Save any events that occur before connect
50 self
._getNotificationReferral
()
52 def _getNotificationReferral(self
):
54 if not d
.called
: d
.errback()
55 self
.timeout
= reactor
.callLater(30, timeout
)
56 dispatchFactory
= msn
.DispatchFactory()
57 dispatchFactory
.userHandle
= self
.username
58 dispatchFactory
.protocol
= DispatchClient
61 d
.addCallbacks(self
._gotNotificationReferral
, self
.connectionFailed
)
62 self
.connectors
.append(reactor
.connectTCP("messenger.hotmail.com", 1863, dispatchFactory
))
63 LogEvent(INFO
, self
.ident
)
65 def _gotNotificationReferral(self
, (host
, port
)):
67 # Create the NotificationClient
68 self
.notificationFactory
= msn
.NotificationFactory()
69 self
.notificationFactory
.userHandle
= self
.username
70 self
.notificationFactory
.password
= self
.password
71 self
.notificationFactory
.msncon
= self
72 self
.notificationFactory
.protocol
= NotificationClient
73 self
.connectors
.append(reactor
.connectTCP(host
, port
, self
.notificationFactory
))
74 LogEvent(INFO
, self
.ident
)
76 def _sendSavedEvents(self
):
77 self
.savedEvents
.send(self
)
78 self
.savedEvents
= None
80 def _notificationClientReady(self
, notificationClient
):
81 self
.notificationClient
= notificationClient
83 def _ensureSwitchboardSession(self
, userHandle
):
84 if not self
.switchboardSessions
.has_key(userHandle
):
85 sb
= OneSwitchboardSession(self
, userHandle
)
87 self
.switchboardSessions
[userHandle
] = sb
92 def getContacts(self
):
93 """ Gets the contact list.
95 @return an instance of MSNContactList (do not modify) if connected,
98 if self
.notificationFactory
:
99 return self
.notificationFactory
.contacts
103 def sendMessage(self
, userHandle
, text
, noerror
=False):
105 Sends a message to a contact. Can only be called after listSynchronized().
107 @param userHandle: the contact's MSN passport.
108 @param text: the text to send.
109 @param noerror: Set this to True if you don't want failed messages to bounce.
111 LogEvent(INFO
, self
.ident
)
112 if self
.notificationClient
:
113 self
._ensureSwitchboardSession
(userHandle
)
114 self
.switchboardSessions
[userHandle
].sendMessage(text
, noerror
)
116 self
.failedMessage(userHandle
, text
)
118 def requestAvatar(self
, userHandle
):
120 Requests the avatar of a contact.
122 @param userHandle: the contact to request an avatar from.
123 @return: a Deferred() if the avatar can be fetched at this time.
124 This will fire with an argument of a tuple with the PNG
125 image data as the only element.
126 Otherwise returns None
129 LogEvent(INFO
, self
.ident
)
130 if not self
.notificationClient
: return
132 self
._ensureSwitchboardSession
(userHandle
)
133 sb
= self
.switchboardSessions
.get(userHandle
)
134 if sb
: return sb
.sendAvatarRequest()
136 def sendFile(self
, userHandle
, filename
, filesize
):
138 Used to send a file to a contact.
140 @param username: the passport of the contact to send a file to.
141 @param filename: the name of the file to send.
142 @param filesize: the size of the file to send.
144 @return: (fileSend, d) A FileSend object and a Deferred.
145 The Deferred will pass one argument in a tuple,
146 whether or not the transfer is accepted. If you
147 receive a True, then you can call write() on the
148 fileSend object to send your file. Call close()
149 when the file is done.
150 NOTE: You MUST write() exactly as much as you
153 raise NotImplementedError # May have to wait for the switchboardSession to exist
155 def sendTypingToContact(self
, userHandle
):
157 Sends typing notification to a contact.
158 @param userHandle: the contact to notify of our typing.
161 sb
= self
.switchboardSessions
.get(userHandle
)
162 if sb
: return sb
.sendTypingNotification()
164 def changeAvatar(self
, imageData
):
166 Changes the user's avatar.
167 @param imageData: the new PNG avatar image data.
169 if self
.notificationClient
:
170 LogEvent(INFO
, self
.ident
)
171 self
.notificationClient
.changeAvatar(imageData
, push
=True)
173 self
.savedEvents
.avatarImageData
= imageData
175 def changeStatus(self
, statusCode
, screenName
, personal
):
177 Changes your status details. All details must be given with
178 each call. This can be called before connection if you wish.
180 @param statusCode: the user's new status (look in msn.statusCodes).
181 @param screenName: the user's new screenName (up to 127 characters).
182 @param personal: the user's new personal message.
185 if self
.notificationClient
:
189 self
.ourStatusChanged(statusCode
, screenName
, personal
)
190 LogEvent(INFO
, self
.ident
)
191 self
.notificationClient
.changeStatus(statusCode
.encode("utf-8")).addCallback(cb
)
192 self
.notificationClient
.changeScreenName(screenName
.encode("utf-8")).addCallback(cb
)
193 if not personal
: personal
= ""
194 self
.notificationClient
.changePersonalMessage(personal
.encode("utf-8"))
196 self
.savedEvents
.statusCode
= statusCode
197 self
.savedEvents
.screenName
= screenName
198 self
.savedEvents
.personal
= personal
200 def addContact(self
, listType
, userHandle
):
201 """ See msn.NotificationClient.addContact """
202 if self
.notificationClient
:
203 return self
.notificationClient
.addContact(listType
, str(userHandle
))
205 self
.savedEvents
.addContacts
.append((listType
, str(userHandle
)))
207 def remContact(self
, listType
, userHandle
):
208 """ See msn.NotificationClient.remContact """
209 if self
.notificationClient
:
210 return self
.notificationClient
.remContact(listType
, str(userHandle
))
212 self
.savedEvents
.remContacts
.append((listType
, str(userHandle
)))
215 """ Shuts down the whole connection. Don't try to call any
216 other methods after this one. """
217 if self
.notificationClient
:
218 self
.notificationClient
.logOut()
219 for c
in self
.connectors
:
221 if self
.notificationFactory
:
222 self
.notificationFactory
.msncon
= None
224 self
.switchboardSessions
= {}
225 LogEvent(INFO
, self
.ident
)
229 def connectionFailed(self
, reason
=''):
230 """ Called when the connection to the server failed. """
232 def connectionLost(self
, reason
=''):
233 """ Called when we are disconnected. """
235 def multipleLogin(self
):
236 """ Called when the server says there has been another login
237 for this account. """
239 def serverGoingDown(self
):
240 """ Called when the server says that it will be going down. """
242 def accountNotVerified(self
):
243 """ Called if this passport has not been verified. Certain
244 functions are not available. """
246 def userMapping(self
, passport
, jid
):
247 """ Called when it is brought to our attention that one of the
248 MSN contacts has a Jabber ID. You should communicate with Jabber. """
251 """ Called when we have authenticated, but before we receive
252 the contact list. """
254 def listSynchronized(self
):
255 """ Called when we have received the contact list. All methods
256 in this class are now valid. """
258 def ourStatusChanged(self
, statusCode
, screenName
, personal
):
259 """ Called when the user's status has changed. """
261 def gotMessage(self
, userHandle
, text
):
262 """ Called when a contact sends us a message """
264 def failedMessage(self
, userHandle
, text
):
265 """ Called when a message we sent has been bounced back. """
267 def contactAvatarChanged(self
, userHandle
, hash):
268 """ Called when we receive a changed avatar hash for a contact.
269 You should call requestAvatar(). """
271 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
272 """ Called when we receive status information for a contact. """
274 def contactPersonalChanged(self
, personal
):
275 """ Called when we receive a new personal message for a contact. """
277 def gotFileReceive(self
, fileReceive
):
278 """ Called when a contact sends the user a file.
279 Call accept(fileHandle) or reject() on the object. """
281 def contactAddedMe(self
, userHandle
):
282 """ Called when a contact adds the user to their list. """
284 def contactRemovedMe(self
, userHandle
):
285 """ Called when a contact removes the user from their list. """
293 self
.avatarImageData
= ""
294 self
.addContacts
= []
295 self
.remContacts
= []
297 def send(self
, msncon
):
298 if self
.avatarImageData
:
299 msncon
.notificationClient
.changeAvatar(self
.avatarImageData
, push
=False)
300 if self
.nickname
or self
.statusCode
or self
.personal
:
301 msncon
.changeStatus(self
.statusCode
, self
.nickname
, self
.personal
)
302 for listType
, userHandle
in self
.addContacts
:
303 msncon
.addContact(listType
, userHandle
)
304 for listType
, userHandle
in self
.remContacts
:
305 msncon
.remContact(listType
, userHandle
)
309 class DispatchClient(msn
.DispatchClient
):
310 def gotNotificationReferral(self
, host
, port
):
311 if self
.factory
.d
.called
: return # Too slow! We've already timed out
312 self
.factory
.d
.callback((host
, port
))
315 class NotificationClient(msn
.NotificationClient
):
316 def loginFailure(self
, message
):
317 self
.factory
.msncon
.connectionFailed(message
)
319 def loggedIn(self
, userHandle
, verified
):
320 LogEvent(INFO
, self
.factory
.msncon
.ident
)
321 msn
.NotificationClient
.loggedIn(self
, userHandle
, verified
)
322 self
.factory
.msncon
._notificationClientReady
(self
)
324 self
.factory
.msncon
.accountNotVerified()
327 msn
.NotificationClient
.logOut(self
)
329 def connectionLost(self
, reason
):
330 if not self
.factory
.msncon
: return # If we called logOut
332 LogEvent(INFO
, self
.factory
.msncon
.ident
)
333 msn
.NotificationClient
.connectionLost(self
, reason
)
334 self
.factory
.msncon
.connectionLost(reason
)
335 # Make sure this event is handled after any others
336 reactor
.callLater(0, wait
)
338 def gotMSNAlert(self
, body
, action
, subscr
):
339 LogEvent(INFO
, self
.factory
.msncon
.ident
)
340 self
.factory
.msncon
.gotMSNAlert(body
, action
, subscr
)
342 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
343 LogEvent(INFO
, self
.factory
.msncon
.ident
)
344 self
.factory
.msncon
.gotInitialEmailNotification(inboxunread
, foldersunread
)
346 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
347 LogEvent(INFO
, self
.factory
.msncon
.ident
)
348 self
.factory
.msncon
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
350 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
351 LogEvent(INFO
, self
.factory
.msncon
.ident
)
352 self
.factory
.msncon
.contactAddedMe(userHandle
)
354 def userRemovedMe(self
, userHandle
):
355 LogEvent(INFO
, self
.factory
.msncon
.ident
)
356 self
.factory
.msncon
.contactRemovedMe(userHandle
)
358 def listSynchronized(self
, *args
):
359 LogEvent(INFO
, self
.factory
.msncon
.ident
)
360 self
.factory
.msncon
._sendSavedEvents
()
361 self
.factory
.msncon
.listSynchronized()
363 def contactAvatarChanged(self
, userHandle
, hash):
364 LogEvent(INFO
, self
.factory
.msncon
.ident
)
365 self
.factory
.msncon
.contactAvatarChanged(userHandle
, hash)
367 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
368 LogEvent(INFO
, self
.factory
.msncon
.ident
)
369 self
.factory
.msncon
.contactStatusChanged(userHandle
, statusCode
, screenName
)
371 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
372 LogEvent(INFO
, self
.factory
.msncon
.ident
)
373 self
.factory
.msncon
.contactStatusChanged(userHandle
, statusCode
, screenName
)
375 def contactOffline(self
, userHandle
):
376 LogEvent(INFO
, self
.factory
.msncon
.ident
)
377 self
.factory
.msncon
.contactStatusChanged(userHandle
, msn
.STATUS_OFFLINE
, "")
379 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
380 LogEvent(INFO
, self
.factory
.msncon
.ident
)
381 sb
= OneSwitchboardSession(self
.factory
.msncon
, userHandle
)
382 sb
.connectReply(host
, port
, key
, sessionID
)
383 sbOld
= self
.factory
.msncon
.switchboardSessions
.get(userHandle
)
386 self
.factory
.msncon
.switchboardSessions
[userHandle
] = sb
388 def multipleLogin(self
):
389 LogEvent(INFO
, self
.factory
.msncon
.ident
)
390 self
.factory
.msncon
.multipleLogin()
392 def serverGoingDown(self
):
393 LogEvent(INFO
, self
.factory
.msncon
.ident
)
394 self
.factory
.msncon
.serverGoingDown()
398 def switchToGroupchat(*args
):
399 raise NotImplementedError
402 class SwitchboardSessionBase
:
403 def __init__(self
, msncon
):
405 self
.userHandle
= msncon
.username
406 self
.ident
= (msncon
.ident
, "INVALID!!")
407 self
.messageBuffer
= []
411 LogEvent(INFO
, self
.ident
)
413 self
.transport
.disconnect()
415 for message
, noerror
in self
.messageBuffer
:
417 self
.msncon
.failedMessage(self
.remoteUser
, message
)
420 LogEvent(INFO
, self
.ident
)
421 def sbRequestAccepted((host
, port
, key
)):
422 LogEvent(INFO
, self
.ident
)
425 factory
= ClientFactory()
426 factory
.buildProtocol
= lambda addr
: self
427 reactor
.connectTCP(host
, port
, factory
)
428 def sbRequestFailed(ignored
=None):
429 LogEvent(INFO
, self
.ident
)
430 del self
.msncon
.switchboardSessions
[self
.remoteUser
]
431 d
= self
.msncon
.notificationClient
.requestSwitchboardServer()
432 d
.addCallbacks(sbRequestAccepted
, sbRequestFailed
)
434 def connectReply(self
, host
, port
, key
, sessionID
):
435 LogEvent(INFO
, self
.ident
)
437 self
.sessionID
= sessionID
439 factory
= ClientFactory()
440 factory
.buildProtocol
= lambda addr
: self
441 reactor
.connectTCP(host
, port
, factory
)
443 def flushBuffer(self
):
444 for message
, noerror
in self
.messageBuffer
[:]:
445 self
.messageBuffer
.remove((message
, noerror
))
446 self
.sendMessage(message
, noerror
)
448 def failedMessage(self
, ignored
):
449 raise NotImplementedError
451 def sendMessage(self
, text
, noerror
):
453 self
.messageBuffer
.append((text
, noerror
))
455 LogEvent(INFO
, self
.ident
)
456 def failedMessage(ignored
):
458 self
.failedMessage(text
)
460 if len(text
) < MAXMESSAGESIZE
:
461 message
= msn
.MSNMessage(message
=str(text
.replace("\n", "\r\n").encode("utf-8")))
462 message
.setHeader("Content-Type", "text/plain; charset=UTF-8")
463 message
.ack
= msn
.MSNMessage
.MESSAGE_NACK
465 d
= msn
.SwitchboardClient
.sendMessage(self
, message
)
467 d
.addCallback(failedMessage
)
470 chunks
= int(math
.ceil(len(text
) / float(MAXMESSAGESIZE
)))
472 guid
= msn
.random_guid()
473 while chunk
< chunks
:
474 offset
= chunk
* MAXMESSAGESIZE
475 text
= message
[offset
: offset
+ MAXMESSAGESIZE
]
476 message
= msn
.MSNMessage(message
=str(text
.replace("\n", "\r\n").encode("utf-8")))
477 message
.ack
= msn
.MSNMessage
.MESSAGE_NACK
479 message
.setHeader("Content-Type", "text/plain; charset=UTF-8")
480 message
.setHeader("Chunks", str(chunks
))
482 message
.setHeader("Chunk", str(chunk
))
484 d
= msn
.SwitchboardClient
.sendMessage(self
, message
)
486 d
.addCallback(failedMessage
)
490 def gotFileReceive(self
, fileReceive
):
491 LogEvent(INFO
, self
.ident
)
492 self
.msncon
.gotFileReceive(fileReceive
)
496 class OneSwitchboardSession(SwitchboardSessionBase
, msn
.SwitchboardClient
):
497 def __init__(self
, msncon
, remoteUser
):
498 SwitchboardSessionBase
.__init
__(self
, msncon
)
499 msn
.SwitchboardClient
.__init
__(self
)
500 self
.remoteUser
= remoteUser
501 self
.ident
= (self
.msncon
, self
.remoteUser
)
502 self
.chattingUsers
= []
506 LogEvent(INFO
, self
.ident
)
508 for user
in self
.chattingUsers
:
509 self
.userJoined(user
)
511 self
.timeout
.cancel()
515 def failedMessage(self
, text
):
516 self
.msncon
.failedMessage(self
.remoteUser
, text
)
520 LogEvent(INFO
, self
.ident
)
522 def failCB(arg
=None):
523 LogEvent(INFO
, ident
, "User has not joined after 30 seconds.")
524 del self
.msncon
.switchboardSessions
[self
.remoteUser
]
525 d
= self
.inviteUser(self
.remoteUser
)
527 self
.timeout
= reactor
.callLater(30.0, failCB
)
531 def gotChattingUsers(self
, users
):
532 for userHandle
in users
.keys():
533 self
.chattingUsers
.append(userHandle
)
535 def userJoined(self
, userHandle
, screenName
=''):
536 LogEvent(INFO
, self
.ident
)
539 if userHandle
!= self
.remoteUser
:
540 # Another user has joined, so we now have three participants.
541 switchToGroupchat(self
, self
.remoteUser
, userHandle
)
545 def userLeft(self
, userHandle
):
547 if userHandle
== self
.remoteUser
:
548 del self
.msncon
.switchboardSessions
[self
.remoteUser
]
549 reactor
.callLater(0, wait
) # Make sure this is handled after everything else
551 def gotMessage(self
, message
):
552 LogEvent(INFO
, self
.ident
)
553 self
.msncon
.gotMessage(self
.remoteUser
, message
.getMessage())
555 def gotFileReceive(self
, fileReceive
):
556 LogEvent(INFO
, self
.ident
)
557 self
.msncon
.gotFileReceive(fileReceive
)
559 def gotContactTyping(self
, message
):
560 LogEvent(INFO
, self
.ident
)
561 self
.msncon
.gotContactTyping(message
.userHandle
)
563 def sendTypingNotification(self
):
564 LogEvent(INFO
, self
.ident
)
566 self
.sendTypingNotification()
568 CAPS
= msn
.MSNContact
.MSNC1 | msn
.MSNContact
.MSNC2 | msn
.MSNContact
.MSNC3 | msn
.MSNContact
.MSNC4
569 def requestAvatar(self
):
570 if not self
.ready
: return
571 msnContacts
= self
.msncon
.getContacts()
572 if not msnContacts
: return
573 msnContact
= msnContacts
.getContact(self
.remoteUser
)
574 if not (msnContact
and msnContact
.caps
& self
.CAPS
and msnContact
.msnobj
): return
575 if msnContact
.msnobjGot
: return
576 msnContact
.msnobjGot
= True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
577 self
.sendAvatarRequest(self
.remoteUser
, msnContact
.msnobj
)