"""An "actor" on the network, whether remote or local""" # Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, 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 os import gobject import dbus import dbus.service from dbus.gobject_service import ExportedGObject import psutils from sugar import env, profile import logging _BUDDY_PATH = "/org/laptop/Sugar/Presence/Buddies/" _BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy" _OWNER_INTERFACE = "org.laptop.Sugar.Presence.Buddy.Owner" _PROP_NICK = "nick" _PROP_KEY = "key" _PROP_ICON = "icon" _PROP_CURACT = "current-activity" _PROP_COLOR = "color" _PROP_OWNER = "owner" _PROP_VALID = "valid" _PROP_OBJID = 'objid' # Will go away soon _PROP_IP4_ADDRESS = "ip4-address" _logger = logging.getLogger('s-p-s.buddy') class Buddy(ExportedGObject): """Person on the network (tracks properties and shared activites) The Buddy is a collection of metadata describing a particular actor/person on the network. The Buddy object tracks a set of activities which the actor has shared with the presence service. Buddies have a "valid" property which is used to flag Buddies which are no longer reachable. That is, a Buddy may represent a no-longer reachable target on the network. The Buddy emits GObject events that the PresenceService uses to track changes in its status. Attributes: _activities -- dictionary mapping activity ID to activity.Activity objects handles -- dictionary mapping Telepathy client plugin to contact handle (an integer representing the JID or unique ID); channel-specific handles do not appear here """ __gsignals__ = { 'validity-changed': # The buddy's validity changed. # Validity starts off False, and becomes True when the buddy # has a color, a nick and a key. # * the new validity: bool (gobject.SIGNAL_RUN_FIRST, None, [bool]), 'property-changed': # One of the buddy's properties has changed. # * those properties that have changed: # dict { str => object } (gobject.SIGNAL_RUN_FIRST, None, [object]), 'icon-changed': # The buddy's icon changed. # * the bytes of the icon: str (gobject.SIGNAL_RUN_FIRST, None, [object]), 'disappeared': # The buddy is offline (has no Telepathy handles and is not the # Owner) (gobject.SIGNAL_RUN_FIRST, None, []), } __gproperties__ = { _PROP_KEY : (str, None, None, None, gobject.PARAM_READWRITE), _PROP_ICON : (object, None, None, gobject.PARAM_READWRITE), _PROP_NICK : (str, None, None, None, gobject.PARAM_READWRITE), _PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE), _PROP_CURACT : (str, None, None, None, gobject.PARAM_READWRITE), _PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE), _PROP_OWNER : (bool, None, None, False, gobject.PARAM_READABLE), _PROP_OBJID : (str, None, None, None, gobject.PARAM_READABLE), _PROP_IP4_ADDRESS : (str, None, None, None, gobject.PARAM_READWRITE) } def __init__(self, bus, object_id, **kwargs): """Initialize the Buddy object bus -- connection to the D-Bus session bus object_id -- the buddy's unique identifier, either based on their key-ID or JID kwargs -- used to initialize the object's properties constructs a DBUS "object path" from the _BUDDY_PATH and object_id """ self._object_id = object_id self._object_path = dbus.ObjectPath(_BUDDY_PATH + object_id) self._activities = {} # Activity ID -> Activity self._activity_sigids = {} self.handles = {} # tp client -> handle self._valid = False self._owner = False self._key = None self._icon = '' self._current_activity = None self._nick = None self._color = None self._ip4_address = None _ALLOWED_INIT_PROPS = [_PROP_NICK, _PROP_KEY, _PROP_ICON, _PROP_CURACT, _PROP_COLOR, _PROP_IP4_ADDRESS] for (key, value) in kwargs.items(): if key not in _ALLOWED_INIT_PROPS: _logger.debug("Invalid init property '%s'; ignoring..." % key) del kwargs[key] # Set icon after superclass init, because it sends DBus and GObject # signals when set icon_data = None if kwargs.has_key(_PROP_ICON): icon_data = kwargs[_PROP_ICON] del kwargs[_PROP_ICON] ExportedGObject.__init__(self, bus, self._object_path, gobject_properties=kwargs) if icon_data: self.props.icon = icon_data def do_get_property(self, pspec): """Retrieve current value for the given property specifier pspec -- property specifier with a "name" attribute """ if pspec.name == _PROP_OBJID: return self._object_id elif pspec.name == _PROP_KEY: return self._key elif pspec.name == _PROP_ICON: return self._icon elif pspec.name == _PROP_NICK: return self._nick elif pspec.name == _PROP_COLOR: return self._color elif pspec.name == _PROP_CURACT: if not self._current_activity: return None if not self._activities.has_key(self._current_activity): return None return self._current_activity elif pspec.name == _PROP_VALID: return self._valid elif pspec.name == _PROP_OWNER: return self._owner elif pspec.name == _PROP_IP4_ADDRESS: return self._ip4_address def do_set_property(self, pspec, value): """Set given property pspec -- property specifier with a "name" attribute value -- value to set emits 'icon-changed' signal on icon setting calls _update_validity on all calls """ if pspec.name == _PROP_ICON: if str(value) != self._icon: self._icon = str(value) self.IconChanged(self._icon) self.emit('icon-changed', self._icon) elif pspec.name == _PROP_NICK: self._nick = value elif pspec.name == _PROP_COLOR: self._color = value elif pspec.name == _PROP_CURACT: self._current_activity = value elif pspec.name == _PROP_KEY: if self._key: raise RuntimeError("Key already set.") self._key = value elif pspec.name == _PROP_IP4_ADDRESS: self._ip4_address = value self._update_validity() # dbus signals @dbus.service.signal(_BUDDY_INTERFACE, signature="ay") def IconChanged(self, icon_data): """Generates DBUS signal with icon_data""" @dbus.service.signal(_BUDDY_INTERFACE, signature="o") def JoinedActivity(self, activity_path): """Generates DBUS signal when buddy joins activity activity_path -- DBUS path to the activity object """ @dbus.service.signal(_BUDDY_INTERFACE, signature="o") def LeftActivity(self, activity_path): """Generates DBUS signal when buddy leaves activity activity_path -- DBUS path to the activity object """ @dbus.service.signal(_BUDDY_INTERFACE, signature="a{sv}") def PropertyChanged(self, updated): """Generates DBUS signal when buddy's property changes updated -- updated property-set (dictionary) with the Buddy's property (changed) values. Note: not the full set of properties, just the changes. """ def add_telepathy_handle(self, tp_client, handle): """Add a Telepathy handle.""" conn = tp_client.get_connection() self.TelepathyHandleAdded(conn.service_name, conn.object_path, handle) self.handles[tp_client] = handle @dbus.service.signal(_BUDDY_INTERFACE, signature='sou') def TelepathyHandleAdded(self, tp_conn_name, tp_conn_path, handle): """Another Telepathy handle has become associated with the buddy. This must only be emitted for non-channel-specific handles. tp_conn_name -- The bus name at which the Telepathy connection may be found tp_conn_path -- The object path at which the Telepathy connection may be found handle -- The handle of type CONTACT, which is not channel-specific, newly associated with the buddy """ def remove_telepathy_handle(self, tp_client, handle): """Remove a Telepathy handle.""" conn = tp_client.get_connection() my_handle = self.handles.get(tp_client, 0) if my_handle == handle: del self.handles[tp_client] self.TelepathyHandleRemoved(conn.service_name, conn.object_path, handle) # the Owner can't disappear - that would be silly if not self.handles and not self._owner: self.emit('disappeared') else: _logger.debug('Telepathy handle %u supposedly removed, but ' 'my handle on that connection is %u - ignoring', handle, my_handle) @dbus.service.signal(_BUDDY_INTERFACE, signature='sou') def TelepathyHandleRemoved(self, tp_conn_name, tp_conn_path, handle): """A Telepathy handle has ceased to be associated with the buddy, probably because that contact went offline. The parameters are the same as for TelepathyHandleAdded. """ # dbus methods @dbus.service.method(_BUDDY_INTERFACE, in_signature="", out_signature="ay") def GetIcon(self): """Retrieve Buddy's icon data returns empty string or dbus.ByteArray """ if not self.props.icon: return "" return dbus.ByteArray(self.props.icon) @dbus.service.method(_BUDDY_INTERFACE, in_signature="", out_signature="ao") def GetJoinedActivities(self): """Retrieve set of Buddy's joined activities (paths) returns list of dbus service paths for the Buddy's joined activities """ acts = [] for act in self.get_joined_activities(): if act.props.valid: acts.append(act.object_path()) return acts @dbus.service.method(_BUDDY_INTERFACE, in_signature="", out_signature="a{sv}") def GetProperties(self): """Retrieve set of Buddy's properties returns dictionary of nick : str(nickname) owner : bool( whether this Buddy is an owner??? ) XXX what is the owner flag for? key : str(public-key) color: Buddy's icon colour XXX what type? current-activity: Buddy's current activity_id, or "" if no current activity """ props = {} props[_PROP_NICK] = self.props.nick props[_PROP_OWNER] = self.props.owner props[_PROP_KEY] = self.props.key props[_PROP_COLOR] = self.props.color if self.props.ip4_address: props[_PROP_IP4_ADDRESS] = self.props.ip4_address else: props[_PROP_IP4_ADDRESS] = "" if self.props.current_activity: props[_PROP_CURACT] = self.props.current_activity else: props[_PROP_CURACT] = "" return props @dbus.service.method(_BUDDY_INTERFACE, in_signature='', out_signature='a(sou)') def GetTelepathyHandles(self): """Return a list of non-channel-specific Telepathy contact handles associated with this Buddy. :Returns: An array of triples (connection well-known bus name, connection object path, handle). """ ret = [] for plugin in self.handles: conn = plugin.get_connection() ret.append((str(conn.service_name), conn.object_path, self.handles[plugin])) # methods def object_path(self): """Retrieve our dbus.ObjectPath object""" return dbus.ObjectPath(self._object_path) def _activity_validity_changed_cb(self, activity, valid): """Join or leave the activity when its validity changes""" if valid: self.JoinedActivity(activity.object_path()) else: self.LeftActivity(activity.object_path()) def add_activity(self, activity): """Add an activity to the Buddy's set of activities activity -- activity.Activity instance calls JoinedActivity """ actid = activity.props.id if self._activities.has_key(actid): return self._activities[actid] = activity # join/leave activity when it's validity changes sigid = activity.connect("validity-changed", self._activity_validity_changed_cb) self._activity_sigids[actid] = sigid if activity.props.valid: self.JoinedActivity(activity.object_path()) def remove_activity(self, activity): """Remove the activity from the Buddy's set of activities activity -- activity.Activity instance calls LeftActivity """ actid = activity.props.id if not self._activities.has_key(actid): return activity.disconnect(self._activity_sigids[actid]) del self._activity_sigids[actid] del self._activities[actid] if activity.props.valid: self.LeftActivity(activity.object_path()) def get_joined_activities(self): """Retrieves list of still-valid activity objects""" acts = [] for act in self._activities.values(): acts.append(act) return acts def set_properties(self, properties): """Set the given set of properties on the object properties -- set of property values to set if no change, no events generated if change, generates property-changed and calls _update_validity """ changed = False changed_props = {} if _PROP_NICK in properties: nick = properties[_PROP_NICK] if nick != self._nick: self._nick = nick changed_props[_PROP_NICK] = nick changed = True if _PROP_COLOR in properties: color = properties[_PROP_COLOR] if color != self._color: self._color = color changed_props[_PROP_COLOR] = color changed = True if _PROP_CURACT in properties: curact = properties[_PROP_CURACT] if curact != self._current_activity: self._current_activity = curact changed_props[_PROP_CURACT] = curact changed = True if _PROP_IP4_ADDRESS in properties: ip4addr = properties[_PROP_IP4_ADDRESS] if ip4addr != self._ip4_address: self._ip4_address = ip4addr changed_props[_PROP_IP4_ADDRESS] = ip4addr changed = True if _PROP_KEY in properties: # don't allow key to be set more than once if self._key is None: key = properties[_PROP_KEY] if key is not None: self._key = key changed_props[_PROP_KEY] = key changed = True if not changed or not changed_props: return # Try emitting PropertyChanged before updating validity # to avoid leaking a PropertyChanged signal before the buddy is # actually valid the first time after creation if self._valid: dbus_changed = {} for key, value in changed_props.items(): if value: dbus_changed[key] = value else: dbus_changed[key] = "" self.PropertyChanged(dbus_changed) self.emit('property-changed', changed_props) self._update_validity() def _update_validity(self): """Check whether we are now valid validity is True if color, nick and key are non-null emits validity-changed if we have changed validity """ try: old_valid = self._valid if self._color and self._nick and self._key: self._valid = True else: self._valid = False if old_valid != self._valid: self.emit("validity-changed", self._valid) except AttributeError: self._valid = False class GenericOwner(Buddy): """Common functionality for Local User-like objects The TestOwner wants to produce something *like* a ShellOwner, but with randomised changes and the like. This class provides the common features for a real local owner and a testing one. """ __gtype_name__ = "GenericOwner" def __init__(self, ps, bus, object_id, **kwargs): """Initialize the GenericOwner instance ps -- presenceservice.PresenceService object bus -- a connection to the D-Bus session bus object_id -- the activity's unique identifier kwargs -- used to initialize the object's properties calls Buddy.__init__ """ self._ps = ps self._server = kwargs.pop("server", "olpc.collabora.co.uk") self._key_hash = kwargs.pop("key_hash", None) self._registered = kwargs.pop("registered", False) self._ip4_addr_monitor = psutils.IP4AddressMonitor.get_instance() self._ip4_addr_monitor.connect("address-changed", self._ip4_address_changed_cb) if self._ip4_addr_monitor.props.address: kwargs["ip4-address"] = self._ip4_addr_monitor.props.address Buddy.__init__(self, bus, object_id, **kwargs) self._owner = True self._bus = dbus.SessionBus() def _ip4_address_changed_cb(self, monitor, address): """Handle IPv4 address change, set property to generate event""" props = {_PROP_IP4_ADDRESS: address} self.set_properties(props) def get_registered(self): """Retrieve whether owner has registered with presence server""" return self._registered def get_server(self): """Retrieve XMPP server hostname (used by the server plugin)""" return self._server def get_key_hash(self): """Retrieve the user's private-key hash (used by the server plugin as a password) """ return self._key_hash def set_registered(self, registered): """Customisation point: handle the registration of the owner""" raise RuntimeError("Subclasses must implement") class ShellOwner(GenericOwner): """Representation of the local-machine owner using Sugar's Shell The ShellOwner uses the Sugar Shell's dbus services to register for updates about the user's profile description. """ __gtype_name__ = "ShellOwner" _SHELL_SERVICE = "org.laptop.Shell" _SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner" _SHELL_PATH = "/org/laptop/Shell" def __init__(self, ps, bus): """Initialize the ShellOwner instance ps -- presenceservice.PresenceService object bus -- a connection to the D-Bus session bus Retrieves initial property values from the profile module. Loads the buddy icon from file as well. XXX note: no error handling on that calls GenericOwner.__init__ """ server = profile.get_server() key_hash = profile.get_private_key_hash() registered = profile.get_server_registered() key = profile.get_pubkey() nick = profile.get_nick_name() color = profile.get_color().to_string() icon_file = os.path.join(env.get_profile_path(), "buddy-icon.jpg") f = open(icon_file, "r") icon = f.read() f.close() GenericOwner.__init__(self, ps, bus, psutils.pubkey_to_keyid(key), key=key, nick=nick, color=color, icon=icon, server=server, key_hash=key_hash, registered=registered) # Ask to get notifications on Owner object property changes in the # shell. If it's not currently running, no problem - we'll get the # signals when it does run for (signal, cb) in (('IconChanged', self._icon_changed_cb), ('ColorChanged', self._color_changed_cb), ('NickChanged', self._nick_changed_cb)): self._bus.add_signal_receiver(cb, signal_name=signal, dbus_interface=self._SHELL_OWNER_INTERFACE, bus_name=self._SHELL_SERVICE, path=self._SHELL_PATH) def set_registered(self, value): """Handle notification that we have been registered""" if value: profile.set_server_registered() def _icon_changed_cb(self, icon): """Handle icon change, set property to generate event""" self.props.icon = icon def _color_changed_cb(self, color): """Handle color change, set property to generate event""" props = {_PROP_COLOR: color} self.set_properties(props) def _nick_changed_cb(self, nick): """Handle nickname change, set property to generate event""" props = {_PROP_NICK: nick} self.set_properties(props) def _cur_activity_changed_cb(self, activity_id): """Handle current-activity change, set property to generate event Filters out local activities (those not in self.activites) because the network users can't join those activities, so the activity_id shared will be None in those cases... """ if not self._activities.has_key(activity_id): # This activity is local-only activity_id = None props = {_PROP_CURACT: activity_id} self.set_properties(props)