From 3ffe8b89e6d495519f03afd981ade616dc3c2430 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Tue, 12 Jun 2007 02:36:31 +0000 Subject: Hook up Salut to the presence service --- diff --git a/src/buddy.py b/src/buddy.py index d0ad63d..d225cc8 100644 --- a/src/buddy.py +++ b/src/buddy.py @@ -667,12 +667,22 @@ class GenericOwner(Buddy): def _set_self_olpc_properties(self, tp): conn = tp.get_connection() # FIXME: omit color/key/ip4-address if None? - conn[CONN_INTERFACE_BUDDY_INFO].SetProperties( - {'color': self._color or '', 'key': self._key or '', - 'ip4-address': self._ip4_address or '' }, + + props = dbus.Dictionary({ + 'color': self._color or '', + 'key': dbus.ByteArray(self._key or ''), + 'ip4-address': self._ip4_address or '', + }, signature='sv') + + # FIXME: clarify whether we're meant to support random extra properties + # (Salut doesn't) + if tp._PROTOCOL == 'salut': + del props['ip4-address'] + + conn[CONN_INTERFACE_BUDDY_INFO].SetProperties(props, reply_handler=_noop, error_handler=lambda e: - _logger.warning('Error setting alias: %s', e)) + _logger.warning('Error setting OLPC properties: %s', e)) # Hack so we can use this as a timeout handler return False @@ -723,8 +733,17 @@ class GenericOwner(Buddy): _logger.debug("server does not accept JPEG format avatars.") return - img_data = _get_buddy_icon_at_size(icon_data, min(maxw, 96), - min(maxh, 96), maxsize) + width = 96 + height = 96 + size = 8192 + if maxw > 0 and width > maxw: + width = maxw + if maxw > 0 and height > maxh: + height = maxh + if maxsize > 0 and size > maxsize: + size = maxsize + + img_data = _get_buddy_icon_at_size(icon_data, width, height, size) conn[CONN_INTERFACE_AVATARS].SetAvatar(img_data, "image/jpeg", reply_handler=set_self_avatar_cb, error_handler=lambda e: diff --git a/src/linklocal_plugin.py b/src/linklocal_plugin.py index b8f6445..7d2073c 100644 --- a/src/linklocal_plugin.py +++ b/src/linklocal_plugin.py @@ -1,3 +1,4 @@ +"""Link-local plugin for Presence Service""" # Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, Collabora Ltd. # @@ -15,13 +16,146 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# Standard library +import logging +from itertools import izip +from string import hexdigits + +# Other libraries import gobject +from telepathy.client import (ConnectionManager, Connection) +from telepathy.interfaces import (CONN_MGR_INTERFACE, CONN_INTERFACE, + CHANNEL_INTERFACE_GROUP) +from telepathy.constants import (HANDLE_TYPE_CONTACT, + CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES) + +# Presence Service local modules +import psutils +from telepathy_plugin import TelepathyPlugin + + +CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' + +_logger = logging.getLogger('s-p-s.linklocal_plugin') + + +class LinkLocalPlugin(TelepathyPlugin): + """Telepathy-python-based presence server interface + + The ServerPlugin instance translates network events from + Telepathy Python into GObject events. It provides direct + python calls to perform the required network operations + to implement the PresenceService. + """ + + _TP_CONN_MANAGER = 'salut' + _PROTOCOL = 'salut' + _OBJ_PATH_PREFIX = "/org/freedesktop/Telepathy/Connection/salut/salut/" -class LinkLocalPlugin(gobject.GObject): def __init__(self, registry, owner): - gobject.GObject.__init__(self) - self._registry = registry - self._owner = owner + TelepathyPlugin.__init__(self, registry, owner) + + def _get_account_info(self): + """Retrieve connection manager parameters for this account + """ + server = self._owner.get_server() + khash = psutils.pubkey_to_keyid(self._owner.props.key) + + return { + 'nickname': '%s' % self._owner.props.nick, + 'first-name': ' ', + 'last-name': '%s' % self._owner.props.nick, + 'jid': '%s@%s' % (khash, server), + 'published-name': '%s' % self._owner.props.nick, + } + + def _find_existing_connection(self): + """Try to find an existing Telepathy connection to this server + + filters the set of connections from + telepathy.client.Connection.get_connections + to find a connection using our protocol with the + "self handle" of that connection being a handle + which matches our account (see _get_account_info) + + returns connection or None + """ + # Search existing connections, if any, that we might be able to use + connections = Connection.get_connections() + for item in connections: + if not item.object_path.startswith(self._OBJ_PATH_PREFIX): + continue + if item[CONN_INTERFACE].GetProtocol() != self._PROTOCOL: + continue + # Any Salut instance will do + return item + return None + + def identify_contacts(self, tp_chan, handles, identifiers=None): + """Work out the "best" unique identifier we can for the given handles, + in the context of the given channel (which may be None), using only + 'fast' connection manager API (that does not involve network + round-trips). + + For the XMPP server case, we proceed as follows: + + * Find the owners of the given handles, if the channel has + channel-specific handles + * If the owner (globally-valid JID) is on a trusted server, return + 'keyid/' plus the 'key fingerprint' (the user part of their JID, + currently implemented as the SHA-1 of the Base64 blob in + owner.key.pub) + * If the owner (globally-valid JID) cannot be found or is on an + untrusted server, return 'xmpp/' plus an escaped form of the JID + + The idea is that we identify buddies by key-ID (i.e. by key, assuming + no collisions) if we can find it without making network round-trips, + but if that's not possible we just use their JIDs. + + :Parameters: + `tp_chan` : telepathy.client.Channel or None + The channel in which the handles were found, or None if they + are known to be channel-specific handles + `handles` : iterable over (int or long) + The contacts' handles in that channel + :Returns: + A dict mapping the provided handles to the best available + unique identifier, which is a string that could be used as a + suffix to an object path + """ + # we need to be able to index into handles, so force them to + # be a sequence + if not isinstance(handles, (tuple, list)): + handles = tuple(handles) + + # we happen to know that Salut has no channel-specific handles + + if identifiers is None: + identifiers = self._conn[CONN_INTERFACE].InspectHandles( + HANDLE_TYPE_CONTACT, handles) + + ret = {} + for handle, ident in izip(handles, identifiers): + # special-case the Owner - we always know who we are + if handle == self.self_handle: + ret[handle] = self._owner.props.objid + continue + + # we also happen to know that on Salut, getting properties + # is immediate, and the key is (well, will be) trustworthy + + if CONN_INTERFACE_BUDDY_INFO in self._conn: + props = self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties( + handle) + key = props.get('key') + else: + key = None + + if key is not None: + khash = psutils.pubkey_to_keyid(key) + ret[handle] = 'keyid/' + khash + else: + ret[handle] = 'salut/' + psutils.escape_identifier(ident) - def cleanup(self): - pass + return ret diff --git a/src/presenceservice.py b/src/presenceservice.py index be58407..bd87a51 100644 --- a/src/presenceservice.py +++ b/src/presenceservice.py @@ -58,18 +58,12 @@ class NotFoundError(DBusException): class PresenceService(ExportedGObject): __gtype_name__ = "PresenceService" - __gsignals__ = { - 'connection-status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_BOOLEAN])) - } - def _create_owner(self): # Overridden by TestPresenceService return ShellOwner(self, self._session_bus) def __init__(self): self._next_object_id = 0 - self._connected = False # all Buddy objects # identifier -> Buddy, GC'd when no more refs exist @@ -106,27 +100,27 @@ class PresenceService(ExportedGObject): self._registry = ManagerRegistry() self._registry.LoadManagers() - # Set up the server connection + # Set up the Telepathy plugins self._server_plugin = ServerPlugin(self._registry, self._owner) - self._handles_buddies[self._server_plugin] = {} - self._activities_by_handle[self._server_plugin] = {} - - self._server_plugin.connect('status', self._server_status_cb) - self._server_plugin.connect('contacts-online', self._contacts_online) - self._server_plugin.connect('contacts-offline', self._contacts_offline) - self._server_plugin.connect('activity-invitation', - self._activity_invitation) - self._server_plugin.connect('private-invitation', - self._private_invitation) - self._server_plugin.start() + self._ll_plugin = LinkLocalPlugin(self._registry, self._owner) + self._plugins = [self._server_plugin, self._ll_plugin] + self._connected_plugins = set() + + for tp in self._plugins: + self._handles_buddies[tp] = {} + self._activities_by_handle[tp] = {} + + tp.connect('status', self._tp_status_cb) + tp.connect('contacts-online', self._contacts_online) + tp.connect('contacts-offline', self._contacts_offline) + tp.connect('activity-invitation', + self._activity_invitation) + tp.connect('private-invitation', + self._private_invitation) + tp.start() self._contacts_online_queue = [] - # Set up the link local connection - self._ll_plugin = LinkLocalPlugin(self._registry, self._owner) - self._handles_buddies[self._ll_plugin] = {} - self._activities_by_handle[self._ll_plugin] = {} - ExportedGObject.__init__(self, self._session_bus, _PRESENCE_PATH) # for activation to work in a race-free way, we should really @@ -143,21 +137,14 @@ class PresenceService(ExportedGObject): """Log event when D-Bus kicks us off the bus for some reason""" _logger.debug("Disconnected from session bus!!!") - def _server_status_cb(self, plugin, status, reason): - - # FIXME: figure out connection status when we have a salut plugin too - old_status = self._connected + def _tp_status_cb(self, plugin, status, reason): if status == CONNECTION_STATUS_CONNECTED: - self._connected = True self._tp_connected(plugin) else: - self._connected = False self._tp_disconnected(plugin) - if self._connected != old_status: - self.emit('connection-status', self._connected) - def _tp_connected(self, tp): + self._connected_plugins.add(tp) self._handles_buddies[tp][tp.self_handle] = self._owner self._owner.add_telepathy_handle(tp, tp.self_handle, tp.self_identifier) @@ -235,6 +222,7 @@ class PresenceService(ExportedGObject): conn.object_path) def _tp_disconnected(self, tp): + self._connected_plugins.discard(tp) if tp.self_handle is not None: self._handles_buddies.setdefault(tp, {}).pop( tp.self_handle, None) @@ -690,10 +678,19 @@ class PresenceService(ExportedGObject): self._share_activity(actid, atype, name, properties, async_cb, async_err_cb) + def _get_preferred_plugin(self): + for tp in self._plugins: + if tp in self._connected_plugins: + return tp + return None + @dbus.service.method(_PRESENCE_INTERFACE, in_signature='', out_signature="so") def GetPreferredConnection(self): - conn = self._server_plugin.get_connection() + tp = self._get_preferred_plugin + if tp is None: + raise NotFoundError('No connection is available') + conn = tp.get_connection() return str(conn.service_name), conn.object_path def cleanup(self): @@ -706,7 +703,7 @@ class PresenceService(ExportedGObject): # FIXME check which tp client we should use to share the activity color = self._owner.props.color activity = Activity(self._session_bus, objid, self, - self._server_plugin, 0, + self._get_preferred_plugin(), 0, id=actid, type=atype, name=name, color=color, local=True) activity.connect("validity-changed", diff --git a/src/server_plugin.py b/src/server_plugin.py index 94d1a89..7c6bdd3 100644 --- a/src/server_plugin.py +++ b/src/server_plugin.py @@ -1,4 +1,4 @@ -"""Telepathy-python presence server interface/implementation plugin""" +"""XMPP server plugin for Presence Service""" # Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, Collabora Ltd. # @@ -35,8 +35,6 @@ import psutils from telepathy_plugin import TelepathyPlugin -_PROTOCOL = "jabber" -_OBJ_PATH_PREFIX = "/org/freedesktop/Telepathy/Connection/gabble/jabber/" _logger = logging.getLogger('s-p-s.server_plugin') @@ -50,6 +48,10 @@ class ServerPlugin(TelepathyPlugin): to implement the PresenceService. """ + _TP_CONN_MANAGER = 'gabble' + _PROTOCOL = 'jabber' + _OBJ_PATH_PREFIX = "/org/freedesktop/Telepathy/Connection/gabble/jabber/" + def __init__(self, registry, owner): TelepathyPlugin.__init__(self, registry, owner) @@ -98,9 +100,9 @@ class ServerPlugin(TelepathyPlugin): # Search existing connections, if any, that we might be able to use connections = Connection.get_connections() for item in connections: - if not item.object_path.startswith(_OBJ_PATH_PREFIX): + if not item.object_path.startswith(self._OBJ_PATH_PREFIX): continue - if item[CONN_INTERFACE].GetProtocol() != _PROTOCOL: + if item[CONN_INTERFACE].GetProtocol() != self._PROTOCOL: continue if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_CONNECTED: test_handle = item[CONN_INTERFACE].RequestHandles( @@ -110,20 +112,16 @@ class ServerPlugin(TelepathyPlugin): return item return None - def _make_new_connection(self): - acct = self._account.copy() - - # Create a new connection - gabble_mgr = self._registry.GetManager('gabble') - name, path = gabble_mgr[CONN_MGR_INTERFACE].RequestConnection( - _PROTOCOL, acct) - conn = Connection(name, path) - del acct - return conn - def _could_connect(self): return bool(self._ip4am.props.address) + def _connected_cb(self): + if self._account['register']: + # we successfully register this account + self._owner.set_registered(True) + + TelepathyPlugin._connected_cb(self) + def _server_is_trusted(self, hostname): """Return True if the server with the given hostname is trusted to verify public-key ownership correctly, and only allows users to diff --git a/src/telepathy_plugin.py b/src/telepathy_plugin.py index 330b74c..0e75d62 100644 --- a/src/telepathy_plugin.py +++ b/src/telepathy_plugin.py @@ -22,7 +22,7 @@ from itertools import izip import gobject -from telepathy.client import Channel +from telepathy.client import (Channel, Connection) from telepathy.constants import (CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_CONNECTING, CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED, @@ -31,14 +31,15 @@ from telepathy.constants import (CONNECTION_STATUS_DISCONNECTED, from telepathy.interfaces import (CONN_INTERFACE, CHANNEL_TYPE_TEXT, CHANNEL_TYPE_STREAMED_MEDIA, CHANNEL_INTERFACE_GROUP, CONN_INTERFACE_PRESENCE, CONN_INTERFACE_AVATARS, - CONN_INTERFACE_ALIASING, CHANNEL_TYPE_CONTACT_LIST) + CONN_INTERFACE_ALIASING, CHANNEL_TYPE_CONTACT_LIST, + CONN_MGR_INTERFACE) CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' -_logger = logging.getLogger('s-p-s.server_plugin') +_logger = logging.getLogger('s-p-s.telepathy_plugin') class TelepathyPlugin(gobject.GObject): @@ -71,6 +72,8 @@ class TelepathyPlugin(gobject.GObject): } _RECONNECT_TIMEOUT = 5000 + _TP_CONN_MANAGER = 'gabble' + _PROTOCOL = 'jabber' def __init__(self, registry, owner): """Initialize the ServerPlugin instance @@ -195,9 +198,9 @@ class TelepathyPlugin(gobject.GObject): if status == CONNECTION_STATUS_DISCONNECTED: def connect_reply(): - _logger.debug('Connect() succeeded') + _logger.debug('%r: Connect() succeeded', self) def connect_error(e): - _logger.debug('Connect() failed: %s', e) + _logger.debug('%r: Connect() failed: %s', self, e) if not self._reconnect_id: self._reconnect_id = gobject.timeout_add(self._RECONNECT_TIMEOUT, self._reconnect_cb) @@ -212,7 +215,15 @@ class TelepathyPlugin(gobject.GObject): raise NotImplementedError def _make_new_connection(self): - raise NotImplementedError + acct = self._account.copy() + + # Create a new connection + mgr = self._registry.GetManager(self._TP_CONN_MANAGER) + name, path = mgr[CONN_MGR_INTERFACE].RequestConnection( + self._PROTOCOL, acct) + conn = Connection(name, path) + del acct + return conn def _handle_connection_status_change(self, status, reason): if status == self._conn_status: @@ -220,17 +231,14 @@ class TelepathyPlugin(gobject.GObject): if status == CONNECTION_STATUS_CONNECTING: self._conn_status = status - _logger.debug("status: connecting...") + _logger.debug("%r: connecting...", self) elif status == CONNECTION_STATUS_CONNECTED: - if self._connected_cb(): - _logger.debug("status: connected") - self._conn_status = status - else: - self.cleanup() - _logger.debug("status: was connected, but an error occurred") + self._conn_status = status + _logger.debug("%r: connected", self) + self._connected_cb() elif status == CONNECTION_STATUS_DISCONNECTED: self.cleanup() - _logger.debug("status: disconnected (reason %r)", reason) + _logger.debug("%r: disconnected (reason %r)", self, reason) if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED: # FIXME: handle connection failure; retry later? pass @@ -245,9 +253,6 @@ class TelepathyPlugin(gobject.GObject): self.emit('status', self._conn_status, int(reason)) - def _connected_cb(self): - raise NotImplementedError - def _could_connect(self): return True @@ -277,7 +282,7 @@ class TelepathyPlugin(gobject.GObject): def _contacts_offline(self, handles): """Handle contacts going offline (send message, update set)""" self._online_contacts -= handles - _logger.debug('Contacts now offline: %r', handles) + _logger.debug('%r: Contacts now offline: %r', self, handles) self.emit("contacts-offline", handles) def _contacts_online(self, handles): @@ -308,7 +313,7 @@ class TelepathyPlugin(gobject.GObject): objids.append(handle_to_objid[handle]) self._online_contacts |= frozenset(relevant) - _logger.debug('Contacts now online:') + _logger.debug('%r: Contacts now online:', self) for handle, objid in izip(relevant, objids): _logger.debug(' %u .../%s', handle, objid) self.emit('contacts-online', objids, relevant, jids) @@ -333,8 +338,8 @@ class TelepathyPlugin(gobject.GObject): self._subscribe_remote_pending -= affected self._subscribe_remote_pending |= remote_pending - def _publish_members_changed_cb(self, added, removed, local_pending, - remote_pending, actor, reason): + def _publish_members_changed_cb(self, message, added, removed, + local_pending, remote_pending, actor, reason): if local_pending: # accept all requested subscriptions @@ -401,10 +406,8 @@ class TelepathyPlugin(gobject.GObject): def _connected_cb(self): """Callback on successful connection to a server """ - - if self._account['register']: - # we successfully register this account - self._owner.set_registered(True) + # FIXME: cope with CMs that lack some of the interfaces + # FIXME: cope with CMs with no 'publish' or 'subscribe' # request both handles at the same time to reduce round-trips pub_handle, sub_handle = self._conn[CONN_INTERFACE].RequestHandles( @@ -450,7 +453,6 @@ class TelepathyPlugin(gobject.GObject): # Request presence for everyone we're subscribed to self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) - return True def start(self): """Start up the Telepathy networking connections @@ -464,7 +466,7 @@ class TelepathyPlugin(gobject.GObject): _connect_reply_cb or _connect_error_cb """ - _logger.debug("Starting up...") + _logger.debug("%r: Starting up...", self) if self._reconnect_id > 0: gobject.source_remove(self._reconnect_id) @@ -474,4 +476,4 @@ class TelepathyPlugin(gobject.GObject): if self._could_connect(): self._init_connection() else: - _logger.debug('Postponing connection') + _logger.debug('%r: Postponing connection', self) -- cgit v0.9.1