Package papyon :: Module client

Source Code for Module papyon.client

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # papyon - a python client library for Msn 
  4  # 
  5  # Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com> 
  6  # Copyright (C) 2006-2007 Ole André Vadla Ravnås <oleavr@gmail.com> 
  7  # Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com> 
  8  # Copyright (C) 2008 Richard Spiers <richard.spiers@gmail.com> 
  9  # 
 10  # This program is free software; you can redistribute it and/or modify 
 11  # it under the terms of the GNU General Public License as published by 
 12  # the Free Software Foundation; either version 2 of the License, or 
 13  # (at your option) any later version. 
 14  # 
 15  # This program is distributed in the hope that it will be useful, 
 16  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 17  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 18  # GNU General Public License for more details. 
 19  # 
 20  # You should have received a copy of the GNU General Public License 
 21  # along with this program; if not, write to the Free Software 
 22  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 23   
 24  """Client 
 25   
 26  This module contains the main class used to login into the MSN Messenger 
 27  network. The following example demonstrates a simple client. 
 28   
 29      >>> import papyon 
 30      >>> 
 31      >>> server = ('messenger.hotmail.com', 1863) 
 32      >>> account = ('papyon@hotmail.com', 'papyon is great !') 
 33      >>> 
 34      >>> client = papyon.Client(server) 
 35      >>> client.login(*account) 
 36      >>> 
 37      >>> if __name__ == "__main__": 
 38      ...     import gobject 
 39      ...     import logging 
 40      ...     logging.basicConfig(level=logging.DEBUG) # allows us to see the protocol debug 
 41      ... 
 42      ...     mainloop = gobject.MainLoop() 
 43      ...     mainloop.run() 
 44   
 45  This client will try to login, but will probably fail because of the wrong 
 46  password, so let's enhance this client so that it displays an error if the 
 47  password was wrong, this will lead us to use the L{papyon.event} interfaces: 
 48   
 49      >>> import papyon 
 50      >>> import papyon.event 
 51      >>> 
 52      >>> class ClientEventHandler(papyon.event.ClientEventInterface): 
 53      ...     def on_client_error(self, error_type, error): 
 54      ...         if error_type == papyon.event.ClientErrorType.AUTHENTICATION: 
 55      ...             print "" 
 56      ...             print "********************************************************" 
 57      ...             print "* You bummer ! you did input a wrong username/password *" 
 58      ...             print "********************************************************" 
 59      ...         else: 
 60      ...             print "ERROR :", error_type, " ->", error 
 61      >>> 
 62      >>> 
 63      >>> server = ('messenger.hotmail.com', 1863) 
 64      >>> account = ('papyon@hotmail.com', 'papyon is great !') 
 65      >>> 
 66      >>> client = papyon.Client(server) 
 67      >>> client_events_handler = ClientEventHandler(client) 
 68      >>> 
 69      >>> client.login(*account) 
 70      >>> 
 71      >>> if __name__ == "__main__": 
 72      ...     import gobject 
 73      ...     import logging 
 74      ... 
 75      ...     logging.basicConfig(level=logging.DEBUG) # allows us to see the protocol debug 
 76      ... 
 77      ...     mainloop = gobject.MainLoop() 
 78      ...     mainloop.run() 
 79   
 80  """ 
 81   
 82   
 83  import papyon.profile as profile 
 84  import papyon.msnp as msnp 
 85   
 86  import papyon.service.SingleSignOn as SSO 
 87  import papyon.service.AddressBook as AB 
 88  import papyon.service.OfflineIM as OIM 
 89  import papyon.service.Spaces as Spaces 
 90   
 91  from papyon.util.decorator import rw_property 
 92  from papyon.transport import * 
 93  from papyon.switchboard_manager import SwitchboardManager 
 94  from papyon.msnp2p import P2PSessionManager 
 95  from papyon.p2p import MSNObjectStore, WebcamHandler 
 96  from papyon.conversation import SwitchboardConversation, \ 
 97      ExternalNetworkConversation 
 98  from papyon.event import ClientState, ClientErrorType, \ 
 99      AuthenticationError, ProtocolError, EventsDispatcher 
