From 3bd9ef8bbf8e899e29cf5aee36242bb0b8405130 Mon Sep 17 00:00:00 2001 From: Raul Gutierrez Segales Date: Thu, 20 Jan 2011 15:58:18 +0000 Subject: Collaboration support for non-Sugar apps --- (limited to 'collaboration/activity.py') diff --git a/collaboration/activity.py b/collaboration/activity.py new file mode 100644 index 0000000..06e36df --- /dev/null +++ b/collaboration/activity.py @@ -0,0 +1,719 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""UI interface to an activity in the presence service + +STABLE. +""" + +import logging +from functools import partial + +import dbus +from dbus import PROPERTIES_IFACE +import gobject +from telepathy.client import Channel +from telepathy.interfaces import CHANNEL, \ + CHANNEL_INTERFACE_GROUP, \ + CHANNEL_TYPE_TUBES, \ + CHANNEL_TYPE_TEXT, \ + CONNECTION, \ + PROPERTIES_INTERFACE +from telepathy.constants import CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, \ + HANDLE_TYPE_ROOM, \ + HANDLE_TYPE_CONTACT, \ + PROPERTY_FLAG_WRITE + + +CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' +CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' + +_logger = logging.getLogger('sugar.presence.activity') + + +class Activity(gobject.GObject): + """UI interface for an Activity in the presence service + + Activities in the presence service represent your and other user's + shared activities. + + Properties: + id + color + name + type + joined + """ + __gsignals__ = { + 'buddy-joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'buddy-left': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'new-channel': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + __gproperties__ = { + 'id': (str, None, None, None, gobject.PARAM_READABLE), + 'name': (str, None, None, None, gobject.PARAM_READWRITE), + 'tags': (str, None, None, None, gobject.PARAM_READWRITE), + 'color': (str, None, None, None, gobject.PARAM_READWRITE), + 'type': (str, None, None, None, gobject.PARAM_READABLE), + 'private': (bool, None, None, True, gobject.PARAM_READWRITE), + 'joined': (bool, None, None, False, gobject.PARAM_READABLE), + } + + def __init__(self, account_path, connection, room_handle=None, + properties=None): + if room_handle is None and properties is None: + raise ValueError('Need to pass one of room_handle or properties') + + if properties is None: + properties = {} + + gobject.GObject.__init__(self) + + self._account_path = account_path + self.telepathy_conn = connection + self.telepathy_text_chan = None + self.telepathy_tubes_chan = None + + self.room_handle = room_handle + self._join_command = None + self._share_command = None + self._id = properties.get('id', None) + self._color = properties.get('color', None) + self._name = properties.get('name', None) + self._type = properties.get('type', None) + self._tags = properties.get('tags', None) + self._private = properties.get('private', True) + self._joined = properties.get('joined', False) + self._channel_self_handle = None + self._text_channel_group_flags = 0 + self._buddies = {} + + self._get_properties_call = None + if not self.room_handle is None: + self._start_tracking_properties() + + def _start_tracking_properties(self): + bus = dbus.SessionBus() + self._get_properties_call = bus.call_async( + self.telepathy_conn.requested_bus_name, + self.telepathy_conn.object_path, + CONN_INTERFACE_ACTIVITY_PROPERTIES, + 'GetProperties', + 'u', + (self.room_handle,), + reply_handler=self.__got_properties_cb, + error_handler=self.__error_handler_cb, + utf8_strings=True) + + # As only one Activity instance is needed per activity process, + # we can afford listening to ActivityPropertiesChanged like this. + self.telepathy_conn.connect_to_signal( + 'ActivityPropertiesChanged', + self.__activity_properties_changed_cb, + dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) + + def __activity_properties_changed_cb(self, room_handle, properties): + print('%r: Activity properties changed to %r', self, + properties) + self._update_properties(properties) + + def __got_properties_cb(self, properties): + print('__got_properties_cb %r', properties) + self._get_properties_call = None + self._update_properties(properties) + + def __error_handler_cb(self, error): + print('__error_handler_cb %r', error) + + def _update_properties(self, new_props): + val = new_props.get('name', self._name) + if isinstance(val, str) and val != self._name: + self._name = val + self.notify('name') + val = new_props.get('tags', self._tags) + if isinstance(val, str) and val != self._tags: + self._tags = val + self.notify('tags') + val = new_props.get('color', self._color) + if isinstance(val, str) and val != self._color: + self._color = val + self.notify('color') + val = bool(new_props.get('private', self._private)) + if val != self._private: + self._private = val + self.notify('private') + val = new_props.get('id', self._id) + if isinstance(val, str) and self._id is None: + self._id = val + self.notify('id') + val = new_props.get('type', self._type) + if isinstance(val, str) and self._type is None: + self._type = val + self.notify('type') + + def object_path(self): + """Get our dbus object path""" + return self._object_path + + def do_get_property(self, pspec): + """Retrieve a particular property from our property dictionary""" + + if pspec.name == 'joined': + return self._joined + + if self._get_properties_call is not None: + print('%r: Blocking on GetProperties() because someone ' + 'wants property %s', self, pspec.name) + self._get_properties_call.block() + + if pspec.name == 'id': + return self._id + elif pspec.name == 'name': + return self._name + elif pspec.name == 'color': + return self._color + elif pspec.name == 'type': + return self._type + elif pspec.name == 'tags': + return self._tags + elif pspec.name == 'private': + return self._private + + def do_set_property(self, pspec, val): + """Set a particular property in our property dictionary""" + # FIXME: need an asynchronous API to set these properties, + # particularly 'private' + + if pspec.name == 'name': + self._name = val + elif pspec.name == 'color': + self._color = val + elif pspec.name == 'tags': + self._tags = val + elif pspec.name == 'private': + self._private = val + else: + raise ValueError('Unknown property %r', pspec.name) + + self._publish_properties() + + def set_private(self, val, reply_handler, error_handler): + print('set_private %r', val) + self._activity.SetProperties({'private': bool(val)}, + reply_handler=reply_handler, + error_handler=error_handler) + + def get_joined_buddies(self): + """Retrieve the set of Buddy objects attached to this activity + + returns list of presence Buddy objects that we can successfully + create from the buddy object paths that PS has for this activity. + """ + return self._buddies.values() + + def get_buddy_by_handle(self, handle): + """Retrieve the Buddy object given a telepathy handle. + + buddy object paths are cached in self._handle_to_buddy_path, + so we can get the buddy without calling PS. + """ + object_path = self._handle_to_buddy_path.get(handle, None) + if object_path: + buddy = self._ps_new_object(object_path) + return buddy + return None + + def invite(self, buddy, message, response_cb): + """Invite the given buddy to join this activity. + + The callback will be called with one parameter: None on success, + or an exception on failure. + """ + if not self._joined: + raise RuntimeError('Cannot invite a buddy to an activity that is' + 'not shared.') + self.telepathy_text_chan.AddMembers([buddy.contact_handle], message, + dbus_interface=CHANNEL_INTERFACE_GROUP, + reply_handler=partial(self.__invite_cb, response_cb), + error_handler=partial(self.__invite_cb, response_cb)) + + def __invite_cb(self, response_cb, error=None): + response_cb(error) + + def set_up_tubes(self, reply_handler, error_handler): + raise NotImplementedError() + + def __joined_cb(self, join_command, error): + print('%r: Join finished %r', self, error) + if error is not None: + self.emit('joined', error is None, str(error)) + self.telepathy_text_chan = join_command.text_channel + self.telepathy_tubes_chan = join_command.tubes_channel + self._channel_self_handle = join_command.channel_self_handle + self._text_channel_group_flags = join_command.text_channel_group_flags + self._start_tracking_buddies() + self._start_tracking_channel() + + def _start_tracking_buddies(self): + group = self.telepathy_text_chan[CHANNEL_INTERFACE_GROUP] + + group.GetAllMembers(reply_handler=self.__get_all_members_cb, + error_handler=self.__error_handler_cb) + + group.connect_to_signal('MembersChanged', + self.__text_channel_members_changed_cb) + + def _start_tracking_channel(self): + channel = self.telepathy_text_chan[CHANNEL] + channel.connect_to_signal('Closed', self.__text_channel_closed_cb) + + def __get_all_members_cb(self, members, local_pending, remote_pending): + print('__get_all_members_cb %r %r', members, + self._text_channel_group_flags) + if self._channel_self_handle in members: + members.remove(self._channel_self_handle) + + if not members: + return + + self._resolve_handles(members, reply_cb=self._add_initial_buddies) + + def _resolve_handles(self, input_handles, reply_cb): + def get_handle_owners_cb(handles): + self.telepathy_conn.InspectHandles(HANDLE_TYPE_CONTACT, handles, + reply_handler=reply_cb, + error_handler=self.__error_handler_cb, + dbus_interface=CONNECTION) + + if self._text_channel_group_flags & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + + group = self.telepathy_text_chan[CHANNEL_INTERFACE_GROUP] + group.GetHandleOwners(input_handles, + reply_handler=get_handle_owners_cb, + error_handler=self.__error_handler_cb) + else: + get_handle_owners_cb(input_handles) + + def _add_initial_buddies(self, contact_ids): + print('__add_initial_buddies %r', contact_ids) + #for contact_id in contact_ids: + # self._buddies[contact_id] = self._get_buddy(contact_id) + # Once we have the initial members, we can finish the join process + self._joined = True + self.emit('joined', True, None) + + def __text_channel_members_changed_cb(self, message, added, removed, + local_pending, remote_pending, + actor, reason): + print('__text_channel_members_changed_cb %r', + [added, message, added, removed, local_pending, + remote_pending, actor, reason]) + if self._channel_self_handle in added: + added.remove(self._channel_self_handle) + if added: + self._resolve_handles(added, reply_cb=self._add_buddies) + + if self._channel_self_handle in removed: + removed.remove(self._channel_self_handle) + if removed: + self._resolve_handles(added, reply_cb=self._remove_buddies) + + def _add_buddies(self, contact_ids): + for contact_id in contact_ids: + if contact_id not in self._buddies: + buddy = self._get_buddy(contact_id) + self.emit('buddy-joined', buddy) + self._buddies[contact_id] = buddy + + def _remove_buddies(self, contact_ids): + for contact_id in contact_ids: + if contact_id in self._buddies: + buddy = self._get_buddy(contact_id) + self.emit('buddy-left', buddy) + del self._buddies[contact_id] + + def _get_buddy(self, contact_id): + if contact_id in self._buddies: + return self._buddies[contact_id] + else: + return Buddy(self._account_path, contact_id) + + def join(self): + """Join this activity. + + Emits 'joined' and otherwise does nothing if we're already joined. + """ + if self._join_command is not None: + return + + if self._joined: + self.emit('joined', True, None) + return + + print('%r: joining', self) + + self._join_command = _JoinCommand(self.telepathy_conn, + self.room_handle) + self._join_command.connect('finished', self.__joined_cb) + self._join_command.run() + + def share(self, share_activity_cb, share_activity_error_cb): + if not self.room_handle is None: + raise ValueError('Already have a room handle') + + self._share_command = _ShareCommand(self.telepathy_conn, self._id) + self._share_command.connect('finished', + partial(self.__shared_cb, + share_activity_cb, + share_activity_error_cb)) + self._share_command.run() + + def __shared_cb(self, share_activity_cb, share_activity_error_cb, + share_command, error): + print('%r: Share finished %r', self, error) + if error is None: + print "There was no error!" + self._joined = True + self.room_handle = share_command.room_handle + self.telepathy_text_chan = share_command.text_channel + self.telepathy_tubes_chan = share_command.tubes_channel + self._channel_self_handle = share_command.channel_self_handle + self._text_channel_group_flags = \ + share_command.text_channel_group_flags + self._publish_properties() + self._start_tracking_properties() + self._start_tracking_buddies() + self._start_tracking_channel() + share_activity_cb(self) + else: + print("error = %s" % error) + share_activity_error_cb(self, error) + + def _publish_properties(self): + properties = {} + + if self._color is not None: + properties['color'] = str(self._color) + if self._name is not None: + properties['name'] = str(self._name) + if self._type is not None: + properties['type'] = self._type + if self._tags is not None: + properties['tags'] = self._tags + properties['private'] = self._private + + self.telepathy_conn.SetProperties( + self.room_handle, + properties, + dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) + + def __share_error_cb(self, share_activity_error_cb, error): + logging.debug('%r: Share failed because: %s', self, error) + share_activity_error_cb(self, error) + + # GetChannels() wrapper + + def get_channels(self): + """Retrieve communications channel descriptions for the activity + + Returns a tuple containing: + - the D-Bus well-known service name of the connection + (FIXME: this is redundant; in Telepathy it can be derived + from that of the connection) + - the D-Bus object path of the connection + - a list of D-Bus object paths representing the channels + associated with this activity + """ + bus_name = self.telepathy_conn.requested_bus_name + connection_path = self.telepathy_conn.object_path + channels = [self.telepathy_text_chan.object_path, + self.telepathy_tubes_chan.object_path] + + print('%r: bus name is %s, connection is %s, channels are %r', + self, bus_name, connection_path, channels) + return bus_name, connection_path, channels + + # Leaving + def __text_channel_closed_cb(self): + self._joined = False + self.emit('joined', False, 'left activity') + + def leave(self): + """Leave this shared activity""" + print('%r: leaving', self) + self.telepathy_text_chan.Close() + + +class _BaseCommand(gobject.GObject): + __gsignals__ = { + 'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self.text_channel = None + self.text_channel_group_flags = None + self.tubes_channel = None + self.room_handle = None + self.channel_self_handle = None + + def run(self): + raise NotImplementedError() + + +class _ShareCommand(_BaseCommand): + def __init__(self, connection, activity_id): + _BaseCommand.__init__(self) + + self._connection = connection + self._activity_id = activity_id + self._finished = False + self._join_command = None + + def run(self): + self._connection.RequestHandles( + HANDLE_TYPE_ROOM, + [self._activity_id], + reply_handler=self.__got_handles_cb, + error_handler=self.__error_handler_cb, + dbus_interface=CONNECTION) + + def __got_handles_cb(self, handles): + logging.debug('__got_handles_cb %r', handles) + self.room_handle = handles[0] + + self._join_command = _JoinCommand(self._connection, self.room_handle) + self._join_command.connect('finished', self.__joined_cb) + self._join_command.run() + + def __joined_cb(self, join_command, error): + print('%r: Join finished %r', self, error) + if error is not None: + self._finished = True + self.emit('finished', error) + return + + self.text_channel = join_command.text_channel + self.text_channel_group_flags = join_command.text_channel_group_flags + self.tubes_channel = join_command.tubes_channel + + self._connection.AddActivity( + self._activity_id, + self.room_handle, + reply_handler=self.__added_activity_cb, + error_handler=self.__error_handler_cb, + dbus_interface=CONN_INTERFACE_BUDDY_INFO) + + def __added_activity_cb(self): + self._finished = True + self.emit('finished', None) + + def __error_handler_cb(self, error): + self._finished = True + self.emit('finished', error) + + +class _JoinCommand(_BaseCommand): + def __init__(self, connection, room_handle): + _BaseCommand.__init__(self) + + self._connection = connection + self._finished = False + self.room_handle = room_handle + self._global_self_handle = None + + def run(self): + if self._finished: + raise RuntimeError('This command has already finished') + self._connection.Get(CONNECTION, 'SelfHandle', + reply_handler=self.__get_self_handle_cb, + error_handler=self.__error_handler_cb, + dbus_interface=PROPERTIES_IFACE) + + def __get_self_handle_cb(self, handle): + self._global_self_handle = handle + + self._connection.RequestChannel(CHANNEL_TYPE_TEXT, + HANDLE_TYPE_ROOM, self.room_handle, True, + reply_handler=self.__create_text_channel_cb, + error_handler=self.__error_handler_cb, + dbus_interface=CONNECTION) + + self._connection.RequestChannel(CHANNEL_TYPE_TUBES, + HANDLE_TYPE_ROOM, self.room_handle, True, + reply_handler=self.__create_tubes_channel_cb, + error_handler=self.__error_handler_cb, + dbus_interface=CONNECTION) + + def __create_text_channel_cb(self, channel_path): + Channel(self._connection.requested_bus_name, channel_path, + ready_handler=self.__text_channel_ready_cb) + + def __create_tubes_channel_cb(self, channel_path): + print "Creating tubes channel with bus name %s" % self._connection.requested_bus_name + print "Creating tubes channel with channel path %s" % channel_path + Channel(self._connection.requested_bus_name, channel_path, + ready_handler=self.__tubes_channel_ready_cb) + + def __error_handler_cb(self, error): + self._finished = True + self.emit('finished', error) + + def __tubes_channel_ready_cb(self, channel): + print('%r: Tubes channel %r is ready', self, channel) + self.tubes_channel = channel + self._tubes_ready() + + def __text_channel_ready_cb(self, channel): + print('%r: Text channel %r is ready', self, channel) + self.text_channel = channel + self._tubes_ready() + + def _tubes_ready(self): + if self.text_channel is None or \ + self.tubes_channel is None: + return + + print('%r: finished setting up tubes', self) + + self._add_self_to_channel() + + def __text_channel_group_flags_changed_cb(self, added, removed): + print('__text_channel_group_flags_changed_cb %r %r', added, + removed) + self.text_channel_group_flags |= added + self.text_channel_group_flags &= ~removed + + def _add_self_to_channel(self): + # FIXME: cope with non-Group channels here if we want to support + # non-OLPC-compatible IMs + + group = self.text_channel[CHANNEL_INTERFACE_GROUP] + + def got_all_members(members, local_pending, remote_pending): + print('got_all_members members %r local_pending %r ' + 'remote_pending %r', members, local_pending, + remote_pending) + + if self.text_channel_group_flags & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + self_handle = self.channel_self_handle + else: + self_handle = self._global_self_handle + + if self_handle in local_pending: + print('%r: We are in local pending - entering', self) + group.AddMembers([self_handle], '', + reply_handler=lambda: None, + error_handler=lambda e: self._join_failed_cb(e, + 'got_all_members AddMembers')) + + if members: + self.__text_channel_members_changed_cb('', members, (), + (), (), 0, 0) + + def got_group_flags(flags): + self.text_channel_group_flags = flags + # by the time we hook this, we need to know the group flags + group.connect_to_signal('MembersChanged', + self.__text_channel_members_changed_cb) + + # bootstrap by getting the current state. This is where we find + # out whether anyone was lying to us in their PEP info + group.GetAllMembers(reply_handler=got_all_members, + error_handler=self.__error_handler_cb) + + def got_self_handle(channel_self_handle): + self.channel_self_handle = channel_self_handle + group.connect_to_signal('GroupFlagsChanged', + self.__text_channel_group_flags_changed_cb) + group.GetGroupFlags(reply_handler=got_group_flags, + error_handler=self.__error_handler_cb) + + group.GetSelfHandle(reply_handler=got_self_handle, + error_handler=self.__error_handler_cb) + + def __text_channel_members_changed_cb(self, message, added, removed, + local_pending, remote_pending, + actor, reason): + print('__text_channel_members_changed_cb added %r removed %r ' + 'local_pending %r remote_pending %r channel_self_handle ' + '%r', added, removed, local_pending, remote_pending, + self.channel_self_handle) + + if self.text_channel_group_flags & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + self_handle = self.channel_self_handle + else: + self_handle = self._global_self_handle + + if self_handle in added: + if PROPERTIES_INTERFACE not in self.text_channel: + self._finished = True + self.emit('finished', None) + else: + self.text_channel[PROPERTIES_INTERFACE].ListProperties( + reply_handler=self.__list_properties_cb, + error_handler=self.__error_handler_cb) + + def __list_properties_cb(self, prop_specs): + # FIXME: invite-only ought to be set on private activities; but + # since only the owner can change invite-only, that would break + # activity scope changes. + props = { + # otherwise buddy resolution breaks + 'anonymous': False, + # anyone who knows about the channel can join + 'invite-only': False, + # so non-owners can invite others + 'invite-restricted': False, + # vanish when there are no members + 'persistent': False, + # don't appear in server room lists + 'private': True, + } + props_to_set = [] + for ident, name, sig_, flags in prop_specs: + value = props.pop(name, None) + if value is not None: + if flags & PROPERTY_FLAG_WRITE: + props_to_set.append((ident, value)) + # FIXME: else error, but only if we're creating the room? + # FIXME: if props is nonempty, then we want to set props that aren't + # supported here - raise an error? + + if props_to_set: + self.text_channel[PROPERTIES_INTERFACE].SetProperties( + props_to_set, reply_handler=self.__set_properties_cb, + error_handler=self.__error_handler_cb) + else: + self._finished = True + self.emit('finished', None) + + def __set_properties_cb(self): + self._finished = True + self.emit('finished', None) -- cgit v0.9.1