diff options
author | Mike C. Fletcher <mcfletch@raistlin.(none)> | 2007-04-21 18:05:08 (GMT) |
---|---|---|
committer | Mike C. Fletcher <mcfletch@raistlin.(none)> | 2007-04-21 18:05:08 (GMT) |
commit | df4919de2faaf3ed9719fe66dda54a0e2b2cd0d5 (patch) | |
tree | 766a298e2f1bd709775e985a4d586da5ebb8be9a /services | |
parent | e571b3bff9b834c099bc82be5e20305d9dfb9730 (diff) | |
parent | a0348a565c09e434514b34b05f3866c97f0aee49 (diff) |
Merge branch 'master' of git+ssh://mcfletch@dev.laptop.org/git/sugar
Diffstat (limited to 'services')
-rw-r--r-- | services/clipboard/clipboardobject.py | 11 | ||||
-rw-r--r-- | services/clipboard/clipboardservice.py | 3 | ||||
-rwxr-xr-x | services/clipboard/sugar-clipboard | 2 | ||||
-rw-r--r-- | services/clipboard/typeregistry.py | 62 | ||||
-rwxr-xr-x | services/console/sugar-console | 2 | ||||
-rwxr-xr-x | services/datastore/sugar-data-store | 2 | ||||
-rw-r--r-- | services/presence/__init__.py | 36 | ||||
-rw-r--r-- | services/presence/activity.py | 162 | ||||
-rw-r--r-- | services/presence/buddy.py | 343 | ||||
-rw-r--r-- | services/presence/presenceservice.py | 52 | ||||
-rw-r--r-- | services/presence/psutils.py | 6 | ||||
-rw-r--r-- | services/presence/server_plugin.py | 163 | ||||
-rwxr-xr-x | services/presence/sugar-presence-service | 2 |
13 files changed, 756 insertions, 90 deletions
diff --git a/services/clipboard/clipboardobject.py b/services/clipboard/clipboardobject.py index 919acd0..ab00b14 100644 --- a/services/clipboard/clipboardobject.py +++ b/services/clipboard/clipboardobject.py @@ -1,3 +1,6 @@ +import os +import logging + import typeregistry class ClipboardObject: @@ -8,6 +11,10 @@ class ClipboardObject: self._percent = 0 self._formats = {} + def destroy(self): + for type, format in self._formats.iteritems(): + format.destroy() + def get_id(self): return self._id @@ -49,6 +56,10 @@ class Format: self._data = data self._on_disk = on_disk + def destroy(self): + if self._on_disk: + os.remove(self._data.replace('file://', '')) + def get_type(self): return self._type diff --git a/services/clipboard/clipboardservice.py b/services/clipboard/clipboardservice.py index 0ed423b..16e37b1 100644 --- a/services/clipboard/clipboardservice.py +++ b/services/clipboard/clipboardservice.py @@ -109,7 +109,8 @@ class ClipboardDBusServiceHelper(dbus.service.Object): @dbus.service.method(_CLIPBOARD_DBUS_INTERFACE, in_signature="o", out_signature="") def delete_object(self, object_path): - del self._objects[str(object_path)] + cb_object = self._objects.pop(str(object_path)) + cb_object.destroy() self.object_deleted(object_path) logging.debug('Deleted object with object_id ' + object_path) diff --git a/services/clipboard/sugar-clipboard b/services/clipboard/sugar-clipboard index 9ee32e7..f9dcfb3 100755 --- a/services/clipboard/sugar-clipboard +++ b/services/clipboard/sugar-clipboard @@ -32,7 +32,7 @@ import dbus.glib from sugar import env -sys.path.insert(0, env.get_service_path('clipboard')) +sys.path.append(env.get_service_path('clipboard')) from clipboardservice import ClipboardService diff --git a/services/clipboard/typeregistry.py b/services/clipboard/typeregistry.py index b794cee..0cab0e9 100644 --- a/services/clipboard/typeregistry.py +++ b/services/clipboard/typeregistry.py @@ -1,5 +1,7 @@ import logging from gettext import gettext as _ +import urlparse +import posixpath class FileType: def __init__(self, formats): @@ -197,6 +199,64 @@ class OOTextFileType(FileType): return mime_type in cls._types matches_mime_type = classmethod(matches_mime_type) +class UriListFileType(FileType): + + _types = set(['text/uri-list']) + + def _is_image(self): + uris = self._formats['text/uri-list'].get_data().split('\n') + if len(uris) == 1: + uri = urlparse.urlparse(uris[0]) + ext = posixpath.splitext(uri.path)[1] + logging.debug(ext) + # FIXME: Bad hack, the type registry should treat text/uri-list as a special case. + if ext in ['.jpg', '.jpeg', '.gif', '.png', '.svg']: + return True + + return False + + def get_name(self): + if self._is_image(): + return _('Image') + else: + return _('File') + + def get_icon(self): + if self._is_image(): + return 'theme:object-image' + else: + return 'theme:stock-missing' + + def get_preview(self): + return '' + + def get_activity(self): + return '' + + def matches_mime_type(cls, mime_type): + return mime_type in cls._types + matches_mime_type = classmethod(matches_mime_type) + +class XoFileType(FileType): + + _types = set(['application/vnd.olpc-x-sugar']) + + def get_name(self): + return _('Activity package') + + def get_icon(self): + return 'theme:stock-missing' + + def get_preview(self): + return '' + + def get_activity(self): + return '' + + def matches_mime_type(cls, mime_type): + return mime_type in cls._types + matches_mime_type = classmethod(matches_mime_type) + class UnknownFileType(FileType): def get_name(self): return _('Object') @@ -221,11 +281,13 @@ class TypeRegistry: self._types.append(MsWordFileType) self._types.append(RtfFileType) self._types.append(OOTextFileType) + self._types.append(UriListFileType) self._types.append(UriFileType) self._types.append(ImageFileType) self._types.append(AbiwordFileType) self._types.append(TextFileType) self._types.append(SqueakProjectFileType) + self._types.append(XoFileType) def get_type(self, formats): for file_type in self._types: diff --git a/services/console/sugar-console b/services/console/sugar-console index d4d7af0..af709a6 100755 --- a/services/console/sugar-console +++ b/services/console/sugar-console @@ -7,6 +7,6 @@ import os import sys from sugar import env -sys.path.insert(0, env.get_service_path('console')) +sys.path.append(env.get_service_path('console')) import console diff --git a/services/datastore/sugar-data-store b/services/datastore/sugar-data-store index 003e273..eb325f2 100755 --- a/services/datastore/sugar-data-store +++ b/services/datastore/sugar-data-store @@ -24,7 +24,7 @@ import logging from sugar import logger from sugar import env -sys.path.insert(0, env.get_service_path('datastore')) +sys.path.append(env.get_service_path('datastore')) logger.start('data-store') logging.info('Starting the data store...') diff --git a/services/presence/__init__.py b/services/presence/__init__.py index e69de29..bd64375 100644 --- a/services/presence/__init__.py +++ b/services/presence/__init__.py @@ -0,0 +1,36 @@ +"""Service to track buddies and activities on the network + +Model objects: + + activity.Activity -- tracks a (shared/shareable) activity + with many properties and observable events + + buddy.Buddy -- tracks a reference to a particular actor + on the network + + buddy.GenericOwner -- actor who owns a particular + activity on the network + + buddy.ShellOwner -- actor who owns the local machine + connects to the owner module (on the server) + +Controller objects: + + presenceservice.PresenceService -- controller which connects + a networking plugin to a DBUS service. Generates events + for networking events, forwards updates/requests to the + server plugin. + + server_plugin.ServerPlugin -- implementation of networking + plugin using telepathy Python (Jabber) to provide the + underlying communications layer. Generates GObject + events that the PresenceService observes to forward onto + the DBUS clients. + +Utility machinery: + + buddyiconcache.BuddyIconCache -- caches buddy icons on disk + based on the "jid" XXX Jabber ID? of the buddy. + + psutils -- trivial function to decode int-list to characters +""" diff --git a/services/presence/activity.py b/services/presence/activity.py index 7471b46..e9c3c05 100644 --- a/services/presence/activity.py +++ b/services/presence/activity.py @@ -30,6 +30,9 @@ class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGOb class Activity(DBusGObject): + """Represents a potentially shareable activity on the network. + """ + __gtype_name__ = "Activity" __gsignals__ = { @@ -50,6 +53,15 @@ class Activity(DBusGObject): } def __init__(self, bus_name, object_id, tp, **kwargs): + """Initializes the activity and sets its properties to default values. + + bus_name -- DBUS name for lookup on local host + object_id -- The unique worldwide ID for this activity + tp -- The server plugin object (stands for "telepathy plugin") + kwargs -- Keyword arguments for the GObject properties + + """ + if not bus_name: raise ValueError("DBus bus name must be valid") if not object_id or not isinstance(object_id, int): @@ -88,6 +100,13 @@ class Activity(DBusGObject): tp.update_activity_properties(self._id) def do_get_property(self, pspec): + """Gets the value of a property associated with this activity. + + pspec -- Property specifier + + returns The value of the given property. + """ + if pspec.name == "id": return self._id elif pspec.name == "name": @@ -104,6 +123,15 @@ class Activity(DBusGObject): return self._local def do_set_property(self, pspec, value): + """Sets the value of a property associated with this activity. + + pspec -- Property specifier + value -- Desired value + + Note that the "type" property can be set only once; attempting to set it + to something different later will raise a RuntimeError. + + """ if pspec.name == "id": self._id = value elif pspec.name == "name": @@ -122,6 +150,15 @@ class Activity(DBusGObject): self._update_validity() def _update_validity(self): + """Sends a "validity-changed" signal if this activity's validity has changed. + + Determines whether this activity's status has changed from valid to + invalid, or invalid to valid, and emits a "validity-changed" signal + if either is true. "Valid" means that the object's type, ID, name, + colour and type properties have all been set to something valid + (i.e., not "None"). + + """ try: old_valid = self._valid if self._color and self._name and self._id and self._type: @@ -138,42 +175,80 @@ class Activity(DBusGObject): @dbus.service.signal(_ACTIVITY_INTERFACE, signature="o") def BuddyJoined(self, buddy_path): + """Generates DBUS signal when a buddy joins this activity. + + buddy_path -- DBUS path to buddy object + """ pass @dbus.service.signal(_ACTIVITY_INTERFACE, signature="o") def BuddyLeft(self, buddy_path): + """Generates DBUS signal when a buddy leaves this activity. + + buddy_path -- DBUS path to buddy object + """ pass @dbus.service.signal(_ACTIVITY_INTERFACE, signature="o") def NewChannel(self, channel_path): + """Generates DBUS signal when a new channel is created for this activity. + + channel_path -- DBUS path to new channel + + XXX - what is this supposed to do? Who is supposed to call it? + What is the channel path? Right now this is never called. + + """ pass # dbus methods @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetId(self): + """DBUS method to get this activity's ID + + returns Activity ID + """ return self.props.id @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetColor(self): + """DBUS method to get this activity's colour + + returns Activity colour + """ return self.props.color @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetType(self): + """DBUS method to get this activity's type + + returns Activity type + """ return self.props.type @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="", async_callbacks=('async_cb', 'async_err_cb')) def Join(self, async_cb, async_err_cb): + """DBUS method to for the local user to attempt to join the activity + + async_cb -- Callback method to be called if join attempt is successful + async_err_cb -- Callback method to be called if join attempt is unsuccessful + + """ self.join(async_cb, async_err_cb) @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="ao") def GetJoinedBuddies(self): + """DBUS method to return a list of valid buddies who are joined in this activity + + returns A list of buddy object paths + """ ret = [] for buddy in self._buddies: if buddy.props.valid: @@ -183,18 +258,37 @@ class Activity(DBusGObject): @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="soao") def GetChannels(self): + """DBUS method to get the list of channels associated with this activity + + returns XXX - Not sure what this returns as get_channels doesn't actually return + a list of channels! + """ return self.get_channels() @dbus.service.method(_ACTIVITY_INTERFACE, in_signature="", out_signature="s") def GetName(self): + """DBUS method to get this activity's name + + returns Activity name + """ return self.props.name # methods def object_path(self): + """Retrieves our dbus.ObjectPath object + + returns DBUS ObjectPath object + """ return dbus.ObjectPath(self._object_path) def get_joined_buddies(self): + """Local method to return a list of valid buddies who are joined in this activity + + This method is called by the PresenceService on the local machine. + + returns A list of buddy objects + """ ret = [] for buddy in self._buddies: if buddy.props.valid: @@ -202,18 +296,40 @@ class Activity(DBusGObject): return ret def buddy_joined(self, buddy): + """Adds a buddy to this activity and sends a BuddyJoined signal + + buddy -- Buddy object representing the buddy being added + + Adds a buddy to this activity if the buddy is not already in the buddy list. + If this activity is "valid", a BuddyJoined signal is also sent. + This method is called by the PresenceService on the local machine. + + """ if buddy not in self._buddies: self._buddies.append(buddy) if self.props.valid: self.BuddyJoined(buddy.object_path()) def buddy_left(self, buddy): + """Removes a buddy from this activity and sends a BuddyLeft signal. + + buddy -- Buddy object representing the buddy being removed + + Removes a buddy from this activity if the buddy is in the buddy list. + If this activity is "valid", a BuddyLeft signal is also sent. + This method is called by the PresenceService on the local machine. + + """ if buddy in self._buddies: self._buddies.remove(buddy) if self.props.valid: self.BuddyLeft(buddy.object_path()) def _handle_share_join(self, tp, text_channel): + """Called when a join to a network activity was successful. + + Called by the _shared_cb and _joined_cb methods. + """ if not text_channel: logging.debug("Error sharing: text channel was None, shouldn't happen") raise RuntimeError("Plugin returned invalid text channel") @@ -225,6 +341,8 @@ class Activity(DBusGObject): return True def _shared_cb(self, tp, activity_id, text_channel, exc, userdata): + """XXX - not documented yet + """ if activity_id != self.props.id: # Not for us return @@ -243,6 +361,11 @@ class Activity(DBusGObject): logging.debug("Share of activity %s succeeded." % self._id) def _share(self, (async_cb, async_err_cb), owner): + """XXX - not documented yet + + XXX - This method is called externally by the PresenceService despite the fact + that this is supposed to be an internal method! + """ logging.debug("Starting share of activity %s" % self._id) if self._joined: async_err_cb(RuntimeError("Already shared activity %s" % self.props.id)) @@ -252,6 +375,8 @@ class Activity(DBusGObject): logging.debug("done with share attempt %s" % self._id) def _joined_cb(self, tp, activity_id, text_channel, exc, userdata): + """XXX - not documented yet + """ if activity_id != self.props.id: # Not for us return @@ -266,6 +391,16 @@ class Activity(DBusGObject): async_cb() def join(self, async_cb, async_err_cb): + """Local method for the local user to attempt to join the activity. + + async_cb -- Callback method to be called if join attempt is successful + async_err_cb -- Callback method to be called if join attempt is unsuccessful + + The two callbacks are passed to the server_plugin ("tp") object, which in turn + passes them back as parameters in a callback to the _joined_cb method; this + callback is set up within this method. + + """ if self._joined: async_err_cb(RuntimeError("Already joined activity %s" % self.props.id)) return @@ -273,19 +408,35 @@ class Activity(DBusGObject): self._tp.join_activity(self.props.id, (sigid, async_cb, async_err_cb)) def get_channels(self): + """Local method to get the list of channels associated with this activity + + returns XXX - expected a list of channels, instead returning a tuple? ??? + """ conn = self._tp.get_connection() # FIXME add tubes and others channels return str(conn.service_name), conn.object_path, [self._text_channel.object_path] def leave(self): + """Local method called when the user wants to leave the activity. + + (XXX - doesn't appear to be called anywhere!) + + """ if self._joined: self._text_channel[CHANNEL_INTERFACE].Close() def _text_channel_closed_cb(self): + """Callback method called when the text channel is closed. + + This callback is set up in the _handle_share_join method. + """ self._joined = False self._text_channel = None def send_properties(self): + """Tells the Telepathy server what the properties of this activity are. + + """ props = {} props['name'] = self._name props['color'] = self._color @@ -293,6 +444,17 @@ class Activity(DBusGObject): self._tp.set_activity_properties(self.props.id, props) def set_properties(self, properties): + """Sets name, colour and/or type properties for this activity all at once. + + properties - Dictionary object containing properties keyed by property names + + Note that if any of the name, colour and/or type property values is changed from + what it originally was, the update_validity method will be called, resulting in + a "validity-changed" signal being generated. (Also note that unlike with the + do_set_property method, it *is* possible to change an already-set activity type + to something else; this may be a bug.) Called by the PresenceService on the + local machine. + """ changed = False if "name" in properties.keys(): name = properties["name"] 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(): diff --git a/services/presence/presenceservice.py b/services/presence/presenceservice.py index fd4cbb6..1312fec 100644 --- a/services/presence/presenceservice.py +++ b/services/presence/presenceservice.py @@ -40,24 +40,36 @@ class NotFoundError(dbus.DBusException): dbus.DBusException.__init__(self) self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound' +class DBusGObjectMetaclass(dbus.service.InterfaceType, gobject.GObjectMeta): pass +class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGObjectMetaclass + +class PresenceService(DBusGObject): + __gtype_name__ = "PresenceService" + + __gsignals__ = { + 'connection-status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_BOOLEAN])) + } -class PresenceService(dbus.service.Object): def __init__(self, test=0): self._next_object_id = 0 + self._connected = False self._buddies = {} # key -> Buddy self._handles_buddies = {} # tp client -> (handle -> Buddy) self._activities = {} # activity id -> Activity + gobject.GObject.__init__(self) + bus = dbus.SessionBus() self._bus_name = dbus.service.BusName(_PRESENCE_SERVICE, bus=bus) # Create the Owner object objid = self._get_next_object_id() if test > 0: - self._owner = TestOwner(self._bus_name, objid, test) + self._owner = TestOwner(self, self._bus_name, objid, test) else: - self._owner = ShellOwner(self._bus_name, objid) + self._owner = ShellOwner(self, self._bus_name, objid) self._buddies[self._owner.props.key] = self._owner self._registry = ManagerRegistry() @@ -92,8 +104,15 @@ class PresenceService(dbus.service.Object): async_err_cb(exc) def _server_status_cb(self, plugin, status, reason): + # FIXME: figure out connection status when we have a salut plugin too + old_status = self._connected if status == CONNECTION_STATUS_CONNECTED: - pass + self._connected = True + else: + self._connected = False + + if self._connected != old_status: + self.emit('connection-status', self._connected) def _contact_online(self, tp, handle, props): new_buddy = False @@ -120,8 +139,11 @@ class PresenceService(dbus.service.Object): else: self.BuddyDisappeared(buddy.object_path()) logging.debug("Buddy left: %s (%s)" % (buddy.props.nick, buddy.props.color)) - + def _contact_offline(self, tp, handle): + if not self._handles_buddies[tp].has_key(handle): + return + buddy = self._handles_buddies[tp].pop(handle) key = buddy.props.key @@ -144,11 +166,11 @@ class PresenceService(dbus.service.Object): logging.debug("Buddy %s icon updated" % buddy.props.nick) buddy.props.icon = avatar - def _buddy_properties_changed(self, tp, handle, prop): + def _buddy_properties_changed(self, tp, handle, properties): buddy = self._handles_buddies[tp].get(handle) if buddy: - buddy.set_properties(prop) - logging.debug("Buddy %s properties updated" % buddy.props.nick) + buddy.set_properties(properties) + logging.debug("Buddy %s properties updated: %s" % (buddy.props.nick, properties.keys())) def _new_activity(self, activity_id, tp): try: @@ -257,11 +279,10 @@ class PresenceService(dbus.service.Object): @dbus.service.method(_PRESENCE_INTERFACE, in_signature="s", out_signature="o") def GetActivityById(self, actid): - if self._activities.has_key(actid): - act = self._activities[actid] - if act.props.valid: - return act.object_path() - raise NotFoundError("The activity was not found.") + act = self.internal_get_activity(actid) + if not act or not act.props.valid: + raise NotFoundError("The activity was not found.") + return act.object_path() @dbus.service.method(_PRESENCE_INTERFACE, out_signature="ao") def GetBuddies(self): @@ -330,6 +351,11 @@ class PresenceService(dbus.service.Object): if activity: activity.set_properties(props) + def internal_get_activity(self, actid): + if not self._activities.has_key(actid): + return None + return self._activities[actid] + def main(test=False): loop = gobject.MainLoop() diff --git a/services/presence/psutils.py b/services/presence/psutils.py index 995ea3a..76583d6 100644 --- a/services/presence/psutils.py +++ b/services/presence/psutils.py @@ -16,7 +16,11 @@ def bytes_to_string(bytes): - # Handle both DBus byte arrays and strings + """The function converts a D-BUS byte array provided by dbus to string format. + + bytes -- a D-Bus array of bytes. Handle both DBus byte arrays and strings + + """ try: # DBus Byte array ret = ''.join([chr(item) for item in bytes]) diff --git a/services/presence/server_plugin.py b/services/presence/server_plugin.py index 5180692..5330567 100644 --- a/services/presence/server_plugin.py +++ b/services/presence/server_plugin.py @@ -1,3 +1,4 @@ +"""Telepathy-python presence server interface/implementation plugin""" # Copyright (C) 2007, Red Hat, Inc. # Copyright (C) 2007, Collabora Ltd. # @@ -41,7 +42,7 @@ CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' _PROTOCOL = "jabber" class InvalidBuddyError(Exception): - pass + """(Unused) exception to indicate an invalid buddy specifier""" def _buddy_icon_save_cb(buf, data): data[0] += buf @@ -76,6 +77,13 @@ def _get_buddy_icon_at_size(icon, maxw, maxh, maxsize): class ServerPlugin(gobject.GObject): + """Telepathy-python-based presence server interface + + The ServerPlugin instance translates network events from + Telepathy Python into GObject events. It provides direct + python calls to perform the required network operations + to implement the PresenceService. + """ __gsignals__ = { 'contact-online': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), @@ -104,6 +112,14 @@ class ServerPlugin(gobject.GObject): } def __init__(self, registry, owner): + """Initialize the ServerPlugin instance + + registry -- telepathy.client.ManagerRegistry from the + PresenceService, used to find the "gabble" connection + manager in this case... + owner -- presence.buddy.GenericOwner instance (normally a + presence.buddy.ShellOwner instance) + """ gobject.GObject.__init__(self) self._icon_cache = BuddyIconCache() @@ -125,6 +141,20 @@ class ServerPlugin(gobject.GObject): self._reconnect_id = 0 def _owner_property_changed_cb(self, owner, properties): + """Local user's configuration properties have changed + + owner -- the Buddy object for the local user + properties -- set of updated properties + + calls: + + _set_self_current_activity current-activity + _set_self_alias nick + _set_self_olpc_properties color + + depending on which properties are present in the + set of properties. + """ logging.debug("Owner properties changed: %s" % properties) if properties.has_key("current-activity"): @@ -137,10 +167,21 @@ class ServerPlugin(gobject.GObject): self._set_self_olpc_properties() def _owner_icon_changed_cb(self, owner, icon): + """Owner has changed their icon, forward to network""" logging.debug("Owner icon changed to size %d" % len(str(icon))) self._set_self_avatar(icon) def _get_account_info(self): + """Retrieve metadata dictionary describing this account + + returns dictionary with: + + server : server url from owner + account : printable-ssh-key-hash@server + password : ssh-key-hash + register : whether to register (i.e. whether not yet + registered) + """ account_info = {} account_info['server'] = self._owner.get_server() @@ -155,6 +196,16 @@ class ServerPlugin(gobject.GObject): return account_info def _find_existing_connection(self): + """Try to find an existing Telepathy connection to this server + + filters the set of connections from + telepathy.client.Connection.get_connections + to find a connection using our protocol with the + "self handle" of that connection being a handle + which matches our account (see _get_account_info) + + returns connection or None + """ our_name = self._account['account'] # Search existing connections, if any, that we might be able to use @@ -173,9 +224,20 @@ class ServerPlugin(gobject.GObject): return None def get_connection(self): + """Retrieve our telepathy.client.Connection object""" return self._conn def _init_connection(self): + """Set up our connection + + if there is no existing connection + (_find_existing_connection returns None) + produce a new connection with our protocol for our + account. + + if there is an existing connection, reuse it by + registering for various of events on it. + """ conn = self._find_existing_connection() if not conn: acct = self._account.copy() @@ -201,6 +263,10 @@ class ServerPlugin(gobject.GObject): return conn def _request_list_channel(self, name): + """Request a contact-list channel from Telepathy + + name -- publish/subscribe, for the type of channel + """ handle = self._conn[CONN_INTERFACE].RequestHandles( CONNECTION_HANDLE_TYPE_LIST, [name])[0] chan_path = self._conn[CONN_INTERFACE].RequestChannel( @@ -212,6 +278,9 @@ class ServerPlugin(gobject.GObject): return channel def _connected_cb(self): + """Callback on successful connection to a server + """ + if self._account['register']: # we successfully register this account self._owner.props.registered = True @@ -257,7 +326,7 @@ class ServerPlugin(gobject.GObject): self._activity_properties_changed_cb) # Set initial buddy properties, avatar, and activities - self._set_self_olpc_properties(True) + self._set_self_olpc_properties() self._set_self_alias() self._set_self_activities() self._set_self_current_activity() @@ -324,34 +393,58 @@ class ServerPlugin(gobject.GObject): def _internal_join_activity(self, activity_id, signal, userdata): handle = self._activities.get(activity_id) if not handle: - self._conn[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_ROOM, [activity_id], + # FIXME: figure out why the server can't figure this out itself + room_jid = activity_id + "@conference." + self._account["server"] + self._conn[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_ROOM, [room_jid], reply_handler=lambda *args: self._join_activity_get_channel_cb(activity_id, signal, userdata, *args), error_handler=lambda *args: self._join_error_cb(activity_id, signal, userdata, *args)) else: self._join_activity_get_channel_cb(activity_id, userdata, [handle]) def share_activity(self, activity_id, userdata): + """Share activity with the network + + activity_id -- unique ID for the activity + userdata -- opaque token to be passed in the resulting event + (id, callback, errback) normally + + Asks the Telepathy server to create a "conference" channel + for the activity or return a handle to an already created + conference channel for the activity. + """ self._internal_join_activity(activity_id, "activity-shared", userdata) def join_activity(self, activity_id, userdata): + """Join an activity on the network (or locally) + + activity_id -- unique ID for the activity + userdata -- opaque token to be passed in the resulting event + (id, callback, errback) normally + + Asks the Telepathy server to create a "conference" channel + for the activity or return a handle to an already created + conference channel for the activity. + """ self._internal_join_activity(activity_id, "activity-joined", userdata) def _ignore_success_cb(self): - pass + """Ignore an event (null-operation)""" def _log_error_cb(self, msg, err): + """Log a message (error) at debug level with prefix msg""" logging.debug("Error %s: %s" % (msg, err)) - def _set_self_olpc_properties(self, set_key=False): + def _set_self_olpc_properties(self): + """Set color and key on our Telepathy server identity""" props = {} props['color'] = self._owner.props.color - if set_key: - props['key'] = dbus.ByteArray(self._owner.props.key) + props['key'] = dbus.ByteArray(self._owner.props.key) self._conn[CONN_INTERFACE_BUDDY_INFO].SetProperties(props, reply_handler=self._ignore_success_cb, error_handler=lambda *args: self._log_error_cb("setting properties", *args)) def _set_self_alias(self): + """Forwarded to SetActivities on AliasInfo channel""" alias = self._owner.props.nick self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() self._conn[CONN_INTERFACE_ALIASING].SetAliases({self_handle : alias}, @@ -359,11 +452,19 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("setting alias", *args)) def _set_self_activities(self): + """Forward set of joined activities to network + + uses SetActivities on BuddyInfo channel + """ self._conn[CONN_INTERFACE_BUDDY_INFO].SetActivities(self._joined_activities, reply_handler=self._ignore_success_cb, error_handler=lambda *args: self._log_error_cb("setting activities", *args)) def _set_self_current_activity(self): + """Forward our current activity (or "") to network + + uses SetCurrentActivity on BuddyInfo channel + """ cur_activity = self._owner.props.current_activity cur_activity_handle = 0 if not cur_activity: @@ -381,12 +482,20 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("setting current activity", *args)) def _get_handle_for_activity(self, activity_id): + """Retrieve current handle for given activity or None""" for (act, handle) in self._joined_activities: if activity_id == act: return handle return None def _status_changed_cb(self, state, reason): + """Handle notification of connection-status change + + state -- CONNECTION_STATUS_* + reason -- integer code describing the reason... + + returns False XXX what does that mean? + """ if state == CONNECTION_STATUS_CONNECTING: logging.debug("State: connecting...") elif state == CONNECTION_STATUS_CONNECTED: @@ -403,6 +512,16 @@ class ServerPlugin(gobject.GObject): return False def start(self): + """Start up the Telepathy networking connections + + if we are already connected, query for the initial contact + information. + + if we are already connecting, do nothing + + otherwise initiate a connection and transfer control to + _connect_reply_cb or _connect_error_cb + """ logging.debug("Starting up...") # If the connection is already connected query initial contacts conn_status = self._conn[CONN_INTERFACE].GetStatus() @@ -418,25 +537,30 @@ class ServerPlugin(gobject.GObject): error_handler=self._connect_error_cb) def _connect_reply_cb(self): + """Handle connection success""" if self._reconnect_id > 0: gobject.source_remove(self._reconnect_id) def _reconnect(self): + """Reset number-of-attempted connections and re-attempt""" self._reconnect_id = 0 self.start() return False def _connect_error_cb(self, exception): + """Handle connection failure""" logging.debug("Connect error: %s" % exception) if not self._reconnect_id: self._reconnect_id = gobject.timeout_add(10000, self._reconnect) def cleanup(self): + """If we still have a connection, disconnect it""" if not self._conn: return self._conn[CONN_INTERFACE].Disconnect() def _contact_offline(self, handle): + """Handle contact going offline (send message, update set)""" if not self._online_contacts.has_key(handle): return if self._online_contacts[handle]: @@ -444,13 +568,16 @@ class ServerPlugin(gobject.GObject): del self._online_contacts[handle] def _contact_online_activities_cb(self, handle, activities): + """Handle contact's activity list update""" self._buddy_activities_changed_cb(handle, activities) def _contact_online_activities_error_cb(self, handle, err): + """Handle contact's activity list being unavailable""" logging.debug("Handle %s - Error getting activities: %s" % (handle, err)) self._contact_offline(handle) def _contact_online_aliases_cb(self, handle, props, aliases): + """Handle contact's alias being received (do further queries)""" if not aliases or not len(aliases): logging.debug("Handle %s - No aliases" % handle) self._contact_offline(handle) @@ -466,10 +593,12 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._contact_online_activities_error_cb(handle, *args)) def _contact_online_aliases_error_cb(self, handle, err): + """Handle failure to retrieve given user's alias/information""" logging.debug("Handle %s - Error getting nickname: %s" % (handle, err)) self._contact_offline(handle) def _contact_online_properties_cb(self, handle, props): + """Handle failure to retrieve given user's alias/information""" if not props.has_key('key'): logging.debug("Handle %s - invalid key." % handle) self._contact_offline(handle) @@ -487,16 +616,19 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._contact_online_aliases_error_cb(handle, *args)) def _contact_online_properties_error_cb(self, handle, err): + """Handle error retrieving property-set for a user (handle)""" logging.debug("Handle %s - Error getting properties: %s" % (handle, err)) self._contact_offline(handle) def _contact_online(self, handle): + """Handle a contact coming online""" self._online_contacts[handle] = None self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties(handle, reply_handler=lambda *args: self._contact_online_properties_cb(handle, *args), error_handler=lambda *args: self._contact_online_properties_error_cb(handle, *args)) def _presence_update_cb(self, presence): + """Send update for online/offline status of presence""" for handle in presence: timestamp, statuses = presence[handle] online = handle in self._online_contacts @@ -511,18 +643,19 @@ class ServerPlugin(gobject.GObject): self._contact_offline(handle) def _avatar_updated_cb(self, handle, new_avatar_token): + """Handle update of given user (handle)'s avatar""" if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner property changes since those # are handled locally return if not self._online_contacts.has_key(handle): - logging.debug("Handle %s not valid yet...") + logging.debug("Handle %s unknown." % handle) return jid = self._online_contacts[handle] if not jid: - logging.debug("Handle %s not valid yet...") + logging.debug("Handle %s not valid yet..." % handle) return icon = self._icon_cache.get_icon(jid, new_avatar_token) @@ -535,6 +668,7 @@ class ServerPlugin(gobject.GObject): self.emit("avatar-updated", handle, icon) def _alias_changed_cb(self, aliases): + """Handle update of aliases for all users""" for handle, alias in aliases: prop = {'nick': alias} #print "Buddy %s alias changed to %s" % (handle, alias) @@ -542,14 +676,16 @@ class ServerPlugin(gobject.GObject): self._buddy_properties_changed_cb(handle, prop) def _buddy_properties_changed_cb(self, handle, properties): + """Handle update of given user (handle)'s properties""" if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner property changes since those # are handled locally return if self._online_contacts.has_key(handle) and self._online_contacts[handle]: - self.emit("buddy-properties-changed", handle, properties) + self.emit("buddy-properties-changed", handle, properties) def _buddy_activities_changed_cb(self, handle, activities): + """Handle update of given user (handle)'s activities""" if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner activity changes since those # are handled locally @@ -563,6 +699,8 @@ class ServerPlugin(gobject.GObject): self.emit("buddy-activities-changed", handle, activities_id) def _buddy_current_activity_changed_cb(self, handle, activity, channel): + """Handle update of given user (handle)'s current activity""" + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner current activity changes since those # are handled locally @@ -577,6 +715,8 @@ class ServerPlugin(gobject.GObject): self._buddy_properties_changed_cb(handle, prop) def _new_channel_cb(self, object_path, channel_type, handle_type, handle, suppress_handler): + """Handle creation of a new channel + """ if handle_type == CONNECTION_HANDLE_TYPE_ROOM and channel_type == CHANNEL_TYPE_TEXT: channel = Channel(self._conn._dbus_object._named_service, object_path) @@ -595,6 +735,7 @@ class ServerPlugin(gobject.GObject): self.emit("private-invitation", object_path) def update_activity_properties(self, act_id): + """Request update from network on the activity properties of act_id""" handle = self._activities.get(act_id) if not handle: raise RuntimeError("Unknown activity %s: couldn't find handle.") @@ -604,6 +745,7 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("getting activity properties", *args)) def set_activity_properties(self, act_id, props): + """Send update to network on the activity properties of act_id (props)""" handle = self._activities.get(act_id) if not handle: raise RuntimeError("Unknown activity %s: couldn't find handle.") @@ -613,6 +755,7 @@ class ServerPlugin(gobject.GObject): error_handler=lambda *args: self._log_error_cb("setting activity properties", *args)) def _activity_properties_changed_cb(self, room, properties): + """Handle update of properties for a "room" (activity handle)""" for act_id, act_handle in self._activities.items(): if room == act_handle: self.emit("activity-properties-changed", act_id, properties) diff --git a/services/presence/sugar-presence-service b/services/presence/sugar-presence-service index bba32b5..7eec696 100755 --- a/services/presence/sugar-presence-service +++ b/services/presence/sugar-presence-service @@ -24,7 +24,7 @@ import os from sugar import logger from sugar import env -sys.path.insert(0, env.get_service_path('presence')) +sys.path.append(env.get_service_path('presence')) test=0 if len(sys.argv) > 1: |