100   
101  import logging 
102   
103  __all__ = ['Client'] 
104   
105  logger = logging.getLogger('client') 
106 107 -class Client(EventsDispatcher):
108 """This class provides way to connect to the notification server as well 109 as methods to manage the contact list, and the personnal settings. 110 @sort: __init__, login, logout, state, profile, address_book, 111 msn_object_store, oim_box, spaces""" 112
113 - def __init__(self, server, proxies={}, transport_class=DirectConnection):
114 """Initializer 115 116 @param server: the Notification server to connect to. 117 @type server: tuple(host, port) 118 119 @param proxies: proxies that we can use to connect 120 @type proxies: {type: string => L{gnet.proxy.ProxyInfos}} 121 122 @param transport_class: the transport class to use for the network 123 connection 124 @type transport_class: L{papyon.transport.BaseTransport}""" 125 EventsDispatcher.__init__(self) 126 127 self.__state = ClientState.CLOSED 128 129 self._proxies = proxies 130 self._transport_class = transport_class 131 self._proxies = proxies 132 133 self._transport = transport_class(server, ServerType.NOTIFICATION, 134 self._proxies) 135 self._protocol = msnp.NotificationProtocol(self, self._transport, 136 self._proxies) 137 138 self._switchboard_manager = SwitchboardManager(self) 139 self._switchboard_manager.register_handler(SwitchboardConversation) 140 141 self._p2p_session_manager = P2PSessionManager(self) 142 self._webcam_handler = WebcamHandler(self) 143 self._p2p_session_manager.register_handler(self._webcam_handler) 144 145 self._msn_object_store = MSNObjectStore(self) 146 self._p2p_session_manager.register_handler(self._msn_object_store) 147 148 149 150 self._external_conversations = {} 151 152 self._sso = None 153 154 self._profile = None 155 self._address_book = None 156 self._oim_box = None 157 self._mailbox = None 158 159 self.__die = False 160 self.__connect_transport_signals() 161 self.__connect_protocol_signals() 162 self.__connect_switchboard_manager_signals() 163 self.__connect_webcam_handler_signals()
164 165 ### public: 166 @property
167 - def msn_object_store(self):
168 """The MSNObjectStore instance associated with this client. 169 @rtype: L{MSNObjectStore<papyon.p2p.MSNObjectStore>}""" 170 return self._msn_object_store
171 172 @property
173 - def webcam_handler(self):
174 return self._webcam_handler
175 176 @property
177 - def profile(self):
178 """The profile of the current user 179 @rtype: L{User<papyon.profile.Profile>}""" 180 return self._profile
181 182 @property
183 - def address_book(self):
184 """The address book of the current user 185 @rtype: L{AddressBook<papyon.service.AddressBook>}""" 186 return self._address_book
187 188 @property
189 - def oim_box(self):
190 """The offline IM for the current user 191 @rtype: L{OfflineIM<papyon.service.OfflineIM>}""" 192 return self._oim_box
193 194 @property
195 - def mailbox(self):
196 """The mailbox of the current user 197 @rtype: L{<papyon.msnp.mailbox.Mailbox>}""" 198 return self._mailbox
199 200 @property
201 - def spaces(self):
202 """The MSN Spaces of the current user 203 @rtype: L{Spaces<papyon.service.Spaces>}""" 204 return self._spaces
205 206 @property
207 - def state(self):
208 """The state of this Client 209 @rtype: L{papyon.event.ClientState}""" 210 return self.__state
211
212 - def login(self, account, password):
213 """Login to the server. 214 215 @param account: the account to use for authentication. 216 @type account: utf-8 encoded string 217 218 @param password: the password needed to authenticate to the account 219 @type password: utf-8 encoded string 220 """ 221 if (self._state != ClientState.CLOSED): 222 logger.warning('login already in progress') 223 self.__die = False 224 self._state = ClientState.CONNECTING 225 self._profile = profile.Profile((account, password), self._protocol) 226 self.__connect_profile_signals() 227 self._mailbox = msnp.Mailbox(self._protocol) 228 self.__connect_mailbox_signals() 229 self._transport.establish_connection()
230
231 - def logout(self):
232 """Logout from the server.""" 233 if self._state == ClientState.CLOSED: 234 logger.warning('alreay logged out') 235 return 236 self.__die = True 237 self._switchboard_manager.close() 238 if self.__state < ClientState.AUTHENTICATING: 239 self._transport.lose_connection() 240 else: 241 self._protocol.signoff() 242 self.__state = ClientState.CLOSED
243 244 ### protected: 245 @rw_property
246 - def _state():
247 def fget(self): 248 return self.__state
249 def fset(self, state): 250 self.__state = state 251 self._dispatch("on_client_state_changed", state)
252 return locals() 253
254 - def _register_external_conversation(self, conversation):
255 for contact in conversation.participants: 256 break 257 258 if contact in self._external_conversations: 259 logger.warning("trying to register an external conversation twice") 260 return 261 self._external_conversations[contact] = conversation
262
263 - def _unregister_external_conversation(self, conversation):
264 for contact in conversation.participants: 265 break 266 del self._external_conversations[contact]
267 268 ### private:
269 - def __connect_profile_signals(self):
270 """Connect profile signals""" 271 def property_changed(profile, pspec): 272 method_name = "on_profile_%s_changed" % pspec.name.replace("-", "_") 273 self._dispatch(method_name)
274 275 self.profile.connect("notify::presence", property_changed) 276 self.profile.connect("notify::display-name", property_changed) 277 self.profile.connect("notify::personal-message", property_changed) 278 self.profile.connect("notify::current-media", property_changed) 279 self.profile.connect("notify::msn-object", property_changed) 280
281 - def __connect_mailbox_signals(self):
282 """Connect mailbox signals""" 283 def new_mail_received(mailbox, mail): 284 self._dispatch("on_mailbox_new_mail_received", mail)
285 286 def unread_changed(mailbox, unread_count, initial): 287 method_name = "on_mailbox_unread_mail_count_changed" 288 self._dispatch(method_name, unread_count, initial) 289 290 self.mailbox.connect("unread-mail-count-changed", unread_changed) 291 self.mailbox.connect("new-mail-received", new_mail_received) 292
293 - def __connect_contact_signals(self, contact):
294 """Connect contact signals""" 295 def event(contact, *args): 296 event_name = args[-1] 297 event_args = args[:-1] 298 method_name = "on_contact_%s" % event_name.replace("-", "_") 299 self._dispatch(method_name, contact, *event_args)
300 301 def property_changed(contact, pspec): 302 method_name = "on_contact_%s_changed" % pspec.name.replace("-", "_") 303 self._dispatch(method_name, contact) 304 305 contact.connect("notify::memberships", property_changed) 306 contact.connect("notify::presence", property_changed) 307 contact.connect("notify::display-name", property_changed) 308 contact.connect("notify::personal-message", property_changed) 309 contact.connect("notify::current-media", property_changed) 310 contact.connect("notify::msn-object", property_changed) 311 contact.connect("notify::client-capabilities", property_changed) 312 313 def connect_signal(name): 314 contact.connect(name, event, name) 315 connect_signal("infos-changed") 316
317 - def __connect_transport_signals(self):
318 """Connect transport signals""" 319 def connect_success(transp): 320 self._sso = SSO.SingleSignOn(self.profile.account, 321 self.profile.password, 322 self._proxies) 323 self._address_book = AB.AddressBook(self._sso, self, self._proxies) 324 self.__connect_addressbook_signals() 325 self._oim_box = OIM.OfflineMessagesBox(self._sso, self, self._proxies) 326 self.__connect_oim_box_signals() 327 self._spaces = Spaces.Spaces(self._sso, self._proxies) 328 329 self._state = ClientState.CONNECTED
330 331 def connect_failure(transp, reason): 332 self._dispatch("on_client_error", ClientErrorType.NETWORK, reason) 333 self._state = ClientState.CLOSED 334 335 def disconnected(transp, reason): 336 if not self.__die: 337 self._dispatch("on_client_error", ClientErrorType.NETWORK, reason) 338 self.__die = False 339 self._state = ClientState.CLOSED 340 341 self._transport.connect("connection-success", connect_success) 342 self._transport.connect("connection-failure", connect_failure) 343 self._transport.connect("connection-lost", disconnected) 344
345 - def __connect_protocol_signals(self):
346 """Connect protocol signals""" 347 def state_changed(proto, param): 348 state = proto.state 349 if state == msnp.ProtocolState.AUTHENTICATING: 350 self._state = ClientState.AUTHENTICATING 351 elif state == msnp.ProtocolState.AUTHENTICATED: 352 self._state = ClientState.AUTHENTICATED 353 elif state == msnp.ProtocolState.SYNCHRONIZING: 354 self._state = ClientState.SYNCHRONIZING 355 elif state == msnp.ProtocolState.SYNCHRONIZED: 356 self._state = ClientState.SYNCHRONIZED 357 elif state == msnp.ProtocolState.OPEN: 358 self._state = ClientState.OPEN 359 im_contacts = self.address_book.contacts 360 for contact in im_contacts: 361 self.__connect_contact_signals(contact)
362 363 def authentication_failed(proto): 364 self._dispatch("on_client_error", ClientErrorType.AUTHENTICATION, 365 AuthenticationError.INVALID_USERNAME_OR_PASSWORD) 366 self.__die = True 367 self._transport.lose_connection() 368 369 def disconnected_by_other(proto): 370 self._dispatch("on_client_error", ClientErrorType.PROTOCOL, 371 ProtocolError.OTHER_CLIENT) 372 self.__die = True 373 self._transport.lose_connection() 374 375 def server_down(proto): 376 self._dispatch("on_client_error", ClientErrorType.PROTOCOL, 377 ProtocolError.SERVER_DOWN) 378 self.__die = True 379 self._transport.lose_connection() 380 381 def unmanaged_message_received(proto, sender, message): 382 if sender in self._external_conversations: 383 conversation = self._external_conversations[sender] 384 conversation._on_message_received(message) 385 else: 386 conversation = ExternalNetworkConversation(self, [sender]) 387 self._register_external_conversation(conversation) 388 if self._dispatch("on_invite_conversation", conversation) == 0: 389 logger.warning("No event handler attached for conversations") 390 conversation._on_message_received(message) 391 392 self._protocol.connect("notify::state", state_changed) 393 self._protocol.connect("authentication-failed", authentication_failed) 394 self._protocol.connect("disconnected-by-other", disconnected_by_other) 395 self._protocol.connect("server-down", server_down) 396 self._protocol.connect("unmanaged-message-received", unmanaged_message_received) 397
398 - def __connect_switchboard_manager_signals(self):
399 """Connect Switchboard Manager signals""" 400 def handler_created(switchboard_manager, handler_class, handler): 401 if handler_class is SwitchboardConversation: 402 if self._dispatch("on_invite_conversation", handler) == 0: 403 logger.warning("No event handler attached for conversations") 404 else: 405 logger.warning("Unknown Switchboard Handler class %s" % handler_class)
406 407 self._switchboard_manager.connect("handler-created", handler_created) 408
409 - def __connect_addressbook_signals(self):
410 """Connect AddressBook signals""" 411 def event(address_book, *args): 412 event_name = args[-1] 413 event_args = args[:-1] 414 if event_name == "messenger-contact-added": 415 self.__connect_contact_signals(event_args[0]) 416 method_name = "on_addressbook_%s" % event_name.replace("-", "_") 417 self._dispatch(method_name, *event_args)
418 def error(address_book, error_code): 419 self._dispatch("on_client_error", ClientErrorType.ADDRESSBOOK, error_code) 420 self.__die = True 421 self._transport.lose_connection() 422 423 self.address_book.connect('error', error) 424 425 def connect_signal(name): 426 self.address_book.connect(name, event, name) 427 428 connect_signal("messenger-contact-added") 429 connect_signal("contact-deleted") 430 connect_signal("contact-blocked") 431 connect_signal("contact-unblocked") 432 connect_signal("group-added") 433 connect_signal("group-deleted") 434 connect_signal("group-renamed") 435 connect_signal("group-contact-added") 436 connect_signal("group-contact-deleted") 437
438 - def __connect_oim_box_signals(self):
439 """Connect Offline IM signals""" 440 def event(oim_box, *args): 441 method_name = "on_oim_%s" % args[-1].replace("-", "_") 442 self._dispatch(method_name, *args[:-1])
443 def state_changed(oim_box, pspec): 444 self._dispatch("on_oim_state_changed", oim_box.state) 445 def error(oim_box, error_code): 446 self._dispatch("on_client_error", ClientErrorType.OFFLINE_MESSAGES, error_code) 447 448 self.oim_box.connect("notify::state", state_changed) 449 self.oim_box.connect('error', error) 450 451 def connect_signal(name): 452 self.oim_box.connect(name, event, name) 453 connect_signal("messages-received") 454 connect_signal("messages-fetched") 455 connect_signal("message-sent") 456 connect_signal("messages-deleted") 457
458 - def __connect_webcam_handler_signals(self):
459 """Connect Webcam Handler signals""" 460 def session_created(webcam_handler, session, producer): 461 self._dispatch("on_invite_webcam", session, producer)
462 463 self._webcam_handler.connect("session-created", session_created) 464