Logo Search packages:      
Sourcecode: telepathy-butterfly version File versions

connection.py

# telepathy-butterfly - an MSN connection manager for Telepathy
#
# Copyright (C) 2006 Ali Sabil <ali.sabil@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import weakref
import gobject
import logging
import imghdr

import dbus
import telepathy

import pymsn
import contacts
import conversation

logger = logging.getLogger('telepathy-butterfly:connection')

MSN_INTERFACE_DEBUG = "org.freedesktop.TpMSN.Debug"

class MsnPresence(object):
    ONLINE = 'available'
    AWAY = 'away'
    BUSY = 'dnd'
    IDLE = 'xa'
    BRB = 'brb'
    PHONE = 'phone'
    LUNCH = 'lunch'
    INVISIBLE = 'hidden'
    OFFLINE = 'offline'

    telepathy_to_pymsn = {
            ONLINE:     pymsn.PresenceStatus.ONLINE,
            AWAY:       pymsn.PresenceStatus.AWAY,
            BUSY:       pymsn.PresenceStatus.BUSY,
            IDLE:       pymsn.PresenceStatus.IDLE,
            BRB:        pymsn.PresenceStatus.BE_RIGHT_BACK,
            PHONE:      pymsn.PresenceStatus.ON_THE_PHONE,
            LUNCH:      pymsn.PresenceStatus.OUT_TO_LUNCH,
            INVISIBLE:  pymsn.PresenceStatus.INVISIBLE,
            OFFLINE:    pymsn.PresenceStatus.OFFLINE
            }

    pymsn_to_telepathy = {
            pymsn.PresenceStatus.ONLINE:         ONLINE,
            pymsn.PresenceStatus.AWAY:           AWAY,
            pymsn.PresenceStatus.BUSY:           BUSY,
            pymsn.PresenceStatus.IDLE:           IDLE,
            pymsn.PresenceStatus.BE_RIGHT_BACK:  BRB,
            pymsn.PresenceStatus.ON_THE_PHONE:   PHONE,
            pymsn.PresenceStatus.OUT_TO_LUNCH:   LUNCH,
            pymsn.PresenceStatus.INVISIBLE:      INVISIBLE,
            pymsn.PresenceStatus.OFFLINE:        OFFLINE

            }

MsnPassportHandle = telepathy.server.Handle

MsnContactListHandle = telepathy.server.Handle


