From eaef567cabc5c0dda9706faa0e17e20909131ade Mon Sep 17 00:00:00 2001 From: simpoir Date: Fri, 30 Jan 2009 04:14:48 +0000 Subject: ajout initial du tree jhbuild [jhbuild base] --- (limited to 'src/sugar/presence') diff --git a/src/sugar/presence/Makefile.am b/src/sugar/presence/Makefile.am new file mode 100644 index 0000000..0c4368b --- /dev/null +++ b/src/sugar/presence/Makefile.am @@ -0,0 +1,9 @@ +sugardir = $(pythondir)/sugar/presence +sugar_PYTHON = \ + __init__.py \ + activity.py \ + buddy.py \ + sugartubeconn.py \ + tubeconn.py \ + presenceservice.py + diff --git a/src/sugar/presence/__init__.py b/src/sugar/presence/__init__.py new file mode 100644 index 0000000..1136c19 --- /dev/null +++ b/src/sugar/presence/__init__.py @@ -0,0 +1,24 @@ +# 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. + +"""Client-code's interface to the PresenceService + +Provides a simplified API for accessing the dbus service +which coordinates native network presence and sharing +information. This includes both "buddies" and "shared +activities". +""" diff --git a/src/sugar/presence/activity.py b/src/sugar/presence/activity.py new file mode 100644 index 0000000..dc02aa1 --- /dev/null +++ b/src/sugar/presence/activity.py @@ -0,0 +1,410 @@ +# Copyright (C) 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. + +"""UI interface to an activity in the presence service + +STABLE. +""" + +import logging + +import dbus +import gobject +import telepathy + +_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), + } + + _PRESENCE_SERVICE = "org.laptop.Sugar.Presence" + _ACTIVITY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Activity" + + def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): + """Initialse the activity interface, connecting to service""" + gobject.GObject.__init__(self) + self._telepathy_room_handle = None + self._object_path = object_path + self._ps_new_object = new_obj_cb + self._ps_del_object = del_obj_cb + bobj = bus.get_object(self._PRESENCE_SERVICE, object_path) + self._activity = dbus.Interface(bobj, self._ACTIVITY_DBUS_INTERFACE) + self._activity.connect_to_signal('BuddyHandleJoined', + self._buddy_handle_joined_cb) + self._activity.connect_to_signal('BuddyLeft', + self._buddy_left_cb) + self._activity.connect_to_signal('NewChannel', self._new_channel_cb) + self._activity.connect_to_signal('PropertiesChanged', + self._properties_changed_cb, + utf8_strings=True) + # FIXME: this *would* just use a normal proxy call, but I want the + # pending call object so I can block on it, and normal proxy methods + # don't return those as of dbus-python 0.82.1; so do it the hard way + self._get_properties_call = bus.call_async(self._PRESENCE_SERVICE, + object_path, self._ACTIVITY_DBUS_INTERFACE, 'GetProperties', + '', (), self._get_properties_reply_cb, + self._get_properties_error_cb, utf8_strings=True) + + self._id = None + self._color = None + self._name = None + self._type = None + self._tags = None + self._private = True + self._joined = False + # Cache for get_buddy_by_handle, maps handles to buddy object paths + self._handle_to_buddy_path = {} + self._buddy_path_to_handle = {} + + # Set up by set_up_tubes() + self.telepathy_conn = None + self.telepathy_tubes_chan = None + self.telepathy_text_chan = None + self._telepathy_room = None + + def __repr__(self): + return ('' % (self._object_path, id(self))) + + def _get_properties_reply_cb(self, new_props): + self._get_properties_call = None + _logger.debug('%r: initial GetProperties returned', self) + self._properties_changed_cb(new_props) + + def _get_properties_error_cb(self, e): + self._get_properties_call = None + # FIXME: do something with the error + _logger.warning('%r: Error doing initial GetProperties: %s', self, e) + + def _properties_changed_cb(self, new_props): + _logger.debug('%r: Activity properties changed to %r', 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: + _logger.debug('%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 + + # FIXME: need an asynchronous API to set these properties, particularly + # 'private' + def do_set_property(self, pspec, val): + """Set a particular property in our property dictionary""" + if pspec.name == "name": + self._activity.SetProperties({'name': val}) + self._name = val + elif pspec.name == "color": + self._activity.SetProperties({'color': val}) + self._color = val + elif pspec.name == "tags": + self._activity.SetProperties({'tags': val}) + self._tags = val + elif pspec.name == "private": + self._activity.SetProperties({'private': val}) + self._private = val + + def set_private(self, val, reply_handler, error_handler): + self._activity.SetProperties({'private': bool(val)}, + reply_handler=reply_handler, + error_handler=error_handler) + + def _emit_buddy_joined_signal(self, object_path): + """Generate buddy-joined GObject signal with presence Buddy object""" + self.emit('buddy-joined', self._ps_new_object(object_path)) + return False + + def _buddy_handle_joined_cb(self, object_path, handle): + _logger.debug('%r: buddy %s joined with handle %u', self, object_path, + handle) + gobject.idle_add(self._emit_buddy_joined_signal, object_path) + self._handle_to_buddy_path[handle] = object_path + self._buddy_path_to_handle[object_path] = handle + + def _emit_buddy_left_signal(self, object_path): + """Generate buddy-left GObject signal with presence Buddy object + + XXX note use of _ps_new_object instead of _ps_del_object here + """ + self.emit('buddy-left', self._ps_new_object(object_path)) + return False + + def _buddy_left_cb(self, object_path): + _logger.debug('%r: buddy %s left', self, object_path) + gobject.idle_add(self._emit_buddy_left_signal, object_path) + handle = self._buddy_path_to_handle.pop(object_path, None) + if handle: + self._handle_to_buddy_path.pop(handle, None) + + def _emit_new_channel_signal(self, object_path): + """Generate new-channel GObject signal with channel object path + + New telepathy-python communications channel has been opened + """ + self.emit('new-channel', object_path) + return False + + def _new_channel_cb(self, object_path): + _logger.debug('%r: new channel created at %s', self, object_path) + gobject.idle_add(self._emit_new_channel_signal, object_path) + + 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. + """ + resp = self._activity.GetJoinedBuddies() + buddies = [] + for item in resp: + try: + buddies.append(self._ps_new_object(item)) + except dbus.DBusException: + _logger.debug( + 'get_joined_buddies failed to get buddy object for %r', + item) + return buddies + + 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. + """ + op = buddy.object_path() + _logger.debug('%r: inviting %s', self, op) + self._activity.Invite(op, message, + reply_handler=lambda: response_cb(None), + error_handler=response_cb) + + # Joining and sharing (FIXME: sharing is actually done elsewhere) + + def set_up_tubes(self, reply_handler, error_handler): + + chans = [] + + def tubes_ready(): + if self.telepathy_text_chan is None or \ + self.telepathy_tubes_chan is None: + return + + _logger.debug('%r: finished setting up tubes', self) + reply_handler() + + def tubes_chan_ready(chan): + _logger.debug('%r: Tubes channel %r is ready', self, chan) + self.telepathy_tubes_chan = chan + tubes_ready() + + def text_chan_ready(chan): + _logger.debug('%r: Text channel %r is ready', self, chan) + self.telepathy_text_chan = chan + tubes_ready() + + def conn_ready(conn): + _logger.debug('%r: Connection %r is ready', self, conn) + self.telepathy_conn = conn + found_text_channel = False + found_tubes_channel = False + + for chan_path, chan_iface, handle_type, handle_ in chans: + if handle_type != telepathy.HANDLE_TYPE_ROOM: + return + + if chan_iface == telepathy.CHANNEL_TYPE_TEXT: + telepathy.client.Channel( + conn.service_name, chan_path, + ready_handler=text_chan_ready, + error_handler=error_handler) + found_text_channel = True + + elif chan_iface == telepathy.CHANNEL_TYPE_TUBES: + telepathy.client.Channel( + conn.service_name, chan_path, + ready_handler=tubes_chan_ready, + error_handler=error_handler) + found_tubes_channel = True + + if not found_text_channel: + error_handler(AssertionError("Presence Service didn't create " + "a chatroom")) + elif not found_tubes_channel: + error_handler(AssertionError("Presence Service didn't create " + "tubes channel")) + + def channels_listed(bus_name, conn_path, channels): + _logger.debug('%r: Connection on %s at %s, channels: %r', + self, bus_name, conn_path, channels) + + # can't use assignment for this due to Python scoping + chans.extend(channels) + + telepathy.client.Connection(bus_name, conn_path, + ready_handler=conn_ready, + error_handler=error_handler) + + + self._activity.ListChannels(reply_handler=channels_listed, + error_handler=error_handler) + + def _join_cb(self): + _logger.debug('%r: Join finished', self) + self._joined = True + self.emit("joined", True, None) + + def _join_error_cb(self, err): + _logger.debug('%r: Join failed because: %s', self, err) + self.emit("joined", False, str(err)) + + def join(self): + """Join this activity. + + Emits 'joined' and otherwise does nothing if we're already joined. + """ + if self._joined: + self.emit("joined", True, None) + return + + _logger.debug('%r: joining', self) + + def joined(): + self.set_up_tubes(reply_handler=self._join_cb, + error_handler=self._join_error_cb) + + self._activity.Join(reply_handler=joined, + error_handler=self._join_error_cb) + + # 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, connection, channels) = self._activity.GetChannels() + _logger.debug('%r: bus name is %s, connection is %s, channels are %r', + self, bus_name, connection, channels) + return bus_name, connection, channels + + # Leaving + + def _leave_cb(self): + """Callback for async action of leaving shared activity.""" + self.emit("joined", False, "left activity") + + def _leave_error_cb(self, err): + """Callback for error in async leaving of shared activity.""" + _logger.debug('Failed to leave activity: %s', err) + + def leave(self): + """Leave this shared activity""" + _logger.debug('%r: leaving', self) + self._joined = False + self._activity.Leave(reply_handler=self._leave_cb, + error_handler=self._leave_error_cb) diff --git a/src/sugar/presence/buddy.py b/src/sugar/presence/buddy.py new file mode 100644 index 0000000..fab23d2 --- /dev/null +++ b/src/sugar/presence/buddy.py @@ -0,0 +1,239 @@ +# Copyright (C) 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. + +"""UI interface to a buddy in the presence service + +STABLE. +""" + +import gobject +import gtk +import dbus + +class Buddy(gobject.GObject): + """UI interface for a Buddy in the presence service + + Each buddy interface tracks a set of activities and properties + that can be queried to provide UI controls for manipulating + the presence interface. + + Properties Dictionary: + 'key': public key, + 'nick': nickname , + 'color': color (XXX what format), + 'current-activity': (XXX dbus path?), + 'owner': (XXX dbus path?), + 'icon': (XXX pixel data for an icon?) + See __gproperties__ + """ + __gsignals__ = { + 'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([])), + 'joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'property-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + __gproperties__ = { + 'key' : (str, None, None, None, gobject.PARAM_READABLE), + 'icon' : (str, None, None, None, gobject.PARAM_READABLE), + 'nick' : (str, None, None, None, gobject.PARAM_READABLE), + 'color' : (str, None, None, None, gobject.PARAM_READABLE), + 'current-activity' : (object, None, None, gobject.PARAM_READABLE), + 'owner' : (bool, None, None, False, gobject.PARAM_READABLE), + 'ip4-address' : (str, None, None, None, gobject.PARAM_READABLE) + } + + _PRESENCE_SERVICE = "org.laptop.Sugar.Presence" + _BUDDY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Buddy" + + def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): + """Initialise the reference to the buddy + + bus -- dbus bus object + new_obj_cb -- callback to call when this buddy joins an activity + del_obj_cb -- callback to call when this buddy leaves an activity + object_path -- path to the buddy object + """ + gobject.GObject.__init__(self) + self._object_path = object_path + self._ps_new_object = new_obj_cb + self._ps_del_object = del_obj_cb + self._properties = {} + self._activities = {} + + bobj = bus.get_object(self._PRESENCE_SERVICE, object_path) + self._buddy = dbus.Interface(bobj, self._BUDDY_DBUS_INTERFACE) + + self._icon_changed_signal = self._buddy.connect_to_signal( + 'IconChanged', self._icon_changed_cb, byte_arrays=True) + self._joined_activity_signal = self._buddy.connect_to_signal( + 'JoinedActivity', self._joined_activity_cb) + self._left_activity_signal = self._buddy.connect_to_signal( + 'LeftActivity', self._left_activity_cb) + self._property_changed_signal = self._buddy.connect_to_signal( + 'PropertyChanged', self._property_changed_cb) + + self._properties = self._get_properties_helper() + + activities = self._buddy.GetJoinedActivities() + for op in activities: + self._activities[op] = self._ps_new_object(op) + self._icon = None + + def destroy(self): + self._icon_changed_signal.remove() + self._joined_activity_signal.remove() + self._left_activity_signal.remove() + self._property_changed_signal.remove() + + def _get_properties_helper(self): + """Retrieve the Buddy's property dictionary from the service object + """ + props = self._buddy.GetProperties(byte_arrays=True) + if not props: + return {} + return props + + def do_get_property(self, pspec): + """Retrieve a particular property from our property dictionary + + pspec -- XXX some sort of GTK specifier object with attributes + including 'name', 'active' and 'icon-name' + """ + if pspec.name == "key": + return self._properties["key"] + elif pspec.name == "nick": + return self._properties["nick"] + elif pspec.name == "color": + return self._properties["color"] + elif pspec.name == "current-activity": + if not self._properties.has_key("current-activity"): + return None + curact = self._properties["current-activity"] + if not len(curact): + return None + for activity in self._activities.values(): + if activity.props.id == curact: + return activity + return None + elif pspec.name == "owner": + return self._properties["owner"] + elif pspec.name == "icon": + if not self._icon: + self._icon = str(self._buddy.GetIcon(byte_arrays=True)) + return self._icon + elif pspec.name == "ip4-address": + # IPv4 address will go away quite soon + if not self._properties.has_key("ip4-address"): + return None + return self._properties["ip4-address"] + + def object_path(self): + """Retrieve our dbus object path""" + return self._object_path + + def _emit_icon_changed_signal(self, bytes): + """Emit GObject signal when icon has changed""" + self._icon = str(bytes) + self.emit('icon-changed') + return False + + def _icon_changed_cb(self, icon_data): + """Handle dbus signal by emitting a GObject signal""" + gobject.idle_add(self._emit_icon_changed_signal, icon_data) + + def _emit_joined_activity_signal(self, object_path): + """Emit activity joined signal with Activity object""" + self.emit('joined-activity', self._ps_new_object(object_path)) + return False + + def _joined_activity_cb(self, object_path): + """Handle dbus signal by emitting a GObject signal + + Stores the activity in activities dictionary as well + """ + if not self._activities.has_key(object_path): + self._activities[object_path] = self._ps_new_object(object_path) + gobject.idle_add(self._emit_joined_activity_signal, object_path) + + def _emit_left_activity_signal(self, object_path): + """Emit activity left signal with Activity object + + XXX this calls self._ps_new_object instead of self._ps_del_object, + which would seem to be the incorrect callback? + """ + self.emit('left-activity', self._ps_new_object(object_path)) + return False + + def _left_activity_cb(self, object_path): + """Handle dbus signal by emitting a GObject signal + + Also removes from the activities dictionary + """ + if self._activities.has_key(object_path): + del self._activities[object_path] + gobject.idle_add(self._emit_left_activity_signal, object_path) + + def _handle_property_changed_signal(self, prop_list): + """Emit property-changed signal with property dictionary + + Generates a property-changed signal with the results of + _get_properties_helper() + """ + self._properties = self._get_properties_helper() + # FIXME: don't leak unexposed property names + self.emit('property-changed', prop_list) + return False + + def _property_changed_cb(self, prop_list): + """Handle dbus signal by emitting a GObject signal""" + gobject.idle_add(self._handle_property_changed_signal, prop_list) + + def get_icon_pixbuf(self): + """Retrieve Buddy's icon as a GTK pixel buffer + + XXX Why aren't the icons coming in as SVG? + """ + if self.props.icon and len(self.props.icon): + pbl = gtk.gdk.PixbufLoader() + pbl.write(self.props.icon) + pbl.close() + return pbl.get_pixbuf() + else: + return None + + def get_joined_activities(self): + """Retrieve the set of all activities which this buddy has joined + + Uses the GetJoinedActivities method on the service + object to produce object paths, wraps each in an + Activity object. + + returns list of presence Activity objects + """ + try: + resp = self._buddy.GetJoinedActivities() + except dbus.exceptions.DBusException: + return [] + acts = [] + for item in resp: + acts.append(self._ps_new_object(item)) + return acts diff --git a/src/sugar/presence/presenceservice.py b/src/sugar/presence/presenceservice.py new file mode 100644 index 0000000..a7fd1a4 --- /dev/null +++ b/src/sugar/presence/presenceservice.py @@ -0,0 +1,609 @@ +# Copyright (C) 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. + +"""UI class to access system-level presence object + +STABLE. +""" + +import logging +import traceback + +import dbus +import dbus.exceptions +import dbus.glib +import gobject + +from sugar.presence.buddy import Buddy +from sugar.presence.activity import Activity + + +DBUS_SERVICE = "org.laptop.Sugar.Presence" +DBUS_INTERFACE = "org.laptop.Sugar.Presence" +DBUS_PATH = "/org/laptop/Sugar/Presence" + +_logger = logging.getLogger('sugar.presence.presenceservice') + + +class PresenceService(gobject.GObject): + """UI-side interface to the dbus presence service + + This class provides UI programmers with simplified access + to the dbus service of the same name. It allows for observing + various events from the presence service as GObject events, + as well as some basic introspection queries. + """ + __gsignals__ = { + 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + # parameters: (activity: Activity, inviter: Buddy, message: unicode) + 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, None, ([object]*3)), + 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT, str])), + 'activity-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])) + } + + _PS_BUDDY_OP = DBUS_PATH + "/Buddies/" + _PS_ACTIVITY_OP = DBUS_PATH + "/Activities/" + + + def __init__(self, allow_offline_iface=True): + """Initialise the service and attempt to connect to events + """ + gobject.GObject.__init__(self) + self._objcache = {} + self._joined = None + + # Get a connection to the session bus + self._bus = dbus.SessionBus() + self._bus.add_signal_receiver(self._name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + # attempt to load the interface to the service... + self._allow_offline_iface = allow_offline_iface + self._get_ps() + + def _name_owner_changed_cb(self, name, old, new): + if name != DBUS_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # PS went away, clear out PS dbus service wrapper + self._ps_ = None + elif (not old and not len(old)) and (new and len(new)): + # PS started up + self._get_ps() + + _ps_ = None + def _get_ps(self): + """Retrieve dbus interface to PresenceService + + Also registers for updates from various dbus events on the + interface. + + If unable to retrieve the interface, we will temporarily + return an _OfflineInterface object to allow the calling + code to continue functioning as though it had accessed a + real presence service. + + If successful, caches the presence service interface + for use by other methods and returns that interface + """ + if not self._ps_: + try: + # NOTE: We need to follow_name_owner_changes here + # because we can not connect to a signal unless + # we follow the changes or we start the service + # before we connect. Starting the service here + # causes a major bottleneck during startup + ps = dbus.Interface( + self._bus.get_object(DBUS_SERVICE, + DBUS_PATH, + follow_name_owner_changes=True), + DBUS_INTERFACE + ) + except dbus.exceptions.DBusException, err: + _logger.error( + """Failure retrieving %r interface from + the D-BUS service %r %r: %s""", + DBUS_INTERFACE, DBUS_SERVICE, DBUS_PATH, err + ) + if self._allow_offline_iface: + return _OfflineInterface() + raise RuntimeError("Failed to connect to the presence service.") + else: + self._ps_ = ps + ps.connect_to_signal('BuddyAppeared', + self._buddy_appeared_cb) + ps.connect_to_signal('BuddyDisappeared', + self._buddy_disappeared_cb) + ps.connect_to_signal('ActivityAppeared', + self._activity_appeared_cb) + ps.connect_to_signal('ActivityDisappeared', + self._activity_disappeared_cb) + ps.connect_to_signal('ActivityInvitation', + self._activity_invitation_cb) + ps.connect_to_signal('PrivateInvitation', + self._private_invitation_cb) + return self._ps_ + + _ps = property( + _get_ps, None, None, + """DBUS interface to the PresenceService + (services/presence/presenceservice)""" + ) + + def _new_object(self, object_path): + """Turn new object path into (cached) Buddy/Activity instance + + object_path -- full dbus path of the new object, must be + prefixed with either of _PS_BUDDY_OP or _PS_ACTIVITY_OP + + Note that this method is called throughout the class whenever + the representation of the object is required, it is not only + called when the object is first discovered. The point is to only have + _one_ Python object for any D-Bus object represented by an object path, + effectively wrapping the D-Bus object in a single Python GObject. + + returns presence Buddy or Activity representation + """ + obj = None + try: + obj = self._objcache[object_path] + _logger.debug('Reused proxy %r', obj) + except KeyError: + if object_path.startswith(self._PS_BUDDY_OP): + obj = Buddy(self._bus, self._new_object, + self._del_object, object_path) + elif object_path.startswith(self._PS_ACTIVITY_OP): + obj = Activity(self._bus, self._new_object, + self._del_object, object_path) + try: + # Pre-fill the activity's ID + activity_id = obj.props.id + except dbus.exceptions.DBusException: + logging.debug('Cannot get the activity ID') + else: + raise RuntimeError("Unknown object type") + self._objcache[object_path] = obj + _logger.debug('Created proxy %r', obj) + return obj + + def _have_object(self, object_path): + return object_path in self._objcache.keys() + + def _del_object(self, object_path): + """Fully remove an object from the object cache when + it's no longer needed. + """ + del self._objcache[object_path] + + def _emit_buddy_appeared_signal(self, object_path): + """Emit GObject event with presence.buddy.Buddy object""" + self.emit('buddy-appeared', self._new_object(object_path)) + return False + + def _buddy_appeared_cb(self, op): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_buddy_appeared_signal, op) + + def _emit_buddy_disappeared_signal(self, object_path): + """Emit GObject event with presence.buddy.Buddy object""" + # Don't try to create a new object here if needed; it will probably + # fail anyway because the object has already been destroyed in the PS + if self._have_object(object_path): + obj = self._objcache[object_path] + self.emit('buddy-disappeared', obj) + + # We cannot maintain the object in the cache because that would keep + # a lot of objects from being collected. That includes UI objects + # due to signals using strong references. + # If we want to cache some despite the memory usage increase, + # we could use a LRU cache limited to some value. + del self._objcache[object_path] + obj.destroy() + + return False + + def _buddy_disappeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_buddy_disappeared_signal, object_path) + + def _emit_activity_invitation_signal(self, activity_path, buddy_path, + message): + """Emit GObject event with presence.activity.Activity object""" + self.emit('activity-invitation', self._new_object(activity_path), + self._new_object(buddy_path), unicode(message)) + return False + + def _activity_invitation_cb(self, activity_path, buddy_path, message): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_activity_invitation_signal, activity_path, + buddy_path, message) + + def _emit_private_invitation_signal(self, bus_name, connection, + channel, chan_type): + """Emit GObject event with bus_name, connection and channel""" + self.emit('private-invitation', bus_name, connection, + channel, chan_type) + return False + + def _private_invitation_cb(self, bus_name, connection, channel, chan_type): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_private_invitation_signal, bus_name, + connection, channel, chan_type) + + def _emit_activity_appeared_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" + self.emit('activity-appeared', self._new_object(object_path)) + return False + + def _activity_appeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_activity_appeared_signal, object_path) + + def _emit_activity_disappeared_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" + self.emit('activity-disappeared', self._new_object(object_path)) + return False + + def _activity_disappeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" + gobject.idle_add(self._emit_activity_disappeared_signal, object_path) + + def get(self, object_path): + """Return the Buddy or Activity object corresponding to the given + D-Bus object path. + """ + return self._new_object(object_path) + + def get_activities(self): + """Retrieve set of all activities from service + + returns list of Activity objects for all object paths + the service reports exist (using GetActivities) + """ + try: + resp = self._ps.GetActivities() + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve activity list from presence service: %s""" + % err + ) + return [] + else: + acts = [] + for item in resp: + acts.append(self._new_object(item)) + return acts + + def _get_activities_cb(self, reply_handler, resp): + acts = [] + for item in resp: + acts.append(self._new_object(item)) + + reply_handler(acts) + + def _get_activities_error_cb(self, error_handler, e): + if error_handler: + error_handler(e) + else: + _logger.warn( + """Unable to retrieve activity-list from presence service: %s""" + % e + ) + + def get_activities_async(self, reply_handler=None, error_handler=None): + """Retrieve set of all activities from service asyncronously + """ + + if not reply_handler: + logging.error('Function get_activities_async called without' \ + 'a reply handler. Can not run.') + return + + self._ps.GetActivities( + reply_handler=lambda resp: \ + self._get_activities_cb(reply_handler, resp), + error_handler=lambda e: \ + self._get_activities_error_cb(error_handler, e)) + + + 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 + """ + try: + act_op = self._ps.GetActivityById(activity_id) + except dbus.exceptions.DBusException, err: + if warn_if_none: + _logger.warn("Unable to retrieve activity handle for %r from " + "presence service: %s", activity_id, err) + return None + return self._new_object(act_op) + + def get_buddies(self): + """Retrieve set of all buddies from service + + returns list of Buddy objects for all object paths + the service reports exist (using GetBuddies) + """ + try: + resp = self._ps.GetBuddies() + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve buddy-list from presence service: %s""" + % err + ) + return [] + else: + buddies = [] + for item in resp: + buddies.append(self._new_object(item)) + return buddies + + def _get_buddies_cb(self, reply_handler, resp): + buddies = [] + for item in resp: + buddies.append(self._new_object(item)) + + reply_handler(buddies) + + def _get_buddies_error_cb(self, error_handler, e): + if error_handler: + error_handler(e) + else: + _logger.warn( + """Unable to retrieve buddy-list from presence service: %s""" + % e + ) + + def get_buddies_async(self, reply_handler=None, error_handler=None): + """Retrieve set of all buddies from service asyncronously + """ + + if not reply_handler: + logging.error('Function get_buddies_async called without' \ + 'a reply handler. Can not run.') + return + + self._ps.GetBuddies( + reply_handler=lambda resp: \ + self._get_buddies_cb(reply_handler, resp), + error_handler=lambda e: \ + self._get_buddies_error_cb(error_handler, e)) + + def get_buddy(self, key): + """Retrieve single Buddy object for the given public key + + key -- buddy's public encryption key + + returns single Buddy object or None if the activity + is not found using GetBuddyByPublicKey on the + service + """ + try: + buddy_op = self._ps.GetBuddyByPublicKey(dbus.ByteArray(key)) + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve buddy handle + for %r from presence service: %s""" + % key, err + ) + return None + return self._new_object(buddy_op) + + 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 + """ + try: + buddy_op = self._ps.GetBuddyByTelepathyHandle(tp_conn_name, + tp_conn_path, + handle) + except dbus.exceptions.DBusException, err: + _logger.warn('Unable to retrieve buddy handle for handle %u at ' + 'conn %s:%s from presence service: %s', + handle, tp_conn_name, tp_conn_path, err) + return None + return self._new_object(buddy_op) + + def get_owner(self): + """Retrieves the laptop "owner" Buddy object.""" + try: + owner_op = self._ps.GetOwner() + except dbus.exceptions.DBusException, err: + _logger.warn( + """Unable to retrieve local user/owner + from presence service: %s""" + % err + ) + raise RuntimeError("Could not get owner object.") + return self._new_object(owner_op) + + def _share_activity_cb(self, activity, op): + """Finish sharing the activity + """ + # FIXME find a better way to shutup pylint + psact = self._new_object(op) + psact._joined = True + _logger.debug('%r: Just shared, setting up tubes', activity) + psact.set_up_tubes(reply_handler=lambda: + self.emit("activity-shared", True, psact, None), + error_handler=lambda e: + self._share_activity_error_cb(activity, e)) + + def _share_activity_error_cb(self, activity, err): + """Notify with GObject event of unsuccessful sharing of activity""" + _logger.debug("Error sharing activity %s: %s" % + (activity.get_id(), err)) + self.emit("activity-shared", False, None, err) + + def share_activity(self, activity, properties=None, private=True): + """Ask presence service to ask the activity to share itself publicly. + + Uses the AdvertiseActivity method on the service to ask for the + sharing of the given activity. Arranges to emit activity-shared + event with: + + (success, Activity, err) + + on success/failure. + + returns None + """ + actid = activity.get_id() + + if properties is None: + properties = {} + + # Ensure the activity is not already shared/joined + for obj in self._objcache.values(): + if not isinstance(object, Activity): + continue + if obj.props.id == actid or obj.props.joined: + raise RuntimeError("Activity %s is already shared." % + actid) + + atype = activity.get_bundle_id() + name = activity.props.title + properties['private'] = bool(private) + self._ps.ShareActivity(actid, atype, name, properties, + reply_handler=lambda op: \ + self._share_activity_cb(activity, op), + error_handler=lambda e: \ + self._share_activity_error_cb(activity, e)) + + 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""" + + try: + bus_name, object_path = self._ps.GetPreferredConnection() + except dbus.exceptions.DBusException: + logging.error(traceback.format_exc()) + return None + + return bus_name, object_path + +class _OfflineInterface( object ): + """Offline-presence-service interface + + Used to mimic the behaviour of a real PresenceService sufficiently + to avoid crashing client code that expects the given interface. + + XXX we could likely return a "MockOwner" object reasonably + easily, but would it be worth it? + """ + def raiseException( self, *args, **named ): + """Raise dbus.exceptions.DBusException""" + raise dbus.exceptions.DBusException( + """PresenceService Interface not available""" + ) + GetActivities = raiseException + GetActivityById = raiseException + GetBuddies = raiseException + GetBuddyByPublicKey = raiseException + GetOwner = raiseException + GetPreferredConnection = raiseException + def ShareActivity( + self, actid, atype, name, properties, + reply_handler, error_handler, + ): + """Pretend to share and fail...""" + exc = IOError( + """Unable to share activity as PresenceService + is not currenly available""" + ) + return error_handler( exc ) + +class _MockPresenceService(gobject.GObject): + """Test fixture allowing testing of items that use PresenceService + + See PresenceService for usage and purpose + """ + __gsignals__ = { + 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])), + 'activity-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + def get_activities(self): + return [] + + def get_activity(self, activity_id): + return None + + def get_buddies(self): + return [] + + def get_buddy(self, key): + return None + + def get_owner(self): + return None + + def share_activity(self, activity, properties=None): + return None + +_ps = None +def get_instance(allow_offline_iface=False): + """Retrieve this process' view of the PresenceService""" + global _ps + if not _ps: + _ps = PresenceService(allow_offline_iface) + return _ps + diff --git a/src/sugar/presence/sugartubeconn.py b/src/sugar/presence/sugartubeconn.py new file mode 100644 index 0000000..954ef67 --- /dev/null +++ b/src/sugar/presence/sugartubeconn.py @@ -0,0 +1,63 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# 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 + +"""Subclass of TubeConnection that converts handles to Sugar Buddies + +STABLE. +""" + +from telepathy.constants import ( + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES) + +from sugar.presence.tubeconn import TubeConnection +from sugar.presence import presenceservice + + +class SugarTubeConnection(TubeConnection): + """Subclass of TubeConnection that converts handles to Sugar Buddies""" + + def __new__(cls, conn, tubes_iface, tube_id, address=None, + group_iface=None, mainloop=None): + self = super(SugarTubeConnection, cls).__new__( + cls, conn, tubes_iface, tube_id, address=address, + group_iface=group_iface, mainloop=mainloop) + self._conn = conn + self._group_iface = group_iface + return self + + def get_buddy(self, cs_handle): + """Retrieve a Buddy object given a telepathy handle. + + cs_handle: A channel-specific CONTACT type handle. + returns: sugar.presence Buddy object or None + """ + pservice = presenceservice.get_instance() + if self.self_handle == cs_handle: + # It's me, just get my global handle + handle = self._conn.GetSelfHandle() + elif self._group_iface.GetGroupFlags() & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + # The group (channel) has channel specific handles + handle = self._group_iface.GetHandleOwners([cs_handle])[0] + else: + # The group does not have channel specific handles + handle = cs_handle + + # deal with failure to get the handle owner + if handle == 0: + return None + return pservice.get_buddy_by_telepathy_handle( + self._conn.service_name, self._conn.object_path, handle) diff --git a/src/sugar/presence/test_presence.txt b/src/sugar/presence/test_presence.txt new file mode 100644 index 0000000..d0736a9 --- /dev/null +++ b/src/sugar/presence/test_presence.txt @@ -0,0 +1,26 @@ +This is a test of presence. + +To test this service we will start up a mock dbus library: + + >>> from sugar.testing import mockdbus + >>> import dbus + >>> pres_service = mockdbus.MockService( + ... 'org.laptop.Presence', '/org/laptop/Presence', name='pres') + >>> pres_service.install() + >>> pres_interface = dbus.Interface(pres_service, 'org.laptop.Presence') + +Then we import the library (second, to make sure it connects to our +mocked system, though the lazy instantiation in get_instance() should +handle it): + + >>> from sugar.presence import PresenceService + >>> ps = PresenceService.get_instance() + >>> pres_interface.make_response('getServices', []) + >>> ps.get_services() + Called pres.org.laptop.Presence:getServices() + [] + >>> pres_interface.make_response('getBuddies', []) + >>> ps.get_buddies() + Called pres.org.laptop.Presence:getBuddies() + [] + diff --git a/src/sugar/presence/tubeconn.py b/src/sugar/presence/tubeconn.py new file mode 100644 index 0000000..8606db6 --- /dev/null +++ b/src/sugar/presence/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): + + # pylint: disable-msg=W0212 + # Confused by __new__ + def __new__(cls, conn, tubes_iface, tube_id, address=None, + group_iface=None, mainloop=None): + 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 + + # pylint: disable-msg=W0201 + # Confused by __new__ + def _on_get_self_handle_reply(self, handle): + 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, []) -- cgit v0.9.1