Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/services/presence/buddy.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/presence/buddy.py')
-rw-r--r--services/presence/buddy.py343
1 files changed, 282 insertions, 61 deletions
diff --git a/services/presence/buddy.py b/services/presence/buddy.py
index 35191e6..ab91cea 100644
--- a/services/presence/buddy.py
+++ b/services/presence/buddy.py
@@ -1,3 +1,4 @@
+"""An "actor" on the network, whether remote or local"""
# Copyright (C) 2007, Red Hat, Inc.
# Copyright (C) 2007, Collabora Ltd.
#
@@ -29,6 +30,7 @@ _BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy"
_OWNER_INTERFACE = "org.laptop.Sugar.Presence.Buddy.Owner"
class NotFoundError(dbus.DBusException):
+ """Raised when a given actor is not found on the network"""
def __init__(self):
dbus.DBusException.__init__(self)
self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound'
@@ -37,9 +39,35 @@ class DBusGObjectMetaclass(dbus.service.InterfaceType, gobject.GObjectMeta): pas
class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGObjectMetaclass
+_PROP_NICK = "nick"
+_PROP_KEY = "key"
+_PROP_ICON = "icon"
+_PROP_CURACT = "current-activity"
+_PROP_COLOR = "color"
+_PROP_OWNER = "owner"
+_PROP_VALID = "valid"
+
class Buddy(DBusGObject):
- """Represents another person on the network and keeps track of the
- activities and resources they make available for sharing."""
+ """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 telepresence client to
+ "handle" (XXX what's that)
+ """
__gsignals__ = {
'validity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
@@ -51,17 +79,26 @@ class Buddy(DBusGObject):
}
__gproperties__ = {
- 'key' : (str, None, None, None,
+ _PROP_KEY : (str, None, None, None,
gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY),
- 'icon' : (object, None, None, gobject.PARAM_READWRITE),
- 'nick' : (str, None, None, None, gobject.PARAM_READWRITE),
- 'color' : (str, None, None, None, gobject.PARAM_READWRITE),
- 'current-activity' : (str, None, None, None, gobject.PARAM_READWRITE),
- 'valid' : (bool, None, None, False, gobject.PARAM_READABLE),
- 'owner' : (bool, None, None, False, gobject.PARAM_READABLE)
+ _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)
}
def __init__(self, bus_name, object_id, **kwargs):
+ """Initialize the Buddy object
+
+ bus_name -- DBUS object bus name (identifier)
+ object_id -- the activity's unique identifier
+ kwargs -- used to initialize the object's properties
+
+ constructs a DBUS "object path" from the _BUDDY_PATH
+ and object_id
+ """
if not bus_name:
raise ValueError("DBus bus name must be valid")
if not object_id or not isinstance(object_id, int):
@@ -83,44 +120,62 @@ class Buddy(DBusGObject):
self._nick = None
self._color = None
- if not kwargs.get("key"):
+ if not kwargs.get(_PROP_KEY):
raise ValueError("key required")
+ _ALLOWED_INIT_PROPS = [_PROP_NICK, _PROP_KEY, _PROP_ICON, _PROP_CURACT, _PROP_COLOR]
+ for (key, value) in kwargs.items():
+ if key not in _ALLOWED_INIT_PROPS:
+ logging.debug("Invalid init property '%s'; ignoring..." % key)
+ del kwargs[key]
+
gobject.GObject.__init__(self, **kwargs)
def do_get_property(self, pspec):
- if pspec.name == "key":
+ """Retrieve current value for the given property specifier
+
+ pspec -- property specifier with a "name" attribute
+ """
+ if pspec.name == _PROP_KEY:
return self._key
- elif pspec.name == "icon":
+ elif pspec.name == _PROP_ICON:
return self._icon
- elif pspec.name == "nick":
+ elif pspec.name == _PROP_NICK:
return self._nick
- elif pspec.name == "color":
+ elif pspec.name == _PROP_COLOR:
return self._color
- elif pspec.name == "current-activity":
+ 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 == "valid":
+ elif pspec.name == _PROP_VALID:
return self._valid
- elif pspec.name == "owner":
+ elif pspec.name == _PROP_OWNER:
return self._owner
def do_set_property(self, pspec, value):
- if pspec.name == "icon":
+ """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 == "nick":
+ elif pspec.name == _PROP_NICK:
self._nick = value
- elif pspec.name == "color":
+ elif pspec.name == _PROP_COLOR:
self._color = value
- elif pspec.name == "current-activity":
+ elif pspec.name == _PROP_CURACT:
self._current_activity = value
- elif pspec.name == "key":
+ elif pspec.name == _PROP_KEY:
self._key = value
self._update_validity()
@@ -129,27 +184,42 @@ class Buddy(DBusGObject):
@dbus.service.signal(_BUDDY_INTERFACE,
signature="ay")
def IconChanged(self, icon_data):
- pass
+ """Generates DBUS signal with icon_data"""
@dbus.service.signal(_BUDDY_INTERFACE,
signature="o")
def JoinedActivity(self, activity_path):
- pass
+ """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):
- pass
+ """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):
- pass
+ """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.
+ """
# 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)
@@ -157,6 +227,11 @@ class Buddy(DBusGObject):
@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():
acts.append(act.object_path())
@@ -165,22 +240,41 @@ class Buddy(DBusGObject):
@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['nick'] = self.props.nick
- props['owner'] = self.props.owner
- props['key'] = self.props.key
- props['color'] = self.props.color
+ 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.current_activity:
- props['current-activity'] = self.props.current_activity
+ props[_PROP_CURACT] = self.props.current_activity
else:
- props['current-activity'] = ""
+ props[_PROP_CURACT] = ""
return props
# methods
def object_path(self):
+ """Retrieve our dbus.ObjectPath object"""
return dbus.ObjectPath(self._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
@@ -189,6 +283,12 @@ class Buddy(DBusGObject):
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
@@ -197,6 +297,7 @@ class Buddy(DBusGObject):
self.LeftActivity(activity.object_path())
def get_joined_activities(self):
+ """Retrieves list of still-valid activity objects"""
acts = []
for act in self._activities.values():
if act.props.valid:
@@ -204,36 +305,54 @@ class Buddy(DBusGObject):
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
- if "nick" in properties.keys():
- nick = properties["nick"]
+ changed_props = {}
+ if _PROP_NICK in properties.keys():
+ nick = properties[_PROP_NICK]
if nick != self._nick:
self._nick = nick
+ changed_props[_PROP_NICK] = nick
changed = True
- if "color" in properties.keys():
- color = properties["color"]
+ if _PROP_COLOR in properties.keys():
+ color = properties[_PROP_COLOR]
if color != self._color:
self._color = color
+ changed_props[_PROP_COLOR] = color
changed = True
- if "current-activity" in properties.keys():
- curact = properties["current-activity"]
+ if _PROP_CURACT in properties.keys():
+ curact = properties[_PROP_CURACT]
if curact != self._current_activity:
self._current_activity = curact
+ changed_props[_PROP_CURACT] = curact
changed = True
- if not changed:
+ if not changed or not len(changed_props.keys()):
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:
- self.PropertyChanged(properties)
- self.emit('property-changed', properties)
+ self.PropertyChanged(changed_props)
+ 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:
@@ -247,6 +366,13 @@ class Buddy(DBusGObject):
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"
__gproperties__ = {
@@ -255,7 +381,17 @@ class GenericOwner(Buddy):
'key-hash' : (str, None, None, None, gobject.PARAM_READABLE | gobject.PARAM_CONSTRUCT)
}
- def __init__(self, bus_name, object_id, **kwargs):
+ def __init__(self, ps, bus_name, object_id, **kwargs):
+ """Initialize the GenericOwner instance
+
+ ps -- presenceservice.PresenceService object
+ bus_name -- DBUS object bus name (identifier)
+ object_id -- the activity's unique identifier
+ kwargs -- used to initialize the object's properties
+
+ calls Buddy.__init__
+ """
+ self._ps = ps
self._server = 'olpc.collabora.co.uk'
self._key_hash = None
self._registered = False
@@ -273,28 +409,47 @@ class GenericOwner(Buddy):
self._owner = True
def get_registered(self):
+ """Retrieve whether owner has registered with presence server"""
return self._registered
def get_server(self):
+ """Retrieve presence server (XXX url??)"""
return self._server
def get_key_hash(self):
+ """Retrieve the user's private-key hash"""
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):
- """Class representing the owner of the machine. This is the client
- portion of the Owner, paired with the server portion in Owner.py."""
-
+ """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, bus_name, object_id, test=False):
+ def __init__(self, ps, bus_name, object_id, test=False):
+ """Initialize the ShellOwner instance
+
+ ps -- presenceservice.PresenceService object
+ bus_name -- DBUS object bus name (identifier)
+ object_id -- the activity's unique identifier
+ test -- ignored
+
+ 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()
@@ -307,7 +462,7 @@ class ShellOwner(GenericOwner):
icon = f.read()
f.close()
- GenericOwner.__init__(self, bus_name, object_id, key=key, nick=nick,
+ GenericOwner.__init__(self, ps, bus_name, object_id, key=key, nick=nick,
color=color, icon=icon, server=server, key_hash=key_hash,
registered=registered)
@@ -324,10 +479,17 @@ class ShellOwner(GenericOwner):
pass
def set_registered(self, value):
+ """Handle notification that we have been registered"""
if value:
profile.set_server_registered()
def _name_owner_changed_handler(self, name, old, new):
+ """Handle DBUS notification of a new / renamed service
+
+ Watches for the _SHELL_SERVICE, i.e. the Sugar Shell,
+ and registers with it if we have not yet registered
+ with it (using _connect_to_shell).
+ """
if name != self._SHELL_SERVICE:
return
if (old and len(old)) and (not new and not len(new)):
@@ -338,6 +500,11 @@ class ShellOwner(GenericOwner):
self._connect_to_shell()
def _connect_to_shell(self):
+ """Connect to the Sugar Shell service to watch for events
+
+ Connects the various XChanged events on the Sugar Shell
+ service to our _x_changed_cb methods.
+ """
obj = self._bus.get_object(self._SHELL_SERVICE, self._SHELL_PATH)
self._shell_owner = dbus.Interface(obj, self._SHELL_OWNER_INTERFACE)
self._shell_owner.connect_to_signal('IconChanged', self._icon_changed_cb)
@@ -347,21 +514,30 @@ class ShellOwner(GenericOwner):
self._cur_activity_changed_cb)
def _icon_changed_cb(self, icon):
+ """Handle icon change, set property to generate event"""
self.props.icon = icon
def _color_changed_cb(self, color):
- props = {'color': color}
+ """Handle color change, set property to generate event"""
+ props = {_PROP_COLOR: color}
self.set_properties(props)
def _nick_changed_cb(self, nick):
- props = {'nick': 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 = {'current-activity': activity_id}
+ props = {_PROP_CURACT: activity_id}
self.set_properties(props)
@@ -371,9 +547,12 @@ class TestOwner(GenericOwner):
__gtype_name__ = "TestOwner"
- def __init__(self, bus_name, object_id, test_num):
+ def __init__(self, ps, bus_name, object_id, test_num):
self._cp = ConfigParser()
self._section = "Info"
+ self._test_activities = []
+ self._test_cur_act = ""
+ self._change_timeout = 0
self._cfg_file = os.path.join(env.get_profile_path(), 'test-buddy-%d' % test_num)
@@ -392,11 +571,46 @@ class TestOwner(GenericOwner):
color = xocolor.XoColor().to_string()
icon = _get_random_image()
- GenericOwner.__init__(self, bus_name, object_id, key=pubkey, nick=nick,
+ logging.debug("pubkey is %s" % pubkey)
+ GenericOwner.__init__(self, ps, bus_name, object_id, key=pubkey, nick=nick,
color=color, icon=icon, registered=registered, key_hash=privkey_hash)
+ self._ps.connect('connection-status', self._ps_connection_status_cb)
+
+ def _share_reply_cb(self, actid, object_path):
+ activity = self._ps.internal_get_activity(actid)
+ if not activity or not object_path:
+ logging.debug("Couldn't find activity %s even though it was shared." % actid)
+ return
+ logging.debug("Shared activity %s (%s)." % (actid, activity.props.name))
+ self._test_activities.append(activity)
+
+ def _share_error_cb(self, actid, err):
+ logging.debug("Error sharing activity %s: %s" % (actid, str(err)))
+
+ def _ps_connection_status_cb(self, ps, connected):
+ if not connected:
+ return
+
+ if not len(self._test_activities):
+ # Share some activities
+ actid = util.unique_id("Activity 1")
+ callbacks = (lambda *args: self._share_reply_cb(actid, *args),
+ lambda *args: self._share_error_cb(actid, *args))
+ atype = "org.laptop.WebActivity"
+ properties = {"foo": "bar"}
+ self._ps._share_activity(actid, atype, "Wembley Stadium", properties, callbacks)
+
+ actid2 = util.unique_id("Activity 2")
+ callbacks = (lambda *args: self._share_reply_cb(actid2, *args),
+ lambda *args: self._share_error_cb(actid2, *args))
+ atype = "org.laptop.WebActivity"
+ properties = {"baz": "bar"}
+ self._ps._share_activity(actid2, atype, "Maine Road", properties, callbacks)
+
# Change a random property ever 10 seconds
- gobject.timeout_add(10000, self._update_something)
+ if self._change_timeout == 0:
+ self._change_timeout = gobject.timeout_add(10000, self._update_something)
def set_registered(self, value):
if value:
@@ -437,23 +651,26 @@ class TestOwner(GenericOwner):
self.props.icon = _get_random_image()
elif it == 1:
from sugar.graphics import xocolor
- props = {'color': xocolor.XoColor().to_string()}
+ props = {_PROP_COLOR: xocolor.XoColor().to_string()}
self.set_properties(props)
elif it == 2:
- props = {'nick': _get_random_name()}
+ props = {_PROP_NICK: _get_random_name()}
self.set_properties(props)
elif it == 3:
- bork = random.randint(25, 65)
- it = ""
- for i in range(0, bork):
- it += chr(random.randint(40, 127))
- from sugar import util
- props = {'current-activity': util.unique_id(it)}
+ actid = ""
+ idx = random.randint(0, len(self._test_activities))
+ # if idx == len(self._test_activites), it means no current
+ # activity
+ if idx < len(self._test_activities):
+ activity = self._test_activities[idx]
+ actid = activity.props.id
+ props = {_PROP_CURACT: actid}
self.set_properties(props)
return True
def _hash_private_key(self):
+ """Unused method to has a private key, see profile"""
self.privkey_hash = None
key_path = os.path.join(env.get_profile_path(), 'owner.key')
@@ -504,6 +721,7 @@ def _extract_public_key(keyfile):
return key
def _extract_private_key(keyfile):
+ """Get a private key from a private key file"""
# Extract the private key
try:
f = open(keyfile, "r")
@@ -527,6 +745,7 @@ def _extract_private_key(keyfile):
return key
def _get_new_keypair(num):
+ """Retrieve a public/private key pair for testing"""
# Generate keypair
privkeyfile = os.path.join("/tmp", "test%d.key" % num)
pubkeyfile = os.path.join("/tmp", 'test%d.key.pub' % num)
@@ -559,10 +778,12 @@ def _get_new_keypair(num):
return (pubkey, privkey)
def _get_random_name():
+ """Produce random names for testing"""
names = ["Liam", "Noel", "Guigsy", "Whitey", "Bonehead"]
return names[random.randint(0, len(names) - 1)]
def _get_random_image():
+ """Produce a random image for display"""
import cairo, math, random, gtk
def rand():