class MsnConnection(telepathy.server.Connection, 
                    pymsn.Client, 
                    telepathy.server.ConnectionInterfacePresence, 
                    telepathy.server.ConnectionInterfaceAliasing, 
                    telepathy.server.ConnectionInterfaceAvatars):

    _mandatory_parameters = {   'account':'s',
                                'password':'s'
                            }
    _optional_parameters =  {
                                'server':'s',
                                'port':'q',
                                'http-proxy-server' : 's',
                                'http-proxy-port' : 'q',
                                'http-proxy-username' : 's',
                                'http-proxy-password' : 's',
                                'https-proxy-server' : 's',
                                'https-proxy-port' : 'q',
                                'https-proxy-username' : 's',
                                'https-proxy-password' : 's'
                            }
    _parameter_defaults =   {
                                'server':'messenger.hotmail.com',
                                'port':1863
                            }
    
    def __init__(self, manager, parameters):
        """
        Initialize a connection to the Msn service.
        """
        self.check_parameters(parameters)
        server = (parameters['server'], parameters['port'])
        account = (parameters['account'], parameters['password'])
        
        proxies = {}
        
        proxy = self._build_proxy_infos(parameters, 'http')
        if proxy: proxies['http'] = proxy
        
        proxy = self._build_proxy_infos(parameters, 'https')
        if proxy: proxies['https'] = proxy

        telepathy.server.Connection.__init__(self, 'msn', unicode(parameters['account']))
        pymsn.Client.__init__(self, server, account, proxies = proxies)
        telepathy.server.ConnectionInterfacePresence.__init__(self)
        telepathy.server.ConnectionInterfaceAliasing.__init__(self)
        telepathy.server.ConnectionInterfaceAvatars.__init__(self)
        
        self.profile.connect("notify::presence", self._user_presence_update_cb)
        self.profile.connect("notify::friendly-name", self._user_friendly_name_update_cb)
        self.profile.connect("notify::personal-message", self._user_personal_message_update_cb)

        self._die = False
        self._manager = manager

        self._avatars = {}
        self._contacts_handles = weakref.WeakValueDictionary()
        self._list_handles = weakref.WeakValueDictionary()
        
        self._im_channels = weakref.WeakValueDictionary()
        self._list_channels = weakref.WeakValueDictionary()
        
        self._self_handle = self._get_handle_for_contact(parameters['account'])

        logger.info("Connection created")


    # pymsn callbacks
    def on_connect_failure(self, proto):
        pymsn.Client.on_connect_failure(self, proto)
        self.StatusChanged(telepathy.CONNECTION_STATUS_DISCONNECTED,
                telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR)

    def on_login_failure(self, proto):
        pymsn.Client.on_login_failure(self, proto)
        self.StatusChanged(telepathy.CONNECTION_STATUS_DISCONNECTED,
                telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)

    def on_login_success(self, proto):
        self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTED,
                telepathy.CONNECTION_STATUS_REASON_REQUESTED)
        pymsn.Client.on_login_success(self, proto)

    def on_disconnected(self, conn):
        pymsn.Client.on_disconnected(self, conn)
        if self._die:
            reason = telepathy.CONNECTION_STATUS_REASON_REQUESTED
        else:
            reason = telepathy.CONNECTION_STATUS_REASON_NONE_SPECIFIED
        self.StatusChanged(telepathy.CONNECTION_STATUS_DISCONNECTED, reason)
        gobject.idle_add(self._manager.disconnected, self)
    
    def __split_cmd(self, cmd):
        name = cmd.name.decode("utf-8")
        
        if cmd.arguments:
            args = " ".join(cmd.arguments).decode("utf-8")
        else:
            args = u""
        
        if cmd.payload:
            payload = cmd.payload.decode("utf-8")
        else:
            payload = u""
        
        return (name, args, payload)
        
    def on_command_received(self, transport, cmd):
        self.ProtocolIncoming(*self.__split_cmd(cmd))

    def on_command_sent(self, transport, cmd):
        self.ProtocolOutgoing(*self.__split_cmd(cmd))

    def on_contact_list_status_change(self, proto, property):
        pymsn.Client.on_contact_list_status_change(self, proto, property)
        if proto.get_property("contactlist-status") == pymsn.ContactListStatus.SYNCHRONIZING:
            # create channel for handling subscriptions (Forward List)
            subscribe_handle = self._get_handle_for_list('subscribe')
            subscribe_channel = contacts.MsnSubscribeListChannel(self, subscribe_handle)
            self._list_channels[subscribe_handle] = subscribe_channel
            self.add_channel(subscribe_channel, subscribe_handle, suppress_handler=False)
            # create channel for handling publishing (Forward List)
            publish_handle = self._get_handle_for_list('publish')
            publish_channel = contacts.MsnPublishListChannel(self, publish_handle)
            self._list_channels[publish_handle] = publish_channel
            self.add_channel(publish_channel, publish_handle, suppress_handler=False)

    def on_switchboard_invitation(self, proto, server, key, session, passport, friendly_name):
        sender = self._get_handle_for_contact(passport)
        inviter = self.get_contact_by_passport(passport)
        chan = conversation.MsnTextConversation(self, [inviter,], server, key, session)
        self._im_channels[sender] = chan
        self.add_channel(chan, sender, suppress_handler=False)

    # not used for telepathy ?
    #def on_contact_list_tag_added(self, proto, guid, name):
    #    pymsn.Client.on_contact_list_tag_added(self, proto, guid, name)

    #def on_contact_list_tag_renamed(self, proto, guid, name):
    #    pymsn.Client.on_contact_list_tag_renamed(self, proto, guid, name)

    #def on_contact_list_tag_removed(self, proto, guid):
    #    pymsn.Client.on_contact_list_tag_removed(self, proto, guid)

    #def on_mail_received(self, proto, message):
    #    pymsn.Client.on_mail_received(self, proto, message)
    
    def on_contact_list_contact_received(self, proto, passport, contact):
        pymsn.Client.on_contact_list_contact_received

        contact.connect("notify::presence", self._contact_presence_update_cb)
        contact.connect("notify::friendly-name", self._contact_friendly_name_update_cb)
        contact.connect("notify::personal-message", self._contact_presence_update_cb)
        # For avatar update
        contact.connect("notify::msnobject", self._contact_msnobject_update_cb)
        handle = self._get_handle_for_contact(passport)

        for chan in self._list_channels.values():
            if getattr(chan, 'contact_added', None):
                chan.contact_added(handle, contact)
                
    def Connect(self):
        logger.info("Connecting")
        gobject.idle_add(self._connect_cb)

    def Disconnect(self):
        self._die = True
        self.logout()

    def RequestHandles(self, handle_type, names, sender):
        self.check_connected()
        self.check_handle_type(handle_type)
        handles = []
        if handle_type == telepathy.CONNECTION_HANDLE_TYPE_CONTACT:
            get_handle = self._get_handle_for_contact
        elif handle_type == telepathy.CONNECTION_HANDLE_TYPE_LIST:
            get_handle = self._get_handle_for_list
        else:
            raise telepathy.NotAvailable('Msn only support lists and contacts')
        
        for name in names:
            handle = get_handle(name)
            handles.append(handle.get_id())
            self.add_client_handle(handle, sender)
        
        return handles
        
    def RequestChannel(self, type, handle_type, handle_id, suppress_handler):
        self.check_connected()
        chan = None
        if type == telepathy.CHANNEL_TYPE_TEXT:
            self.check_handle(telepathy.CONNECTION_HANDLE_TYPE_CONTACT, handle_id)

            handle = self._handles[(telepathy.CONNECTION_HANDLE_TYPE_CONTACT, handle_id)]

            if handle_type != telepathy.CONNECTION_HANDLE_TYPE_CONTACT:
                raise telepathy.InvalidHandle('only contact handles are valid for text channels at the moment')

            if handle in self._im_channels:
                chan = self._im_channels[handle]
            else:
                invitee = self.get_contact_by_passport(handle.get_name())
                if invitee.presence == pymsn.PresenceStatus.OFFLINE:
                    raise telepathy.NotAvailable('Contact not available')
                chan = conversation.MsnTextConversation(self, [invitee])
                self._im_channels[handle] = chan
        elif type == telepathy.CHANNEL_TYPE_CONTACT_LIST:
            self.check_handle(telepathy.CONNECTION_HANDLE_TYPE_LIST, handle_id)

            handle = self._handles[(telepathy.CONNECTION_HANDLE_TYPE_LIST, handle_id)]
            if handle in self._list_channels:
                chan = self._list_channels[handle]
            else:
                raise telepathy.NotAvailable('list channel %s not available' % handle.get_name())
        else:
            raise telepathy.NotImplemented('unknown channel type %s' % type)

        assert(chan)

        if not chan in self._channels:
            self.add_channel(chan, handle, suppress_handler)

        return chan._object_path

    # Statuses
    def GetStatuses(self):
        # the arguments are in common to all on-line presences
        arguments = { 'message':'s' }
        # you get one of these for each status
        # {name:(type, self, exclusive, {argument:types}}
        return {
            MsnPresence.ONLINE:( telepathy.CONNECTION_PRESENCE_TYPE_AVAILABLE,
                True, True, arguments ),
            MsnPresence.AWAY:( telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
                True, True, arguments ),
            MsnPresence.BUSY:( telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
                True, True, arguments ),
            MsnPresence.IDLE:( telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
                True, True, arguments ),
            MsnPresence.BRB:( telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
                True, True, arguments ),
            MsnPresence.PHONE:( telepathy.CONNECTION_PRESENCE_TYPE_AWAY,
                True, True, arguments ),
            MsnPresence.LUNCH:( telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
                True, True, arguments ),
            MsnPresence.INVISIBLE:( telepathy.CONNECTION_PRESENCE_TYPE_OFFLINE,
                True, True, {} ),
            MsnPresence.OFFLINE:( telepathy.CONNECTION_PRESENCE_TYPE_OFFLINE,
                True, True, {} )
        }

    def RequestPresence(self, contacts):
        presences = {}
        for handle_id in contacts:
            handle = self._get_handle_for_handle_id(
                    telepathy.CONNECTION_HANDLE_TYPE_CONTACT, handle_id)
            passport = handle.get_name()

            contact = self.get_contact_by_passport(passport)
            presence = MsnPresence.pymsn_to_telepathy[ contact.get_property("presence") ]
            message = contact.get_property("personal-message").decode("utf-8")
            
            arguments = {}
            if message:
                arguments['message'] = message
            
            presences[handle] = (0, {presence:arguments}) # :TODO: Timestamp ?

        self.PresenceUpdate(presences)

    def SetStatus(self, statuses):
        status, arguments = statuses.items()[0]
        if status == MsnPresence.OFFLINE:
            self._die = True

        presence = MsnPresence.telepathy_to_pymsn[ status ]
        message = arguments['message']

        logger.debug("SetStatus: presence=%s, message=%s" % (presence, message))
        if self._status != telepathy.CONNECTION_STATUS_CONNECTED:
            self._initial_status = presence #TODO: initial personal mesage
        else:
            self.profile.personal_message = message.decode('UTF-8')
            self.profile.presence = presence
    
    # Aliases
    def RequestAliases(self, contacts):
        result = []
        for handle_id in contacts:
            handle = self._get_handle_for_handle_id(
                    telepathy.CONNECTION_HANDLE_TYPE_CONTACT, handle_id)
            if handle == self.GetSelfHandle():
                result.append(unicode( self.profile.get_property("friendly-name"), 'UTF-8' ))
            else:
                contact = self.get_contact_by_passport(handle.get_name())
                result.append(unicode(contact.get_property("friendly-name"), 'UTF-8'))
        return result
            
    def SetAliases(self, aliases):
        for handle_id, alias in aliases.iteritems():
            handle = self._get_handle_for_handle_id(
                    telepathy.CONNECTION_HANDLE_TYPE_CONTACT, handle_id)
            if handle != self.GetSelfHandle():
                raise telepathy.PermissionDenied("MSN doesn't allow setting aliases for contacts")
            self.profile.friendly_name = alias.decode('UTF-8')

    # Avatar
    def GetAvatarRequirements(self):
        return (('image/jpeg', 'image/png', 'image/gif'),
                    96, 96, 192, 192, 500 * 1024) #500kb

    def GetAvatarTokens(self, contacts):
        logger.debug("GetAvatarTokens() - contacts: %s" % contacts)
        result = []
        for handle_id in contacts:
            handle = self._get_handle_for_handle_id(
                    telepathy.CONNECTION_HANDLE_TYPE_CONTACT, handle_id)
            if handle == self.GetSelfHandle():
                result.append(self.profile.msnobject)
            else:
                contact = self.get_contact_by_passport(handle.get_name())
                result.append(contact.get_property("msnobject"))

    def RequestAvatar(self, handle_id):
        logger.debug("RequestAvatar() - handle_id: %s" % handle_id)
        avatar, mime_type = self._avatars.get(handle_id, ("",""))
        if avatar:
            logger.debug("RequestAvatar returns: (%s byte, %s) " % (len(avatar), mime_type))
            return (avatar, mime_type)
        logger.debug("RequestAvatar returns: None")

    def SetAvatar(self, avatar, mime_type):
        logger.debug("SetAvatar()")
        if isinstance(avatar, str): # we got a dbus.ByteArray
            self.profile.display_picture = avatar
        else:
            self.profile.display_picture = "".join([chr(b) for b in avatar])

    def ClearAvatar():
        logger.debug("ClearAvatar()")
        self.profile.display_picture = None

    # Private methods
    def _get_handle_for_handle_id(self, handle_type, handle_id):
        self.check_handle(handle_type, handle_id)
        return self._handles[handle_type, handle_id]

    def _get_handle_for_contact(self, passport):
        if passport in self._contacts_handles:
            handle = self._contacts_handles[passport]
        else:
            handle = MsnPassportHandle(self.get_handle_id(), telepathy.CONNECTION_HANDLE_TYPE_CONTACT, passport)
            self._contacts_handles[passport] = handle
            self._handles[handle.get_type(), handle.get_id()] = handle
            logger.debug( "new contact handle %u %s" % (handle.get_id(), handle.get_name()) )
        return handle

    def _get_handle_for_list(self, list):
        if list in self._list_handles:
            handle = self._list_handles[list]
        else:
            handle = MsnContactListHandle(self.get_handle_id(), telepathy.CONNECTION_HANDLE_TYPE_LIST, list)
            self._list_handles[list] = handle
            self._handles[handle.get_type(), handle.get_id()] = handle
            logger.debug( "new list handle %u %s" % (handle.get_id(), handle.get_name()) )
        return handle
    
    def _build_proxy_infos(self, parameters, type='http'):
        if ( (type+'-proxy-server') in parameters) and ((type+'-proxy-port') in parameters):
            return pymsn.network.ProxyInfos( host = parameters[type+'-proxy-server'],
                                        port = parameters[type+'-proxy-port'],
                                        type = type,
                                        user = parameters.get(type+'-proxy-username',None),
                                        password = parameters.get(type+'-proxy-password',None) )
        else:
            return None

    # Signal callbacks
    def _connect_cb(self):
        """
        Called in the glib mainloop first iteration, to start connection
        """
        self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTING,
                telepathy.CONNECTION_STATUS_REASON_REQUESTED)
        self.login()
        return False
    
    def _contact_friendly_name_update_cb(self, contact, property):
        passport = contact.get_property("passport")
        handle = self._get_handle_for_contact(passport)
        friendly_name = unicode( contact.get_property("friendly-name"), 'UTF-8' )
        self.AliasesChanged(((handle,friendly_name),))

    def _contact_presence_update_cb(self, contact, property):
        passport = contact.get_property("passport")
        handle = self._get_handle_for_contact(passport)

        presence = MsnPresence.pymsn_to_telepathy[ contact.get_property("presence") ]
        message = unicode(contact.get_property("personal-message"), 'UTF-8')
            
        arguments = {}
        if message:
            arguments['message'] = message
            
        self.PresenceUpdate({handle: (0, {presence:arguments})}) # :TODO: Timestamp ?

    def _contact_msnobject_update_cb(self, contact, property):
        logger.debug("_contact_msnobject_update_cb() contact=%s property=%s" % (contact, property))
        passport = contact.get_property("passport")
        handle = self._get_handle_for_contact(passport)
        avatar_token = contact.get_property('msnobject')
        logger.debug("Avatar token: %s handle_id=%s" % (avatar_token, handle.get_id()))

        def __on_dp_request_done(result):
            if result == None:
                logger.debug("_on_dp_request_done: Failed to fetch DP")
                return
        
            logger.debug("_on_dp_request_done: Got DP for contact %s! %d bytes of data" % (contact, len(result)))
            img_type = imghdr.what('', result)
            if img_type is None: img_type = 'jpeg'
            self._avatars[handle.get_id()] = (result, 'image/' + img_type)
            self.AvatarUpdated(handle.get_id(), avatar_token)

        # start fetching data
        if contact.request_display_picture(__on_dp_request_done):
            logger.debug("Requested avatar for contact: %s" % contact)

    def _user_friendly_name_update_cb(self, profile, property):
        passport = profile.get_property("passport")
        handle = self._get_handle_for_contact(passport)
        friendly_name = unicode( profile.get_property("friendly-name"), 'UTF-8' )
        self.AliasesChanged(((handle,friendly_name),))

    def _user_presence_update_cb(self, profile, property):
        passport = profile.get_property("passport")
        handle = self._get_handle_for_contact(passport)

        presence = MsnPresence.pymsn_to_telepathy[ profile.get_property("presence") ]
        message = profile.get_property("personal-message")

        arguments = {}
        if message:
            arguments['message'] = message
            
        self.PresenceUpdate({handle: (0, {presence:arguments})}) # :TODO: Timestamp ?
    
    def _user_personal_message_update_cb(self, profile, property):
        passport = profile.get_property("passport")
        handle = self._get_handle_for_contact(passport)
        friendly_name = unicode( profile.get_property("personal-message"), 'UTF-8' )
        self.AliasesChanged(((handle,friendly_name),))

    # debug
    @dbus.service.signal(MSN_INTERFACE_DEBUG, signature='sss')
    def ProtocolIncoming(self, cmd, args, payload):
        pass
    
    @dbus.service.signal(MSN_INTERFACE_DEBUG, signature='sss')
    def ProtocolOutgoing(self, cmd, args, payload):
        pass

Generated by  Doxygen 1.6.0   Back to index