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 --- diff --git a/collaboration/__init__.py b/collaboration/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/collaboration/__init__.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) diff --git a/collaboration/buddy.py b/collaboration/buddy.py new file mode 100644 index 0000000..b117940 --- /dev/null +++ b/collaboration/buddy.py @@ -0,0 +1,234 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gobject +import gconf +import dbus +from telepathy.client import Connection +from telepathy.interfaces import CONNECTION + +from xocolor import XoColor +import connection_watcher + + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' + +_owner_instance = None + + +class BaseBuddyModel(gobject.GObject): + __gtype_name__ = 'SugarBaseBuddyModel' + + def __init__(self, **kwargs): + self._key = None + self._nick = None + self._color = None + self._tags = None + self._current_activity = None + + gobject.GObject.__init__(self, **kwargs) + + def get_nick(self): + return self._nick + + def set_nick(self, nick): + self._nick = nick + + nick = gobject.property(type=object, getter=get_nick, setter=set_nick) + + def get_key(self): + return self._key + + def set_key(self, key): + self._key = key + + key = gobject.property(type=object, getter=get_key, setter=set_key) + + def get_color(self): + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_tags(self): + return self._tags + + tags = gobject.property(type=object, getter=get_tags) + + def get_current_activity(self): + return self._current_activity + + def set_current_activity(self, current_activity): + if self._current_activity != current_activity: + self._current_activity = current_activity + self.notify('current-activity') + + current_activity = gobject.property(type=object, + getter=get_current_activity, + setter=set_current_activity) + + def is_owner(self): + raise NotImplementedError + + +class OwnerBuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarOwnerBuddyModel' + + def __init__(self): + BaseBuddyModel.__init__(self) + + #client = gconf.client_get_default() + #self.props.nick = client.get_string('/desktop/sugar/user/nick') + self.props.nick = "rgs" + #color = client.get_string('/desktop/sugar/user/color') + self.props.color = XoColor(None) + + #self.props.key = get_profile().pubkey + self.props.key = "foobar" + + self.connect('notify::nick', self.__property_changed_cb) + self.connect('notify::color', self.__property_changed_cb) + self.connect('notify::current-activity', + self.__current_activity_changed_cb) + + bus = dbus.SessionBus() + bus.add_signal_receiver( + self.__name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') + + bus_object = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH) + for service in bus_object.ListNames( + dbus_interface=dbus.BUS_DAEMON_IFACE): + if service.startswith(CONNECTION + '.'): + path = '/%s' % service.replace('.', '/') + Connection(service, path, bus, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + self._sync_properties_on_connection(connection) + + def __name_owner_changed_cb(self, name, old, new): + if name.startswith(CONNECTION + '.') and not old and new: + path = '/' + name.replace('.', '/') + Connection(name, path, ready_handler=self.__connection_ready_cb) + + def __property_changed_cb(self, buddy, pspec): + self._sync_properties() + + def __current_activity_changed_cb(self, buddy, pspec): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + if self.props.current_activity is not None: + activity_id = self.props.current_activity.activity_id + room_handle = self.props.current_activity.room_handle + else: + activity_id = '' + room_handle = 0 + + connection[CONNECTION_INTERFACE_BUDDY_INFO].SetCurrentActivity( + activity_id, + room_handle, + reply_handler=self.__set_current_activity_cb, + error_handler=self.__error_handler_cb) + + def __set_current_activity_cb(self): + logging.debug('__set_current_activity_cb') + + def _sync_properties(self): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + self._sync_properties_on_connection(connection) + + def _sync_properties_on_connection(self, connection): + if CONNECTION_INTERFACE_BUDDY_INFO in connection: + properties = {} + if self.props.key is not None: + properties['key'] = dbus.ByteArray(self.props.key) + if self.props.color is not None: + properties['color'] = self.props.color.to_string() + + logging.debug('calling SetProperties with %r', properties) + connection[CONNECTION_INTERFACE_BUDDY_INFO].SetProperties( + properties, + reply_handler=self.__set_properties_cb, + error_handler=self.__error_handler_cb) + + def __set_properties_cb(self): + logging.debug('__set_properties_cb') + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def __connection_added_cb(self, conn_watcher, connection): + self._sync_properties_on_connection(connection) + + def is_owner(self): + return True + + +def get_owner_instance(): + global _owner_instance + if _owner_instance is None: + _owner_instance = OwnerBuddyModel() + return _owner_instance + + +class BuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarBuddyModel' + + def __init__(self, **kwargs): + + self._account = None + self._contact_id = None + self._handle = None + + BaseBuddyModel.__init__(self, **kwargs) + + def is_owner(self): + return False + + def get_account(self): + return self._account + + def set_account(self, account): + self._account = account + + account = gobject.property(type=object, getter=get_account, + setter=set_account) + + def get_contact_id(self): + return self._contact_id + + def set_contact_id(self, contact_id): + self._contact_id = contact_id + + contact_id = gobject.property(type=object, getter=get_contact_id, + setter=set_contact_id) + + def get_handle(self): + return self._handle + + def set_handle(self, handle): + self._handle = handle + + handle = gobject.property(type=object, getter=get_handle, + setter=set_handle) diff --git a/collaboration/connection_watcher.py b/collaboration/connection_watcher.py new file mode 100644 index 0000000..96af1cf --- /dev/null +++ b/collaboration/connection_watcher.py @@ -0,0 +1,122 @@ +# This should eventually land in telepathy-python, so has the same license: +# Copyright (C) 2008 Collabora Ltd. +# +# This program 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.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser 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 + +# FIXME: this sould go upstream, in telepathy-python + +import logging + +import dbus +import dbus.mainloop.glib +import gobject + +from telepathy.client import Connection +from telepathy.interfaces import CONN_INTERFACE +from telepathy.constants import CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED + + +_instance = None + + +class ConnectionWatcher(gobject.GObject): + __gsignals__ = { + 'connection-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'connection-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, bus=None): + gobject.GObject.__init__(self) + + if bus is None: + self.bus = dbus.Bus() + else: + self.bus = bus + + # D-Bus path -> Connection + self._connections = {} + + self.bus.add_signal_receiver(self._status_changed_cb, + dbus_interface=CONN_INTERFACE, signal_name='StatusChanged', + path_keyword='path') + + for conn in Connection.get_connections(bus): + conn.call_when_ready(self._conn_ready_cb) + + def _status_changed_cb(self, *args, **kwargs): + path = kwargs['path'] + if not path.startswith('/org/freedesktop/Telepathy/Connection/'): + return + + status, reason_ = args + service_name = path.replace('/', '.')[1:] + + if status == CONNECTION_STATUS_CONNECTED: + self._add_connection(service_name, path) + elif status == CONNECTION_STATUS_DISCONNECTED: + self._remove_connection(service_name, path) + + def _conn_ready_cb(self, conn): + if conn.object_path in self._connections: + return + + self._connections[conn.object_path] = conn + self.emit('connection-added', conn) + + def _add_connection(self, service_name, path): + if path in self._connections: + return + + try: + Connection(service_name, path, ready_handler=self._conn_ready_cb) + except dbus.exceptions.DBusException: + logging.debug('%s is propably already gone.', service_name) + + def _remove_connection(self, service_name, path): + conn = self._connections.pop(path, None) + if conn is None: + return + + self.emit('connection-removed', conn) + + def get_connections(self): + return self._connections.values() + + +def get_instance(): + global _instance + if _instance is None: + _instance = ConnectionWatcher() + return _instance + + +if __name__ == '__main__': + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + def connection_added_cb(conn_watcher, conn): + print 'new connection', conn.service_name + + def connection_removed_cb(conn_watcher, conn): + print 'removed connection', conn.service_name + + watcher = ConnectionWatcher() + watcher.connect('connection-added', connection_added_cb) + watcher.connect('connection-removed', connection_removed_cb) + + loop = gobject.MainLoop() + loop.run() diff --git a/collaboration/connectionmanager.py b/collaboration/connectionmanager.py new file mode 100644 index 0000000..5ae59dd --- /dev/null +++ b/collaboration/connectionmanager.py @@ -0,0 +1,122 @@ +# 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. + +""" +UNSTABLE. It should really be internal to the sugar.presence package. +""" + +from functools import partial + +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import ACCOUNT, \ + ACCOUNT_MANAGER +from telepathy.constants import CONNECTION_STATUS_CONNECTED + +ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' + + +class Connection(object): + def __init__(self, account_path, connection): + self.account_path = account_path + self.connection = connection + self.connected = False + + +class ConnectionManager(object): + """Track available telepathy connections""" + + def __init__(self): + self._connections_per_account = {} + + bus = dbus.SessionBus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_paths = account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts', + dbus_interface=PROPERTIES_IFACE) + for account_path in account_paths: + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path) + obj.connect_to_signal('AccountPropertyChanged', + partial(self.__account_property_changed_cb, account_path)) + connection_path = obj.Get(ACCOUNT, 'Connection') + if connection_path != '/': + try: + self._track_connection(account_path, connection_path) + except: + pass + + def __account_property_changed_cb(self, account_path, properties): + if 'Connection' not in properties: + return + if properties['Connection'] == '/': + if account_path in self._connections_per_account: + del self._connections_per_account[account_path] + else: + self._track_connection(account_path, properties['Connection']) + + def _track_connection(self, account_path, connection_path): + connection_name = connection_path.replace('/', '.')[1:] + bus = dbus.SessionBus() + connection = bus.get_object(connection_name, connection_path) + connection.connect_to_signal('StatusChanged', + partial(self.__status_changed_cb, account_path)) + self._connections_per_account[account_path] = \ + Connection(account_path, connection) + + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path) + status = account.Get(ACCOUNT, 'ConnectionStatus') + if status == CONNECTION_STATUS_CONNECTED: + self._connections_per_account[account_path].connected = True + else: + self._connections_per_account[account_path].connected = False + + def __status_changed_cb(self, account_path, status, reason): + if status == CONNECTION_STATUS_CONNECTED: + self._connections_per_account[account_path].connected = True + else: + self._connections_per_account[account_path].connected = False + + def get_preferred_connection(self): + best_connection = None, None + for account_path, connection in self._connections_per_account.items(): + if 'salut' in account_path and connection.connected: + best_connection = account_path, connection.connection + elif 'gabble' in account_path and connection.connected: + best_connection = account_path, connection.connection + break + return best_connection + + def get_connection(self, account_path): + return self._connections_per_account[account_path].connection + + def get_connections_per_account(self): + return self._connections_per_account + + def get_account_for_connection(self, connection_path): + for account_path, connection in self._connections_per_account.items(): + if connection.connection.object_path == connection_path: + return account_path + return None + + +_connection_manager = None +def get_connection_manager(): + global _connection_manager + if not _connection_manager: + _connection_manager = ConnectionManager() + return _connection_manager diff --git a/collaboration/dispatch/Makefile.am b/collaboration/dispatch/Makefile.am new file mode 100644 index 0000000..eb44a32 --- /dev/null +++ b/collaboration/dispatch/Makefile.am @@ -0,0 +1,9 @@ +sugardir = $(pythondir)/sugar/dispatch +sugar_PYTHON = \ + __init__.py \ + dispatcher.py \ + saferef.py + +EXTRA_DIST = \ + license.txt + diff --git a/collaboration/dispatch/__init__.py b/collaboration/dispatch/__init__.py new file mode 100644 index 0000000..9f0a092 --- /dev/null +++ b/collaboration/dispatch/__init__.py @@ -0,0 +1,10 @@ +"""Multi-consumer multi-producer dispatching mechanism + +Originally based on pydispatch (BSD) +http://pypi.python.org/pypi/PyDispatcher/2.0.1 +See license.txt for original license. + +Heavily modified for Django's purposes. +""" + +from dispatcher import Signal diff --git a/collaboration/dispatch/dispatcher.py b/collaboration/dispatch/dispatcher.py new file mode 100644 index 0000000..45c32fe --- /dev/null +++ b/collaboration/dispatch/dispatcher.py @@ -0,0 +1,191 @@ +import weakref +import saferef + +WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref) + + +def _make_id(target): + if hasattr(target, 'im_func'): + return (id(target.im_self), id(target.im_func)) + return id(target) + + +class Signal(object): + """Base class for all signals + + Internal attributes: + receivers -- { receriverkey (id) : weakref(receiver) } + """ + + def __init__(self, providing_args=None): + """providing_args -- A list of the arguments this signal can pass along + in a send() call. + """ + self.receivers = [] + if providing_args is None: + providing_args = [] + self.providing_args = set(providing_args) + + def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): + """Connect receiver to sender for signal + + receiver -- a function or an instance method which is to + receive signals. Receivers must be + hashable objects. + + if weak is True, then receiver must be weak-referencable + (more precisely saferef.safeRef() must be able to create + a reference to the receiver). + + Receivers must be able to accept keyword arguments. + + If receivers have a dispatch_uid attribute, the receiver will + not be added if another receiver already exists with that + dispatch_uid. + + sender -- the sender to which the receiver should respond + Must either be of type Signal, or None to receive events + from any sender. + + weak -- whether to use weak references to the receiver + By default, the module will attempt to use weak + references to the receiver objects. If this parameter + is false, then strong references will be used. + + dispatch_uid -- an identifier used to uniquely identify a particular + instance of a receiver. This will usually be a string, though it + may be anything hashable. + + returns None + """ + if dispatch_uid: + lookup_key = (dispatch_uid, _make_id(sender)) + else: + lookup_key = (_make_id(receiver), _make_id(sender)) + + if weak: + receiver = saferef.safeRef(receiver, + onDelete=self._remove_receiver) + + for r_key, _ in self.receivers: + if r_key == lookup_key: + break + else: + self.receivers.append((lookup_key, receiver)) + + def disconnect(self, receiver=None, sender=None, weak=True, + dispatch_uid=None): + """Disconnect receiver from sender for signal + + receiver -- the registered receiver to disconnect. May be none if + dispatch_uid is specified. + sender -- the registered sender to disconnect + weak -- the weakref state to disconnect + dispatch_uid -- the unique identifier of the receiver to disconnect + + disconnect reverses the process of connect. + + If weak references are used, disconnect need not be called. + The receiver will be remove from dispatch automatically. + + returns None + """ + + if dispatch_uid: + lookup_key = (dispatch_uid, _make_id(sender)) + else: + lookup_key = (_make_id(receiver), _make_id(sender)) + + for idx, (r_key, _) in enumerate(self.receivers): + if r_key == lookup_key: + del self.receivers[idx] + + def send(self, sender, **named): + """Send signal from sender to all connected receivers. + + sender -- the sender of the signal + Either a specific object or None. + + named -- named arguments which will be passed to receivers. + + Returns a list of tuple pairs [(receiver, response), ... ]. + + If any receiver raises an error, the error propagates back + through send, terminating the dispatch loop, so it is quite + possible to not have all receivers called if a raises an + error. + """ + + responses = [] + if not self.receivers: + return responses + + for receiver in self._live_receivers(_make_id(sender)): + response = receiver(signal=self, sender=sender, **named) + responses.append((receiver, response)) + return responses + + def send_robust(self, sender, **named): + """Send signal from sender to all connected receivers catching errors + + sender -- the sender of the signal + Can be any python object (normally one registered with + a connect if you actually want something to occur). + + named -- named arguments which will be passed to receivers. + These arguments must be a subset of the argument names + defined in providing_args. + + Return a list of tuple pairs [(receiver, response), ... ], + may raise DispatcherKeyError + + if any receiver raises an error (specifically any subclass of + Exception), the error instance is returned as the result for that + receiver. + """ + + responses = [] + if not self.receivers: + return responses + + # Call each receiver with whatever arguments it can accept. + # Return a list of tuple pairs [(receiver, response), ... ]. + for receiver in self._live_receivers(_make_id(sender)): + try: + response = receiver(signal=self, sender=sender, **named) + except Exception, err: + responses.append((receiver, err)) + else: + responses.append((receiver, response)) + return responses + + def _live_receivers(self, senderkey): + """Filter sequence of receivers to get resolved, live receivers + + This checks for weak references + and resolves them, then returning only live + receivers. + """ + none_senderkey = _make_id(None) + + for (receiverkey_, r_senderkey), receiver in self.receivers: + if r_senderkey == none_senderkey or r_senderkey == senderkey: + if isinstance(receiver, WEAKREF_TYPES): + # Dereference the weak reference. + receiver = receiver() + if receiver is not None: + yield receiver + else: + yield receiver + + def _remove_receiver(self, receiver): + """Remove dead receivers from connections.""" + + to_remove = [] + for key, connected_receiver in self.receivers: + if connected_receiver == receiver: + to_remove.append(key) + for key in to_remove: + for idx, (r_key, _) in enumerate(self.receivers): + if r_key == key: + del self.receivers[idx] diff --git a/collaboration/dispatch/license.txt b/collaboration/dispatch/license.txt new file mode 100644 index 0000000..0272c28 --- /dev/null +++ b/collaboration/dispatch/license.txt @@ -0,0 +1,66 @@ +sugar.dispatch was originally forked from django.dispatch + +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +django.dispatch was originally forked from PyDispatcher. + +PyDispatcher License: + + Copyright (c) 2001-2003, Patrick K. O'Brien and Contributors + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + The name of Patrick K. O'Brien, or the name of any Contributor, + may not be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/collaboration/dispatch/saferef.py b/collaboration/dispatch/saferef.py new file mode 100644 index 0000000..bb73b5d --- /dev/null +++ b/collaboration/dispatch/saferef.py @@ -0,0 +1,254 @@ +""" +"Safe weakrefs", originally from pyDispatcher. + +Provides a way to safely weakref any function, including bound methods (which +aren't handled by the core weakref module). +""" + +import weakref +import traceback + + +def safeRef(target, onDelete=None): + """Return a *safe* weak reference to a callable target + + target -- the object to be weakly referenced, if it's a + bound method reference, will create a BoundMethodWeakref, + otherwise creates a simple weakref. + onDelete -- if provided, will have a hard reference stored + to the callable to be called after the safe reference + goes out of scope with the reference object, (either a + weakref or a BoundMethodWeakref) as argument. + """ + if hasattr(target, 'im_self'): + if target.im_self is not None: + # Turn a bound method into a BoundMethodWeakref instance. + # Keep track of these instances for lookup by disconnect(). + if not hasattr(target, 'im_func'): + raise TypeError("safeRef target %r has im_self, but no" + " im_func, don't know how to create reference" % + (target, )) + reference = get_bound_method_weakref(target=target, + onDelete=onDelete) + return reference + if callable(onDelete): + return weakref.ref(target, onDelete) + else: + return weakref.ref(target) + + +class BoundMethodWeakref(object): + """'Safe' and reusable weak references to instance methods + + BoundMethodWeakref objects provide a mechanism for + referencing a bound method without requiring that the + method object itself (which is normally a transient + object) is kept alive. Instead, the BoundMethodWeakref + object keeps weak references to both the object and the + function which together define the instance method. + + Attributes: + key -- the identity key for the reference, calculated + by the class's calculateKey method applied to the + target instance method + deletionMethods -- sequence of callable objects taking + single argument, a reference to this object which + will be called when *either* the target object or + target function is garbage collected (i.e. when + this object becomes invalid). These are specified + as the onDelete parameters of safeRef calls. + weakSelf -- weak reference to the target object + weakFunc -- weak reference to the target function + + Class Attributes: + _allInstances -- class attribute pointing to all live + BoundMethodWeakref objects indexed by the class's + calculateKey(target) method applied to the target + objects. This weak value dictionary is used to + short-circuit creation so that multiple references + to the same (object, function) pair produce the + same BoundMethodWeakref instance. + + """ + + _allInstances = weakref.WeakValueDictionary() + + def __new__(cls, target, onDelete=None, *arguments, **named): + """Create new instance or return current instance + + Basically this method of construction allows us to + short-circuit creation of references to already- + referenced instance methods. The key corresponding + to the target is calculated, and if there is already + an existing reference, that is returned, with its + deletionMethods attribute updated. Otherwise the + new instance is created and registered in the table + of already-referenced methods. + """ + key = cls.calculateKey(target) + current = cls._allInstances.get(key) + if current is not None: + current.deletionMethods.append(onDelete) + return current + else: + base = super(BoundMethodWeakref, cls).__new__(cls) + cls._allInstances[key] = base + base.__init__(target, onDelete, *arguments, **named) + return base + + def __init__(self, target, onDelete=None): + """Return a weak-reference-like instance for a bound method + + target -- the instance-method target for the weak + reference, must have im_self and im_func attributes + and be reconstructable via: + target.im_func.__get__( target.im_self ) + which is true of built-in instance methods. + onDelete -- optional callback which will be called + when this weak reference ceases to be valid + (i.e. either the object or the function is garbage + collected). Should take a single argument, + which will be passed a pointer to this object. + """ + def remove(weak, self=self): + """Set self.isDead to true when method or instance is destroyed""" + methods = self.deletionMethods[:] + del self.deletionMethods[:] + try: + # pylint: disable=W0212 + del self.__class__._allInstances[self.key] + except KeyError: + pass + for function in methods: + try: + if callable(function): + function(self) + except Exception, e: + try: + traceback.print_exc() + except AttributeError: + print ('Exception during saferef %s cleanup function' + ' %s: %s' % (self, function, e)) + self.deletionMethods = [onDelete] + self.key = self.calculateKey(target) + self.weakSelf = weakref.ref(target.im_self, remove) + self.weakFunc = weakref.ref(target.im_func, remove) + self.selfName = str(target.im_self) + self.funcName = str(target.im_func.__name__) + + def calculateKey(cls, target): + """Calculate the reference key for this reference + + Currently this is a two-tuple of the id()'s of the + target object and the target function respectively. + """ + return (id(target.im_self), id(target.im_func)) + calculateKey = classmethod(calculateKey) + + def __str__(self): + """Give a friendly representation of the object""" + return '%s( %s.%s )' % (self.__class__.__name__, self.selfName, + self.funcName) + + __repr__ = __str__ + + def __nonzero__(self): + """Whether we are still a valid reference""" + return self() is not None + + def __cmp__(self, other): + """Compare with another reference""" + if not isinstance(other, self.__class__): + return cmp(self.__class__, type(other)) + return cmp(self.key, other.key) + + def __call__(self): + """Return a strong reference to the bound method + + If the target cannot be retrieved, then will + return None, otherwise returns a bound instance + method for our object and function. + + Note: + You may call this method any number of times, + as it does not invalidate the reference. + """ + target = self.weakSelf() + if target is not None: + function = self.weakFunc() + if function is not None: + return function.__get__(target) + return None + + +class BoundNonDescriptorMethodWeakref(BoundMethodWeakref): + """A specialized BoundMethodWeakref, for platforms where instance methods + are not descriptors. + + It assumes that the function name and the target attribute name are the + same, instead of assuming that the function is a descriptor. This approach + is equally fast, but not 100% reliable because functions can be stored on + an attribute named differenty than the function's name such as in: + + class A: pass + def foo(self): return 'foo' + A.bar = foo + + But this shouldn't be a common use case. So, on platforms where methods + aren't descriptors (such as Jython) this implementation has the advantage + of working in the most cases. + """ + def __init__(self, target, onDelete=None): + """Return a weak-reference-like instance for a bound method + + target -- the instance-method target for the weak + reference, must have im_self and im_func attributes + and be reconstructable via: + target.im_func.__get__( target.im_self ) + which is true of built-in instance methods. + onDelete -- optional callback which will be called + when this weak reference ceases to be valid + (i.e. either the object or the function is garbage + collected). Should take a single argument, + which will be passed a pointer to this object. + """ + assert getattr(target.im_self, target.__name__) == target, \ + ("method %s isn't available as the attribute %s of %s" % + (target, target.__name__, target.im_self)) + super(BoundNonDescriptorMethodWeakref, self).__init__(target, onDelete) + + def __call__(self): + """Return a strong reference to the bound method + + If the target cannot be retrieved, then will + return None, otherwise returns a bound instance + method for our object and function. + + Note: + You may call this method any number of times, + as it does not invalidate the reference. + """ + target = self.weakSelf() + if target is not None: + function = self.weakFunc() + if function is not None: + # Using curry() would be another option, but it erases the + # "signature" of the function. That is, after a function is + # curried, the inspect module can't be used to determine how + # many arguments the function expects, nor what keyword + # arguments it supports, and pydispatcher needs this + # information. + return getattr(target, function.__name__) + return None + + +def get_bound_method_weakref(target, onDelete): + """Instantiates the appropiate BoundMethodWeakRef, depending on the details + of the underlying class method implementation""" + if hasattr(target, '__get__'): + # target method is a descriptor, so the default implementation works: + return BoundMethodWeakref(target=target, onDelete=onDelete) + else: + # no luck, use the alternative implementation: + return BoundNonDescriptorMethodWeakref(target=target, + onDelete=onDelete) diff --git a/collaboration/neighborhood.py b/collaboration/neighborhood.py new file mode 100755 index 0000000..ad7ce21 --- /dev/null +++ b/collaboration/neighborhood.py @@ -0,0 +1,1038 @@ +#!/usr/bin/python +# +# neighborhood.py +# +# + +from functools import partial +from hashlib import sha1 + +import traceback +import gobject +import gconf +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import ACCOUNT, \ + ACCOUNT_MANAGER, \ + CHANNEL, \ + CHANNEL_INTERFACE_GROUP, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_FILE_TRANSFER, \ + CLIENT, \ + CONNECTION, \ + CONNECTION_INTERFACE_ALIASING, \ + CONNECTION_INTERFACE_CONTACTS, \ + CONNECTION_INTERFACE_CONTACT_CAPABILITIES, \ + CONNECTION_INTERFACE_REQUESTS, \ + CONNECTION_INTERFACE_SIMPLE_PRESENCE +from telepathy.constants import HANDLE_TYPE_CONTACT, \ + HANDLE_TYPE_LIST, \ + CONNECTION_PRESENCE_TYPE_OFFLINE, \ + CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED +from telepathy.client import Connection, Channel + +from buddy import get_owner_instance +from buddy import BuddyModel + +from xocolor import XoColor + +import activity + +from connectionmanager import get_connection_manager + +import signal, os, sys +from activity import Activity + +ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' +CHANNEL_DISPATCHER_SERVICE = 'org.freedesktop.Telepathy.ChannelDispatcher' +CHANNEL_DISPATCHER_PATH = '/org/freedesktop/Telepathy/ChannelDispatcher' +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' + +_QUERY_DBUS_TIMEOUT = 200 +""" +Time in seconds to wait when querying contact properties. Some jabber servers +will be very slow in returning these queries, so just be patient. +""" + + +class ActivityModel(gobject.GObject): + __gsignals__ = { + 'current-buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'current-buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self, activity_id, room_handle): + gobject.GObject.__init__(self) + + self.activity_id = activity_id + self.room_handle = room_handle + self._bundle = None + self._color = None + self._private = True + self._name = None + self._current_buddies = [] + self._buddies = [] + + def get_color(self): + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_bundle(self): + return self._bundle + + def set_bundle(self, bundle): + self._bundle = bundle + + bundle = gobject.property(type=object, getter=get_bundle, + setter=set_bundle) + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = gobject.property(type=object, getter=get_name, setter=set_name) + + def is_private(self): + return self._private + + def set_private(self, private): + self._private = private + + private = gobject.property(type=object, getter=is_private, + setter=set_private) + + def get_buddies(self): + return self._buddies + + def add_buddy(self, buddy): + self._buddies.append(buddy) + self.notify('buddies') + self.emit('buddy-added', buddy) + + def remove_buddy(self, buddy): + self._buddies.remove(buddy) + self.notify('buddies') + self.emit('buddy-removed', buddy) + + buddies = gobject.property(type=object, getter=get_buddies) + + def get_current_buddies(self): + return self._current_buddies + + def add_current_buddy(self, buddy): + self._current_buddies.append(buddy) + self.notify('current-buddies') + self.emit('current-buddy-added', buddy) + + def remove_current_buddy(self, buddy): + self._current_buddies.remove(buddy) + self.notify('current-buddies') + self.emit('current-buddy-removed', buddy) + + current_buddies = gobject.property(type=object, getter=get_current_buddies) + + +class _Account(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object, object])), + 'buddy-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'current-activity-updated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object, object])), + 'connected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'disconnected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, account_path): + gobject.GObject.__init__(self) + + self.object_path = account_path + + self._connection = None + self._buddy_handles = {} + self._activity_handles = {} + self._self_handle = None + + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self._start_listening() + + def _start_listening(self): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'Connection', + reply_handler=self.__got_connection_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnection')) + obj.connect_to_signal( + 'AccountPropertyChanged', self.__account_property_changed_cb) + + def __error_handler_cb(self, function_name, error): + raise RuntimeError('Error when calling %s: %s' % (function_name, + error)) + + def __got_connection_cb(self, connection_path): + #print('_Account.__got_connection_cb %r', connection_path) + + if connection_path == '/': + self._check_registration_error() + return + + self._prepare_connection(connection_path) + + def _check_registration_error(self): + """ + See if a previous connection attempt failed and we need to unset + the register flag. + """ + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'ConnectionError', + reply_handler=self.__got_connection_error_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnectionError')) + + def __got_connection_error_cb(self, error): + #print('_Account.__got_connection_error_cb %r', error) + if error == 'org.freedesktop.Telepathy.Error.RegistrationExists': + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.UpdateParameters({'register': False}, [], + dbus_interface=ACCOUNT) + + def __account_property_changed_cb(self, properties): + #print('_Account.__account_property_changed_cb %r %r %r', + #self.object_path, properties.get('Connection', None), + # self._connection) + if 'Connection' not in properties: + return + if properties['Connection'] == '/': + self._check_registration_error() + self._connection = None + elif self._connection is None: + self._prepare_connection(properties['Connection']) + + def _prepare_connection(self, connection_path): + connection_name = connection_path.replace('/', '.')[1:] + print("Preparing %s" % connection_name) + self._connection = Connection(connection_name, connection_path, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + print('_Account.__connection_ready_cb %r', connection.object_path) + connection.connect_to_signal('StatusChanged', + self.__status_changed_cb) + + connection[PROPERTIES_IFACE].Get(CONNECTION, + 'Status', + reply_handler=self.__get_status_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetStatus')) + + def __get_status_cb(self, status): + #print('_Account.__get_status_cb %r %r', + #self._connection.object_path, status) + self._update_status(status) + + def __status_changed_cb(self, status, reason): + #print('_Account.__status_changed_cb %r %r', status, reason) + self._update_status(status) + + def _update_status(self, status): + if status == CONNECTION_STATUS_CONNECTED: + self._connection[PROPERTIES_IFACE].Get(CONNECTION, + 'SelfHandle', + reply_handler=self.__get_self_handle_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetSelfHandle')) + self.emit('connected') + else: + for contact_handle, contact_id in self._buddy_handles.items(): + if contact_id is not None: + self.emit('buddy-removed', contact_id) + + for room_handle, activity_id in self._activity_handles.items(): + self.emit('activity-removed', activity_id) + + self._buddy_handles = {} + self._activity_handles = {} + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self.emit('disconnected') + + if status == CONNECTION_STATUS_DISCONNECTED: + self._connection = None + + def __get_self_handle_cb(self, self_handle): + self._self_handle = self_handle + + if CONNECTION_INTERFACE_CONTACT_CAPABILITIES in self._connection: + interface = CONNECTION_INTERFACE_CONTACT_CAPABILITIES + connection = self._connection[interface] + client_name = CLIENT + '.Sugar.FileTransfer' + file_transfer_channel_class = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_CONTACT} + capabilities = [] + connection.UpdateCapabilities( + [(client_name, [file_transfer_channel_class], capabilities)], + reply_handler=self.__update_capabilities_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.UpdateCapabilities')) + + connection = self._connection[CONNECTION_INTERFACE_ALIASING] + connection.connect_to_signal('AliasesChanged', + self.__aliases_changed_cb) + + connection = self._connection[CONNECTION_INTERFACE_SIMPLE_PRESENCE] + connection.connect_to_signal('PresencesChanged', + self.__presences_changed_cb) + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.connect_to_signal('PropertiesChanged', + self.__buddy_info_updated_cb, + byte_arrays=True) + + connection.connect_to_signal('ActivitiesChanged', + self.__buddy_activities_changed_cb) + + connection.connect_to_signal('CurrentActivityChanged', + self.__current_activity_changed_cb) + else: + print('Connection %s does not support OLPC buddy ' + 'properties', self._connection.object_path) + pass + + if CONNECTION_INTERFACE_ACTIVITY_PROPERTIES in self._connection: + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.connect_to_signal( + 'ActivityPropertiesChanged', + self.__activity_properties_changed_cb) + else: + print('Connection %s does not support OLPC activity ' + 'properties', self._connection.object_path) + pass + + properties = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_CONTACT_LIST, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_LIST, + CHANNEL + '.TargetID': 'subscribe', + } + properties = dbus.Dictionary(properties, signature='sv') + connection = self._connection[CONNECTION_INTERFACE_REQUESTS] + is_ours, channel_path, properties = \ + connection.EnsureChannel(properties) + + channel = Channel(self._connection.service_name, channel_path) + channel[CHANNEL_INTERFACE_GROUP].connect_to_signal( + 'MembersChanged', self.__members_changed_cb) + + channel[PROPERTIES_IFACE].Get(CHANNEL_INTERFACE_GROUP, + 'Members', + reply_handler=self.__get_members_ready_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetMembers')) + + def __update_capabilities_cb(self): + pass + + def __aliases_changed_cb(self, aliases): + #print('_Account.__aliases_changed_cb') + for handle, alias in aliases: + if handle in self._buddy_handles: + #print('Got handle %r with nick %r, going to update', + # handle, alias) + properties = {CONNECTION_INTERFACE_ALIASING + '/alias': alias} + self.emit('buddy-updated', self._buddy_handles[handle], + properties) + + def __presences_changed_cb(self, presences): + #print('_Account.__presences_changed_cb %r', presences) + for handle, presence in presences.iteritems(): + if handle in self._buddy_handles: + presence_type, status_, message_ = presence + if presence_type == CONNECTION_PRESENCE_TYPE_OFFLINE: + contact_id = self._buddy_handles[handle] + del self._buddy_handles[handle] + self.emit('buddy-removed', contact_id) + + def __buddy_info_updated_cb(self, handle, properties): + #print('_Account.__buddy_info_updated_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __current_activity_changed_cb(self, contact_handle, activity_id, + room_handle): + #print('_Account.__current_activity_changed_cb %r %r %r', + # contact_handle, activity_id, room_handle) + if contact_handle in self._buddy_handles: + contact_id = self._buddy_handles[contact_handle] + if not activity_id and room_handle: + activity_id = self._activity_handles.get(room_handle, '') + self.emit('current-activity-updated', contact_id, activity_id) + + def __get_current_activity_cb(self, contact_handle, activity_id, + room_handle): + #print('_Account.__get_current_activity_cb %r %r %r', + # contact_handle, activity_id, room_handle) + contact_id = self._buddy_handles[contact_handle] + self.emit('current-activity-updated', contact_id, activity_id) + + def __buddy_activities_changed_cb(self, buddy_handle, activities): + self._update_buddy_activities(buddy_handle, activities) + + def _update_buddy_activities(self, buddy_handle, activities): + #print('_Account._update_buddy_activities') + if not buddy_handle in self._buddy_handles: + self._buddy_handles[buddy_handle] = None + + if not buddy_handle in self._activities_per_buddy: + self._activities_per_buddy[buddy_handle] = set() + + for activity_id, room_handle in activities: + if room_handle not in self._activity_handles: + self._activity_handles[room_handle] = activity_id + self.emit('activity-added', room_handle, activity_id) + + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.GetProperties(room_handle, + reply_handler=partial(self.__get_properties_cb, + room_handle), + error_handler=partial(self.__error_handler_cb, + 'ActivityProperties.GetProperties')) + + # Sometimes we'll get CurrentActivityChanged before we get to + # know about the activity so we miss the event. In that case, + # request again the current activity for this buddy. + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetCurrentActivity( + buddy_handle, + reply_handler=partial(self.__get_current_activity_cb, + buddy_handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity')) + + if not activity_id in self._buddies_per_activity: + self._buddies_per_activity[activity_id] = set() + self._buddies_per_activity[activity_id].add(buddy_handle) + if activity_id not in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].add(activity_id) + if self._buddy_handles[buddy_handle] is not None: + self.emit('buddy-joined-activity', + self._buddy_handles[buddy_handle], + activity_id) + + current_activity_ids = \ + [activity_id for activity_id, room_handle in activities] + for activity_id in self._activities_per_buddy[buddy_handle].copy(): + if not activity_id in current_activity_ids: + self._remove_buddy_from_activity(buddy_handle, activity_id) + + def __get_properties_cb(self, room_handle, properties): + #print('_Account.__get_properties_cb %r %r', room_handle, + # properties) + if properties: + self._update_activity(room_handle, properties) + + def _remove_buddy_from_activity(self, buddy_handle, activity_id): + if buddy_handle in self._buddies_per_activity[activity_id]: + self._buddies_per_activity[activity_id].remove(buddy_handle) + + if activity_id in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].remove(activity_id) + + if self._buddy_handles[buddy_handle] is not None: + self.emit('buddy-left-activity', + self._buddy_handles[buddy_handle], + activity_id) + + if not self._buddies_per_activity[activity_id]: + del self._buddies_per_activity[activity_id] + + for room_handle in self._activity_handles.copy(): + if self._activity_handles[room_handle] == activity_id: + del self._activity_handles[room_handle] + break + + self.emit('activity-removed', activity_id) + + def __activity_properties_changed_cb(self, room_handle, properties): + #print('_Account.__activity_properties_changed_cb %r %r', + # room_handle, properties) + self._update_activity(room_handle, properties) + + def _update_activity(self, room_handle, properties): + if room_handle in self._activity_handles: + self.emit('activity-updated', self._activity_handles[room_handle], + properties) + else: + #print('_Account.__activity_properties_changed_cb unknown ' + # 'activity') + # We don't get ActivitiesChanged for the owner of the connection, + # so we query for its activities in order to find out. + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + handle = self._self_handle + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.Getactivities')) + + def __members_changed_cb(self, message, added, removed, local_pending, + remote_pending, actor, reason): + self._add_buddy_handles(added) + + def __get_members_ready_cb(self, handles): + #print('_Account.__get_members_ready_cb %r', handles) + if not handles: + return + + self._add_buddy_handles(handles) + + def _add_buddy_handles(self, handles): + #print('_Account._add_buddy_handles %r', handles) + interfaces = [CONNECTION, CONNECTION_INTERFACE_ALIASING] + self._connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes( + handles, interfaces, False, + reply_handler=self.__get_contact_attributes_cb, + error_handler=partial(self.__error_handler_cb, + 'Contacts.GetContactAttributes')) + + def __got_buddy_info_cb(self, handle, nick, properties): + #print('_Account.__got_buddy_info_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __get_contact_attributes_cb(self, attributes): + #print('_Account.__get_contact_attributes_cb %r', + # attributes.keys()) + + for handle in attributes.keys(): + nick = attributes[handle][CONNECTION_INTERFACE_ALIASING + '/alias'] + + if handle in self._buddy_handles and \ + not self._buddy_handles[handle] is None: + #print('Got handle %r with nick %r, going to update', + # handle, nick) + self.emit('buddy-updated', self._buddy_handles[handle], + attributes[handle]) + else: + #print('Got handle %r with nick %r, going to add', + # handle, nick) + + contact_id = attributes[handle][CONNECTION + '/contact-id'] + self._buddy_handles[handle] = contact_id + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = \ + self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + + connection.GetProperties( + handle, + reply_handler=partial(self.__got_buddy_info_cb, handle, + nick), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetProperties'), + byte_arrays=True, + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetActivities'), + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetCurrentActivity( + handle, + reply_handler=partial(self.__get_current_activity_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity'), + timeout=_QUERY_DBUS_TIMEOUT) + + self.emit('buddy-added', contact_id, nick, handle) + + def __got_activities_cb(self, buddy_handle, activities): + #print('_Account.__got_activities_cb %r %r', buddy_handle, + # activities) + self._update_buddy_activities(buddy_handle, activities) + + def enable(self): + #print('_Account.enable %s', self.object_path) + self._set_enabled(True) + + def disable(self): + #print('_Account.disable %s', self.object_path) + self._set_enabled(False) + self._connection = None + + def _set_enabled(self, value): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Set(ACCOUNT, 'Enabled', value, + reply_handler=self.__set_enabled_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.SetEnabled'), + dbus_interface='org.freedesktop.DBus.Properties') + + def __set_enabled_cb(self): + #print('_Account.__set_enabled_cb success') + pass + + + +class Neighborhood(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self, params = {}): + gobject.GObject.__init__(self) + + self._buddies = {None: get_owner_instance()} + self._activities = {} + self._server_account = None + self._nicks = {} + + # + # Jabber params + # + self._nickname = params["nickname"] + self._account_id = params["account_id"] + self._server = params["server"] + self._port = params["port"] + self._password = params["password"] + self._register = params["register"] + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts', + dbus_interface=PROPERTIES_IFACE, + reply_handler=self.__got_accounts_cb, + error_handler=self.__error_handler_cb) + + def show_buddies(self): + print "\n\nBuddy list\n\n" + for k in self._nicks.keys(): + try: + print "%s = %s" % (k, self._nicks[k]) + except: + pass + + print "\n\nActivities list\n\n" + for k in self._activities.keys(): + try: + print "%s" % k + except: + pass + + def __got_accounts_cb(self, account_paths): + self._server_account = self._ensure_server_account(account_paths) + self._connect_to_account(self._server_account) + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + + def _connect_to_account(self, account): + account.connect('buddy-added', self.__buddy_added_cb) + account.connect('buddy-updated', self.__buddy_updated_cb) + account.connect('buddy-removed', self.__buddy_removed_cb) + account.connect('buddy-joined-activity', + self.__buddy_joined_activity_cb) + account.connect('buddy-left-activity', self.__buddy_left_activity_cb) + account.connect('activity-added', self.__activity_added_cb) + account.connect('activity-updated', self.__activity_updated_cb) + account.connect('activity-removed', self.__activity_removed_cb) + account.connect('current-activity-updated', + self.__current_activity_updated_cb) + account.connect('connected', self.__account_connected_cb) + account.connect('disconnected', self.__account_disconnected_cb) + + def __account_connected_cb(self, account): + #print('__account_connected_cb %s', account.object_path) + if account == self._server_account: + #self._link_local_account.disable() + pass + + def __account_disconnected_cb(self, account): + #print('__account_disconnected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.enable() + + def _ensure_link_local_account(self, account_paths): + for account_path in account_paths: + if 'salut' in account_path: + #print('Already have a Salut account') + account = _Account(account_path) + account.enable() + return account + + #print('Still dont have a Salut account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + + params = { + 'nickname': nick, + 'first-name': '', + 'last-name': '', + 'jid': self._get_jabber_account_id(), + 'published-name': nick, + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('salut', 'local-xmpp', + 'salut', params, + properties) + return _Account(account_path) + + def _ensure_server_account(self, account_paths): + bus = dbus.Bus() + + for account_path in account_paths: + if 'gabble' in account_path: + obj_acct_mgr = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path) + properties = obj_acct_mgr.Get(ACCOUNT, 'Parameters') + if properties.has_key("server") and properties["server"] == self._server: + print("Enabiling account_path = %s, server = %s", account_path, self._server) + account = _Account(account_path) + account.enable() + return account + + params = { + 'account': self._get_jabber_account_id(), + 'password': self._password, + 'server': self._server, + 'resource': 'sugar', + 'require-encryption': True, + 'ignore-ssl-errors': True, + 'register': self._register, + 'old-ssl': True, + 'port': dbus.UInt32(self._port), + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': self._nickname, + ACCOUNT + '.ConnectAutomatically': True, + } + + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('gabble', 'jabber', + 'jabber', params, + properties) + return _Account(account_path) + + def _get_jabber_account_id(self): + return self._account_id + + def __jabber_server_changed_cb(self, client, timestamp, entry, *extra): + #print('__jabber_server_changed_cb') + + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + + server = client.get_string( + '/desktop/sugar/collaboration/jabber_server') + account_id = self._get_jabber_account_id() + needs_reconnect = account.UpdateParameters({'server': server, + 'account': account_id, + 'register': True}, + dbus.Array([], 's'), + dbus_interface=ACCOUNT) + if needs_reconnect: + account.Reconnect() + + self._update_jid() + + def __nick_changed_cb(self, client, timestamp, entry, *extra): + nick = client.get_string('/desktop/sugar/user/nick') + for account in self._server_account, self._link_local_account: + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account.object_path) + obj.Set(ACCOUNT, 'Nickname', nick, dbus_interface=PROPERTIES_IFACE) + + self._update_jid() + + def _update_jid(self): + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + + account_id = self._get_jabber_account_id() + needs_reconnect = account.UpdateParameters({'jid': account_id}, + dbus.Array([], 's'), + dbus_interface=ACCOUNT) + if needs_reconnect: + account.Reconnect() + + def __buddy_added_cb(self, account, contact_id, nick, handle): + + self._nicks[contact_id] = nick + if contact_id in self._buddies: + #print('__buddy_added_cb buddy already tracked') + return + + buddy = BuddyModel( + nick=nick, + account=account.object_path, + contact_id=contact_id, + handle=handle) + self._buddies[contact_id] = buddy + + def __buddy_updated_cb(self, account, contact_id, properties): + #print('__buddy_updated_cb %r', contact_id) + if contact_id is None: + # Don't know the contact-id yet, will get the full state later + return + + if contact_id not in self._buddies: + #print('__buddy_updated_cb Unknown buddy with contact_id' + # ' %r', contact_id) + return + + buddy = self._buddies[contact_id] + + is_new = buddy.props.key is None and 'key' in properties + + if 'color' in properties: + buddy.props.color = XoColor(properties['color']) + + if 'key' in properties: + buddy.props.key = properties['key'] + + if 'nick' in properties: + buddy.props.nick = properties['nick'] + + if is_new: + self.emit('buddy-added', buddy) + + def __buddy_removed_cb(self, account, contact_id): + #print('Neighborhood.__buddy_removed_cb %r', contact_id) + try: + self._nicks.pop(contact_id) + except: + pass + if contact_id not in self._buddies: + #print('Neighborhood.__buddy_removed_cb Unknown buddy with ' + # 'contact_id %r', contact_id) + return + + buddy = self._buddies[contact_id] + del self._buddies[contact_id] + + if buddy.props.key is not None: + self.emit('buddy-removed', buddy) + + def __activity_added_cb(self, account, room_handle, activity_id): + #print('__activity_added_cb %r %r', room_handle, activity_id) + if activity_id in self._activities: + #print('__activity_added_cb activity already tracked') + return + + activity = ActivityModel(activity_id, room_handle) + self._activities[activity_id] = activity + + def __activity_updated_cb(self, account, activity_id, properties): + print('__activity_updated_cb %r %r', activity_id, properties) + if activity_id not in self._activities: + print('__activity_updated_cb Unknown activity with activity_id %r', activity_id) + return + + ### + # FIXME: this should be configurable, we only care about the activity thats using this lib + # i.e.: Turtle Art + + if properties['type']: + print properties['type'] + + # we should somehow emulate this and say we only have TurtleArtActivity + #registry = bundleregistry.get_registry() + #bundle = registry.get_bundle(properties['type']) + #bundle = None + #if not bundle: + # print('Ignoring shared activity we don''t have') + # return + + activity = self._activities[activity_id] + + is_new = activity.props.bundle is None + + if 'color' in properties: + activity.props.color = XoColor(properties['color']) + activity.props.bundle = None # FIXME: we have no access to the bundleregistry + if 'name' in properties: + activity.props.name = properties['name'] + if 'private' in properties: + activity.props.private = properties['private'] + + if is_new: + print "The activity is new" + self.emit('activity-added', activity) + else: + print "The activity is *NOT* new" + + def __activity_removed_cb(self, account, activity_id): + if activity_id not in self._activities: + print('Unknown activity with id %s. Already removed?', activity_id) + return + activity = self._activities[activity_id] + del self._activities[activity_id] + + self.emit('activity-removed', activity) + + def __current_activity_updated_cb(self, account, contact_id, activity_id): + #print('__current_activity_updated_cb %r %r', contact_id, + # activity_id) + if contact_id not in self._buddies: + #print('__current_activity_updated_cb Unknown buddy with ' + # 'contact_id %r', contact_id) + return + if activity_id and activity_id not in self._activities: + #print('__current_activity_updated_cb Unknown activity with' + # ' id %s', activity_id) + activity_id = '' + + buddy = self._buddies[contact_id] + if buddy.props.current_activity is not None: + if buddy.props.current_activity.activity_id == activity_id: + return + buddy.props.current_activity.remove_current_buddy(buddy) + + if activity_id: + activity = self._activities[activity_id] + buddy.props.current_activity = activity + activity.add_current_buddy(buddy) + else: + buddy.props.current_activity = None + + def __buddy_joined_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + #print('__buddy_joined_activity_cb Unknown buddy with ' + # 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + #print('__buddy_joined_activity_cb Unknown activity with ' + # 'activity_id %r', activity_id) + return + + self._activities[activity_id].add_buddy(self._buddies[contact_id]) + + def __buddy_left_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + #print('__buddy_left_activity_cb Unknown buddy with ' + # 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + #print('__buddy_left_activity_cb Unknown activity with ' + # 'activity_id %r', activity_id) + return + + self._activities[activity_id].remove_buddy(self._buddies[contact_id]) + + def get_buddies(self): + return self._buddies.values() + + def get_buddy_by_key(self, key): + for buddy in self._buddies.values(): + if buddy.key == key: + return buddy + return None + + def get_buddy_by_handle(self, contact_handle): + for buddy in self._buddies.values(): + if not buddy.is_owner() and buddy.handle == contact_handle: + return buddy + return None + + def get_activity(self, activity_id): + return self._activities.get(activity_id, None) + + def get_activity_by_room(self, room_handle): + for activity in self._activities.values(): + if activity.room_handle == room_handle: + return activity + return None + + def get_activities(self): + return self._activities.values() + +_neighborhood = None +def get_neighborhood(params = {}): + global _neighborhood + if _neighborhood is None: + _neighborhood = Neighborhood(params) + return _neighborhood + +if __name__ == "__main__": + params = {} + params["nickname"] = "test" + params["account_id"] = "test" + params["server"] = "localhost" + params["port"] = 5223 + params["password"] = "test" + params["register"] = True + + loop = gobject.MainLoop() + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + n = get_neighborhood(params) + loop.run() diff --git a/collaboration/presenceservice.py b/collaboration/presenceservice.py new file mode 100644 index 0000000..b6c581e --- /dev/null +++ b/collaboration/presenceservice.py @@ -0,0 +1,266 @@ +# 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. + +""" +STABLE. +""" + +import logging + +import gobject +import dbus +import dbus.exceptions +import dbus.glib +from dbus import PROPERTIES_IFACE + +from sugar.presence.buddy import Buddy, Owner +from sugar.presence.activity import Activity +from sugar.presence.connectionmanager import get_connection_manager + +from telepathy.interfaces import ACCOUNT, \ + ACCOUNT_MANAGER, \ + CONNECTION +from telepathy.constants import HANDLE_TYPE_CONTACT + + +_logger = logging.getLogger('sugar.presence.presenceservice') + +ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' + +CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' + + +class PresenceService(gobject.GObject): + """Provides simplified access to the Telepathy framework to activities""" + __gsignals__ = { + 'activity-shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])), + } + + def __init__(self): + """Initialise the service and attempt to connect to events + """ + gobject.GObject.__init__(self) + + self._activity_cache = None + self._buddy_cache = {} + + def get_activity(self, activity_id, warn_if_none=True): + """Retrieve single Activity object for the given unique id + + activity_id -- unique ID for the activity + + returns single Activity object or None if the activity + is not found using GetActivityById on the service + """ + if self._activity_cache is not None: + if self._activity_cache.props.id != activity_id: + raise RuntimeError('Activities can only access their own' + ' shared instance') + return self._activity_cache + else: + connection_manager = get_connection_manager() + connections_per_account = \ + connection_manager.get_connections_per_account() + for account_path, connection in connections_per_account.items(): + if not connection.connected: + continue + logging.debug('Calling GetActivity on %s', account_path) + try: + room_handle = connection.connection.GetActivity( + activity_id, + dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) + except dbus.exceptions.DBusException, e: + name = 'org.freedesktop.Telepathy.Error.NotAvailable' + if e.get_dbus_name() == name: + logging.debug("There's no shared activity with the id " + "%s", activity_id) + else: + raise + else: + activity = Activity(account_path, connection.connection, + room_handle=room_handle) + self._activity_cache = activity + return activity + + return None + + def get_activity_by_handle(self, connection_path, room_handle): + if self._activity_cache is not None: + if self._activity_cache.room_handle != room_handle: + raise RuntimeError('Activities can only access their own' + ' shared instance') + return self._activity_cache + else: + connection_manager = get_connection_manager() + account_path = \ + connection_manager.get_account_for_connection(connection_path) + + connection_name = connection_path.replace('/', '.')[1:] + bus = dbus.SessionBus() + connection = bus.get_object(connection_name, connection_path) + activity = Activity(account_path, connection, + room_handle=room_handle) + self._activity_cache = activity + return activity + + def get_buddy(self, account_path, contact_id): + if (account_path, contact_id) in self._buddy_cache: + return self._buddy_cache[(account_path, contact_id)] + + buddy = Buddy(account_path, contact_id) + self._buddy_cache[(account_path, contact_id)] = buddy + return buddy + + # DEPRECATED + def get_buddy_by_telepathy_handle(self, tp_conn_name, tp_conn_path, + handle): + """Retrieve single Buddy object for the given public key + + :Parameters: + `tp_conn_name` : str + The well-known bus name of a Telepathy connection + `tp_conn_path` : dbus.ObjectPath + The object path of the Telepathy connection + `handle` : int or long + The handle of a Telepathy contact on that connection, + of type HANDLE_TYPE_CONTACT. This may not be a + channel-specific handle. + :Returns: the Buddy object, or None if the buddy is not found + """ + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_paths = account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts', + dbus_interface=PROPERTIES_IFACE) + for account_path in account_paths: + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path) + connection_path = obj.Get(ACCOUNT, 'Connection') + if connection_path == tp_conn_path: + connection_name = connection_path.replace('/', '.')[1:] + connection = bus.get_object(connection_name, connection_path) + contact_ids = connection.InspectHandles(HANDLE_TYPE_CONTACT, + [handle], + dbus_interface=CONNECTION) + return self.get_buddy(account_path, contact_ids[0]) + + raise ValueError('Unknown buddy in connection %s with handle %d', + tp_conn_path, handle) + + def get_owner(self): + """Retrieves the laptop Buddy object.""" + return Owner() + + def __share_activity_cb(self, activity): + """Finish sharing the activity + """ + self.emit('activity-shared', True, activity, None) + + def __share_activity_error_cb(self, activity, error): + """Notify with GObject event of unsuccessful sharing of activity + """ + self.emit('activity-shared', False, activity, error) + + def share_activity(self, activity, properties=None, private=True): + if properties is None: + properties = {} + + if 'id' not in properties: + properties['id'] = activity.get_id() + + if 'type' not in properties: + properties['type'] = activity.get_bundle_id() + + if 'name' not in properties: + properties['name'] = activity.metadata.get('title', None) + + if 'color' not in properties: + properties['color'] = activity.metadata.get('icon-color', None) + + properties['private'] = private + + if self._activity_cache is not None: + raise ValueError('Activity %s is already tracked', + activity.get_id()) + + connection_manager = get_connection_manager() + account_path, connection = \ + connection_manager.get_preferred_connection() + + if connection is None: + self.emit('activity-shared', False, None, + 'No active connection available') + return + + shared_activity = Activity(account_path, connection, + properties=properties) + self._activity_cache = shared_activity + + if shared_activity.props.joined: + raise RuntimeError('Activity %s is already shared.' % + activity.props.id) + + shared_activity.share(self.__share_activity_cb, + self.__share_activity_error_cb) + + def get_preferred_connection(self): + """Gets the preferred telepathy connection object that an activity + should use when talking directly to telepathy + + returns the bus name and the object path of the Telepathy connection + """ + manager = get_connection_manager() + account_path, connection = manager.get_preferred_connection() + if connection is None: + return None + else: + return connection.requested_bus_name, connection.object_path + + # DEPRECATED + def get(self, object_path): + raise NotImplementedError() + + # DEPRECATED + def get_activities(self): + raise NotImplementedError() + + # DEPRECATED + def get_activities_async(self, reply_handler=None, error_handler=None): + raise NotImplementedError() + + # DEPRECATED + def get_buddies(self): + raise NotImplementedError() + + # DEPRECATED + def get_buddies_async(self, reply_handler=None, error_handler=None): + raise NotImplementedError() + + +_ps = None + + +def get_instance(allow_offline_iface=False): + """Retrieve this process' view of the PresenceService""" + global _ps + if not _ps: + _ps = PresenceService() + return _ps diff --git a/collaboration/telepathyclient.py b/collaboration/telepathyclient.py new file mode 100644 index 0000000..5491530 --- /dev/null +++ b/collaboration/telepathyclient.py @@ -0,0 +1,103 @@ +# Copyright (C) 2010 Collabora Ltd. +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import CLIENT, \ + CLIENT_APPROVER, \ + CLIENT_HANDLER, \ + CLIENT_INTERFACE_REQUESTS +from telepathy.server import DBusProperties + +import dispatch + + +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +_instance = None + + +class TelepathyClient(dbus.service.Object, DBusProperties): + def __init__(self): + self._interfaces = set([CLIENT, CLIENT_HANDLER, + CLIENT_INTERFACE_REQUESTS, PROPERTIES_IFACE, + CLIENT_APPROVER]) + + bus = dbus.Bus() + bus_name = dbus.service.BusName(SUGAR_CLIENT_SERVICE, bus=bus) + + dbus.service.Object.__init__(self, bus_name, SUGAR_CLIENT_PATH) + DBusProperties.__init__(self) + + self._implement_property_get(CLIENT, { + 'Interfaces': lambda: list(self._interfaces), + }) + self._implement_property_get(CLIENT_HANDLER, { + 'HandlerChannelFilter': self.__get_filters_cb, + }) + self._implement_property_get(CLIENT_APPROVER, { + 'ApproverChannelFilter': self.__get_filters_cb, + }) + + self.got_channel = dispatch.Signal() + self.got_dispatch_operation = dispatch.Signal() + + def __get_filters_cb(self): + logging.debug('__get_filters_cb') + filter_dict = dbus.Dictionary({}, signature='sv') + return dbus.Array([filter_dict], signature='a{sv}') + + @dbus.service.method(dbus_interface=CLIENT_HANDLER, + in_signature='ooa(oa{sv})aota{sv}', out_signature='') + def HandleChannels(self, account, connection, channels, requests_satisfied, + user_action_time, handler_info): + logging.debug('HandleChannels\n%r\n%r\n%r\n%r\n%r\n%r\n', account, + connection, channels, requests_satisfied, + user_action_time, handler_info) + for channel in channels: + self.got_channel.send(self, account=account, + connection=connection, channel=channel) + + @dbus.service.method(dbus_interface=CLIENT_INTERFACE_REQUESTS, + in_signature='oa{sv}', out_signature='') + def AddRequest(self, request, properties): + logging.debug('AddRequest\n%r\n%r', request, properties) + + @dbus.service.method(dbus_interface=CLIENT_APPROVER, + in_signature='a(oa{sv})oa{sv}', out_signature='', + async_callbacks=('success_cb', 'error_cb_')) + def AddDispatchOperation(self, channels, dispatch_operation_path, + properties, success_cb, error_cb_): + success_cb() + try: + logging.debug('AddDispatchOperation\n%r\n%r\n%r', channels, + dispatch_operation_path, properties) + + self.got_dispatch_operation.send(self, channels=channels, + dispatch_operation_path=dispatch_operation_path, + properties=properties) + except Exception, e: + logging.exception(e) + + +def get_instance(): + global _instance + if not _instance: + _instance = TelepathyClient() + return _instance diff --git a/collaboration/test.py b/collaboration/test.py new file mode 100755 index 0000000..db7be0d --- /dev/null +++ b/collaboration/test.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +import gobject +import dbus +import dbus.mainloop +import dbus.mainloop.glib +from connectionmanager import get_connection_manager + +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + +conn_manager = get_connection_manager() +account_path, connection = conn_manager.get_preferred_connection() +print account_path diff --git a/collaboration/tubeconn.py b/collaboration/tubeconn.py new file mode 100644 index 0000000..1014a46 --- /dev/null +++ b/collaboration/tubeconn.py @@ -0,0 +1,114 @@ +# This should eventually land in telepathy-python, so has the same license: + +# Copyright (C) 2007 Collabora Ltd. +# +# This program 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.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser 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 + +""" +STABLE. +""" + +__all__ = ('TubeConnection', ) +__docformat__ = 'reStructuredText' + + +import logging + +from dbus.connection import Connection + + +logger = logging.getLogger('telepathy.tubeconn') + + +class TubeConnection(Connection): + + def __new__(cls, conn, tubes_iface, tube_id, address=None, + group_iface=None, mainloop=None): + # pylint: disable=W0212 + # Confused by __new__ + if address is None: + address = tubes_iface.GetDBusTubeAddress(tube_id) + self = super(TubeConnection, cls).__new__(cls, address, + mainloop=mainloop) + + self._tubes_iface = tubes_iface + self.tube_id = tube_id + self.participants = {} + self.bus_name_to_handle = {} + self._mapping_watches = [] + + if group_iface is None: + method = conn.GetSelfHandle + else: + method = group_iface.GetSelfHandle + method(reply_handler=self._on_get_self_handle_reply, + error_handler=self._on_get_self_handle_error) + + return self + + def _on_get_self_handle_reply(self, handle): + # pylint: disable=W0201 + # Confused by __new__ + self.self_handle = handle + match = self._tubes_iface.connect_to_signal('DBusNamesChanged', + self._on_dbus_names_changed) + self._tubes_iface.GetDBusNames(self.tube_id, + reply_handler=self._on_get_dbus_names_reply, + error_handler=self._on_get_dbus_names_error) + self._dbus_names_changed_match = match + + def _on_get_self_handle_error(self, e): + logging.basicConfig() + logger.error('GetSelfHandle failed: %s', e) + + def close(self): + self._dbus_names_changed_match.remove() + self._on_dbus_names_changed(self.tube_id, (), self.participants.keys()) + super(TubeConnection, self).close() + + def _on_get_dbus_names_reply(self, names): + self._on_dbus_names_changed(self.tube_id, names, ()) + + def _on_get_dbus_names_error(self, e): + logging.basicConfig() + logger.error('GetDBusNames failed: %s', e) + + def _on_dbus_names_changed(self, tube_id, added, removed): + if tube_id == self.tube_id: + for handle, bus_name in added: + if handle == self.self_handle: + # I've just joined - set my unique name + self.set_unique_name(bus_name) + self.participants[handle] = bus_name + self.bus_name_to_handle[bus_name] = handle + + # call the callback while the removed people are still in + # participants, so their bus names are available + for callback in self._mapping_watches: + callback(added, removed) + + for handle in removed: + bus_name = self.participants.pop(handle, None) + self.bus_name_to_handle.pop(bus_name, None) + + def watch_participants(self, callback): + self._mapping_watches.append(callback) + if self.participants: + # GetDBusNames already returned: fake a participant add event + # immediately + added = [] + for k, v in self.participants.iteritems(): + added.append((k, v)) + callback(added, []) diff --git a/collaboration/xocolor.py b/collaboration/xocolor.py new file mode 100644 index 0000000..395e345 --- /dev/null +++ b/collaboration/xocolor.py @@ -0,0 +1,282 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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. + +""" +STABLE. +""" + +import random +import logging + +import gconf + +colors = [ +['#B20008', '#FF2B34'], \ +['#FF2B34', '#B20008'], \ +['#E6000A', '#FF2B34'], \ +['#FF2B34', '#E6000A'], \ +['#FFADCE', '#FF2B34'], \ +['#9A5200', '#FF2B34'], \ +['#FF2B34', '#9A5200'], \ +['#FF8F00', '#FF2B34'], \ +['#FF2B34', '#FF8F00'], \ +['#FFC169', '#FF2B34'], \ +['#807500', '#FF2B34'], \ +['#FF2B34', '#807500'], \ +['#BE9E00', '#FF2B34'], \ +['#FF2B34', '#BE9E00'], \ +['#F8E800', '#FF2B34'], \ +['#008009', '#FF2B34'], \ +['#FF2B34', '#008009'], \ +['#00B20D', '#FF2B34'], \ +['#FF2B34', '#00B20D'], \ +['#8BFF7A', '#FF2B34'], \ +['#00588C', '#FF2B34'], \ +['#FF2B34', '#00588C'], \ +['#005FE4', '#FF2B34'], \ +['#FF2B34', '#005FE4'], \ +['#BCCDFF', '#FF2B34'], \ +['#5E008C', '#FF2B34'], \ +['#FF2B34', '#5E008C'], \ +['#7F00BF', '#FF2B34'], \ +['#FF2B34', '#7F00BF'], \ +['#D1A3FF', '#FF2B34'], \ +['#9A5200', '#FF8F00'], \ +['#FF8F00', '#9A5200'], \ +['#C97E00', '#FF8F00'], \ +['#FF8F00', '#C97E00'], \ +['#FFC169', '#FF8F00'], \ +['#807500', '#FF8F00'], \ +['#FF8F00', '#807500'], \ +['#BE9E00', '#FF8F00'], \ +['#FF8F00', '#BE9E00'], \ +['#F8E800', '#FF8F00'], \ +['#008009', '#FF8F00'], \ +['#FF8F00', '#008009'], \ +['#00B20D', '#FF8F00'], \ +['#FF8F00', '#00B20D'], \ +['#8BFF7A', '#FF8F00'], \ +['#00588C', '#FF8F00'], \ +['#FF8F00', '#00588C'], \ +['#005FE4', '#FF8F00'], \ +['#FF8F00', '#005FE4'], \ +['#BCCDFF', '#FF8F00'], \ +['#5E008C', '#FF8F00'], \ +['#FF8F00', '#5E008C'], \ +['#A700FF', '#FF8F00'], \ +['#FF8F00', '#A700FF'], \ +['#D1A3FF', '#FF8F00'], \ +['#B20008', '#FF8F00'], \ +['#FF8F00', '#B20008'], \ +['#FF2B34', '#FF8F00'], \ +['#FF8F00', '#FF2B34'], \ +['#FFADCE', '#FF8F00'], \ +['#807500', '#F8E800'], \ +['#F8E800', '#807500'], \ +['#BE9E00', '#F8E800'], \ +['#F8E800', '#BE9E00'], \ +['#FFFA00', '#EDDE00'], \ +['#008009', '#F8E800'], \ +['#F8E800', '#008009'], \ +['#00EA11', '#F8E800'], \ +['#F8E800', '#00EA11'], \ +['#8BFF7A', '#F8E800'], \ +['#00588C', '#F8E800'], \ +['#F8E800', '#00588C'], \ +['#00A0FF', '#F8E800'], \ +['#F8E800', '#00A0FF'], \ +['#BCCEFF', '#F8E800'], \ +['#5E008C', '#F8E800'], \ +['#F8E800', '#5E008C'], \ +['#AC32FF', '#F8E800'], \ +['#F8E800', '#AC32FF'], \ +['#D1A3FF', '#F8E800'], \ +['#B20008', '#F8E800'], \ +['#F8E800', '#B20008'], \ +['#FF2B34', '#F8E800'], \ +['#F8E800', '#FF2B34'], \ +['#FFADCE', '#F8E800'], \ +['#9A5200', '#F8E800'], \ +['#F8E800', '#9A5200'], \ +['#FF8F00', '#F8E800'], \ +['#F8E800', '#FF8F00'], \ +['#FFC169', '#F8E800'], \ +['#008009', '#00EA11'], \ +['#00EA11', '#008009'], \ +['#00B20D', '#00EA11'], \ +['#00EA11', '#00B20D'], \ +['#8BFF7A', '#00EA11'], \ +['#00588C', '#00EA11'], \ +['#00EA11', '#00588C'], \ +['#005FE4', '#00EA11'], \ +['#00EA11', '#005FE4'], \ +['#BCCDFF', '#00EA11'], \ +['#5E008C', '#00EA11'], \ +['#00EA11', '#5E008C'], \ +['#7F00BF', '#00EA11'], \ +['#00EA11', '#7F00BF'], \ +['#D1A3FF', '#00EA11'], \ +['#B20008', '#00EA11'], \ +['#00EA11', '#B20008'], \ +['#FF2B34', '#00EA11'], \ +['#00EA11', '#FF2B34'], \ +['#FFADCE', '#00EA11'], \ +['#9A5200', '#00EA11'], \ +['#00EA11', '#9A5200'], \ +['#FF8F00', '#00EA11'], \ +['#00EA11', '#FF8F00'], \ +['#FFC169', '#00EA11'], \ +['#807500', '#00EA11'], \ +['#00EA11', '#807500'], \ +['#BE9E00', '#00EA11'], \ +['#00EA11', '#BE9E00'], \ +['#F8E800', '#00EA11'], \ +['#00588C', '#00A0FF'], \ +['#00A0FF', '#00588C'], \ +['#005FE4', '#00A0FF'], \ +['#00A0FF', '#005FE4'], \ +['#BCCDFF', '#00A0FF'], \ +['#5E008C', '#00A0FF'], \ +['#00A0FF', '#5E008C'], \ +['#9900E6', '#00A0FF'], \ +['#00A0FF', '#9900E6'], \ +['#D1A3FF', '#00A0FF'], \ +['#B20008', '#00A0FF'], \ +['#00A0FF', '#B20008'], \ +['#FF2B34', '#00A0FF'], \ +['#00A0FF', '#FF2B34'], \ +['#FFADCE', '#00A0FF'], \ +['#9A5200', '#00A0FF'], \ +['#00A0FF', '#9A5200'], \ +['#FF8F00', '#00A0FF'], \ +['#00A0FF', '#FF8F00'], \ +['#FFC169', '#00A0FF'], \ +['#807500', '#00A0FF'], \ +['#00A0FF', '#807500'], \ +['#BE9E00', '#00A0FF'], \ +['#00A0FF', '#BE9E00'], \ +['#F8E800', '#00A0FF'], \ +['#008009', '#00A0FF'], \ +['#00A0FF', '#008009'], \ +['#00B20D', '#00A0FF'], \ +['#00A0FF', '#00B20D'], \ +['#8BFF7A', '#00A0FF'], \ +['#5E008C', '#AC32FF'], \ +['#AC32FF', '#5E008C'], \ +['#7F00BF', '#AC32FF'], \ +['#AC32FF', '#7F00BF'], \ +['#D1A3FF', '#AC32FF'], \ +['#B20008', '#AC32FF'], \ +['#AC32FF', '#B20008'], \ +['#FF2B34', '#AC32FF'], \ +['#AC32FF', '#FF2B34'], \ +['#FFADCE', '#AC32FF'], \ +['#9A5200', '#AC32FF'], \ +['#AC32FF', '#9A5200'], \ +['#FF8F00', '#AC32FF'], \ +['#AC32FF', '#FF8F00'], \ +['#FFC169', '#AC32FF'], \ +['#807500', '#AC32FF'], \ +['#AC32FF', '#807500'], \ +['#BE9E00', '#AC32FF'], \ +['#AC32FF', '#BE9E00'], \ +['#F8E800', '#AC32FF'], \ +['#008009', '#AC32FF'], \ +['#AC32FF', '#008009'], \ +['#00B20D', '#AC32FF'], \ +['#AC32FF', '#00B20D'], \ +['#8BFF7A', '#AC32FF'], \ +['#00588C', '#AC32FF'], \ +['#AC32FF', '#00588C'], \ +['#005FE4', '#AC32FF'], \ +['#AC32FF', '#005FE4'], \ +['#BCCDFF', '#AC32FF'], \ +] + + +def _parse_string(color_string): + if not isinstance(color_string, (str, unicode)): + logging.error('Invalid color string: %r', color_string) + return None + + if color_string == 'white': + return ['#ffffff', '#414141'] + elif color_string == 'insensitive': + return ['#ffffff', '#e2e2e2'] + + splitted = color_string.split(',') + if len(splitted) == 2: + return [splitted[0], splitted[1]] + else: + return None + + +def is_valid(color_string): + return (_parse_string(color_string) != None) + + +class XoColor: + + def __init__(self, color_string=None): + if color_string == None: + randomize = True + elif not is_valid(color_string): + logging.debug('Color string is not valid: %s, ' + 'fallback to default', color_string) + client = gconf.client_get_default() + color_string = client.get_string('/desktop/sugar/user/color') + randomize = False + else: + randomize = False + + if randomize: + n = int(random.random() * (len(colors) - 1)) + [self.stroke, self.fill] = colors[n] + else: + [self.stroke, self.fill] = _parse_string(color_string) + + def __cmp__(self, other): + if isinstance(other, XoColor): + if self.stroke == other.stroke and self.fill == other.fill: + return 0 + return -1 + + def get_stroke_color(self): + return self.stroke + + def get_fill_color(self): + return self.fill + + def to_string(self): + return '%s,%s' % (self.stroke, self.fill) + + +if __name__ == '__main__': + import sys + import re + + f = open(sys.argv[1], 'r') + + print 'colors = [' + + for line in f.readlines(): + match = re.match(r'fill: ([A-Z0-9]*) stroke: ([A-Z0-9]*)', line) + print "['#%s', '#%s'], \\" % (match.group(2), match.group(1)) + + print ']' + + f.close() -- cgit v0.9.1