diff options
author | Simon McVittie <simon.mcvittie@collabora.co.uk> | 2007-06-06 15:20:00 (GMT) |
---|---|---|
committer | Simon McVittie <simon.mcvittie@collabora.co.uk> | 2007-06-06 15:20:00 (GMT) |
commit | 72ffc806b07d6bd0d3ba42d6dc2f55b798fc93af (patch) | |
tree | 80f348b2f8682116d958c38019edb70cbe6de5fb | |
parent | 947cd5b251f9f67fc6c48614a3be871a1514a7fb (diff) |
Copy sugar services/presence/ to presence-service src/
-rw-r--r-- | src/Makefile.am | 31 | ||||
-rw-r--r-- | src/__init__.py | 36 | ||||
-rw-r--r-- | src/activity.py | 715 | ||||
-rw-r--r-- | src/buddy.py | 638 | ||||
-rw-r--r-- | src/buddyiconcache.py | 105 | ||||
-rw-r--r-- | src/linklocal_plugin.py | 27 | ||||
-rw-r--r-- | src/org.laptop.Sugar.Presence.service.in | 4 | ||||
-rw-r--r-- | src/presenceservice.py | 517 | ||||
-rw-r--r-- | src/pstest.py | 317 | ||||
-rw-r--r-- | src/psutils.py | 259 | ||||
-rw-r--r-- | src/server_plugin.py | 1171 | ||||
-rwxr-xr-x | src/sugar-presence-service | 63 | ||||
-rw-r--r-- | src/test_psutils.py | 12 |
13 files changed, 3895 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..44b2be7 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,31 @@ +servicedir = $(datadir)/dbus-1/services +service_in_files = org.laptop.Sugar.Presence.service.in +service_DATA = $(service_in_files:.service.in=.service) + +$(service_DATA): $(service_in_files) Makefile + @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ + +sugardir = $(pkgdatadir)/services/presence +sugar_PYTHON = \ + __init__.py \ + activity.py \ + buddy.py \ + buddyiconcache.py \ + linklocal_plugin.py \ + presenceservice.py \ + pstest.py \ + psutils.py \ + server_plugin.py + +dist_bin_SCRIPTS = sugar-presence-service + +DISTCLEANFILES = $(service_DATA) + +EXTRA_DIST = $(service_in_files) + +dist_check_SCRIPTS = test_psutils.py + +TESTS_ENVIRONMENT = \ + PYTHONPATH=$(top_srcdir):$(top_srcdir)/services/presence \ + $(PYTHON) +TESTS = $(dist_check_SCRIPTS) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..bd64375 --- /dev/null +++ b/src/__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/src/activity.py b/src/activity.py new file mode 100644 index 0000000..2eb21f6 --- /dev/null +++ b/src/activity.py @@ -0,0 +1,715 @@ +# 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 gobject +import dbus +import dbus.service +from dbus.gobject_service import ExportedGObject +from sugar import util +import logging + +from telepathy.constants import CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES +from telepathy.interfaces import (CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP) + +_ACTIVITY_PATH = "/org/laptop/Sugar/Presence/Activities/" +_ACTIVITY_INTERFACE = "org.laptop.Sugar.Presence.Activity" + +_PROP_ID = "id" +_PROP_NAME = "name" +_PROP_COLOR = "color" +_PROP_TYPE = "type" +_PROP_VALID = "valid" +_PROP_LOCAL = "local" +_PROP_JOINED = "joined" +_PROP_CUSTOM_PROPS = "custom-props" + +_logger = logging.getLogger('s-p-s.activity') + +class Activity(ExportedGObject): + """Represents a shared activity seen on the network, or a local activity + that has been, or will be, shared onto the network. + + The activity might be public, restricted to a group, or invite-only. + """ + + __gtype_name__ = "Activity" + + __gsignals__ = { + 'validity-changed': + # The activity's validity has changed. + # An activity is valid if its name, color, type and ID have been + # set. + # Arguments: + # validity: bool + (gobject.SIGNAL_RUN_FIRST, None, [bool]), + 'disappeared': + # Nobody is in this activity any more. + # No arguments. + (gobject.SIGNAL_RUN_FIRST, None, []), + } + + __gproperties__ = { + _PROP_ID : (str, None, None, None, + gobject.PARAM_READWRITE | + gobject.PARAM_CONSTRUCT_ONLY), + _PROP_NAME : (str, None, None, None, gobject.PARAM_READWRITE), + _PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE), + _PROP_TYPE : (str, None, None, None, gobject.PARAM_READWRITE), + _PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE), + _PROP_LOCAL : (bool, None, None, False, + gobject.PARAM_READWRITE | + gobject.PARAM_CONSTRUCT_ONLY), + _PROP_JOINED : (bool, None, None, False, gobject.PARAM_READABLE), + _PROP_CUSTOM_PROPS : (object, None, None, + gobject.PARAM_READWRITE | + gobject.PARAM_CONSTRUCT_ONLY) + } + + _RESERVED_PROPNAMES = __gproperties__.keys() + + def __init__(self, bus, object_id, ps, tp, **kwargs): + """Initializes the activity and sets its properties to default values. + + :Parameters: + `bus` : dbus.bus.BusConnection + A connection to the D-Bus session bus + `object_id` : int + PS ID for this activity, used to construct the object-path + `ps` : presenceservice.PresenceService + The presence service + `tp` : server plugin + The server plugin object (stands for "telepathy plugin") + :Keywords: + `id` : str + The globally unique activity ID (required) + `name` : str + Human-readable title for the activity + `color` : str + Activity color in #RRGGBB,#RRGGBB (stroke,fill) format + `type` : str + D-Bus service name representing the activity type + `local : bool + If True, this activity was initiated locally and is not + (yet) advertised on the network + (FIXME: is this description right?) + `custom-props` : dict + Activity-specific properties + """ + + if not object_id or not isinstance(object_id, int): + raise ValueError("object id must be a valid number") + if not tp: + raise ValueError("telepathy CM must be valid") + + self._ps = ps + self._object_id = object_id + self._object_path = dbus.ObjectPath(_ACTIVITY_PATH + + str(self._object_id)) + + self._buddies = set() + self._member_handles = set() + self._joined = False + + # the telepathy client + self._tp = tp + self._self_handle = None + self._text_channel = None + self._text_channel_group_flags = 0 + + self._valid = False + self._id = None + self._actname = None + self._color = None + self._local = False + self._type = None + self._custom_props = {} + + # ensure no reserved property names are in custom properties + cprops = kwargs.get(_PROP_CUSTOM_PROPS) + if cprops is not None: + (rprops, cprops) = self._split_properties(cprops) + if len(rprops.keys()) > 0: + raise ValueError("Cannot use reserved property names '%s'" + % ", ".join(rprops.keys())) + + if not kwargs.get(_PROP_ID): + raise ValueError("activity id is required") + if not util.validate_activity_id(kwargs[_PROP_ID]): + raise ValueError("Invalid activity id '%s'" % kwargs[_PROP_ID]) + + ExportedGObject.__init__(self, bus, self._object_path, + gobject_properties=kwargs) + if self.props.local and not self.props.valid: + raise RuntimeError("local activities require color, type, and " + "name") + + # If not yet valid, query activity properties + if not self.props.valid: + 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 == _PROP_ID: + return self._id + elif pspec.name == _PROP_NAME: + return self._actname + elif pspec.name == _PROP_COLOR: + return self._color + elif pspec.name == _PROP_TYPE: + return self._type + elif pspec.name == _PROP_VALID: + return self._valid + elif pspec.name == _PROP_JOINED: + return self._joined + elif pspec.name == _PROP_LOCAL: + 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 == _PROP_ID: + if self._id: + raise RuntimeError("activity ID is already set") + self._id = value + elif pspec.name == _PROP_NAME: + self._actname = value + elif pspec.name == _PROP_COLOR: + self._color = value + elif pspec.name == _PROP_TYPE: + if self._type: + raise RuntimeError("activity type is already set") + self._type = value + elif pspec.name == _PROP_JOINED: + self._joined = value + elif pspec.name == _PROP_LOCAL: + self._local = value + elif pspec.name == _PROP_CUSTOM_PROPS: + if not value: + value = {} + (rprops, cprops) = self._split_properties(value) + self._custom_props = {} + for (key, dvalue) in cprops.items(): + self._custom_props[str(key)] = str(dvalue) + + 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._actname and self._id and self._type: + self._valid = True + else: + self._valid = False + + if old_valid != self._valid: + self.emit("validity-changed", self._valid) + except AttributeError: + self._valid = False + + # dbus signals + @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 (randomly generated) unique ID + + :Returns: Activity ID as a string + """ + 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 as a string in the format #RRGGBB,#RRGGBB + """ + 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 as a string, in the same form as a D-Bus + well-known name + """ + 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 corresponding to those buddies + in this activity who are 'valid' (i.e. for whom we have complete + information) + """ + ret = [] + for buddy in self._buddies: + if buddy.props.valid: + ret.append(buddy.object_path()) + return ret + + @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: + a tuple containing: + - the D-Bus well-known service name of the connection + (FIXME: this is redundant; in Telepathy it can be derived + from that of the connection) + - the D-Bus object path of the connection + - a list of D-Bus object paths representing the channels + associated with this activity + """ + 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 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: + ret.append(buddy) + return ret + + def buddy_apparently_joined(self, buddy): + """Adds a buddy to this activity and sends a BuddyJoined signal, + unless we can already see who's in the activity by being in it + ourselves. + + 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 not self._joined: + self._add_buddies((buddy,)) + + def _add_buddies(self, buddies): + buddies = set(buddies) + + # disregard any who are already there + buddies -= self._buddies + + self._buddies |= buddies + + for buddy in buddies: + buddy.add_activity(self) + if self.props.valid: + self.BuddyJoined(buddy.object_path()) + + def _remove_buddies(self, buddies): + buddies = set(buddies) + + # disregard any who are not already there + buddies &= self._buddies + + self._buddies -= buddies + + for buddy in buddies: + buddy.remove_activity(self) + if self.props.valid: + self.BuddyJoined(buddy.object_path()) + + if not self._buddies: + self.emit('disappeared') + + def buddy_apparently_left(self, buddy): + """Removes a buddy from this activity and sends a BuddyLeft signal, + unless we can already see who's in the activity by being in it + ourselves. + + 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 not self._joined: + self._remove_buddies((buddy,)) + + def _text_channel_group_flags_changed_cb(self, flags): + self._text_channel_group_flags = flags + + 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: + _logger.debug("Error sharing: text channel was None, shouldn't " + "happen") + raise RuntimeError("Plugin returned invalid text channel") + + self._text_channel = text_channel + self._text_channel[CHANNEL_INTERFACE].connect_to_signal('Closed', + self._text_channel_closed_cb) + if CHANNEL_INTERFACE_GROUP in self._text_channel: + group = self._text_channel[CHANNEL_INTERFACE_GROUP] + + # FIXME: make these method calls async? + + group.connect_to_signal('GroupFlagsChanged', + self._text_channel_group_flags_changed_cb) + self._text_channel_group_flags = group.GetGroupFlags() + + self._self_handle = group.GetSelfHandle() + + # by the time we hook this, we need to know the group flags + group.connect_to_signal('MembersChanged', + self._text_channel_members_changed_cb) + # bootstrap by getting the current state. This is where we find + # out whether anyone was lying to us in their PEP info + members = set(group.GetMembers()) + added = members - self._member_handles + removed = self._member_handles - members + if added or removed: + self._text_channel_members_changed_cb('', added, removed, + (), (), 0, 0) + + # if we can see any member handles, we're probably able to see + # all members, so can stop caring about PEP announcements for this + # activity + self._joined = (self._self_handle in self._member_handles) + else: + self._joined = True + + 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 + + (sigid, owner, async_cb, async_err_cb) = userdata + self._tp.disconnect(sigid) + + if exc: + _logger.debug("Share of activity %s failed: %s" % (self._id, exc)) + async_err_cb(exc) + else: + self._handle_share_join(tp, text_channel) + self.send_properties() + owner.add_activity(self) + async_cb(dbus.ObjectPath(self._object_path)) + _logger.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! + """ + _logger.debug("Starting share of activity %s" % self._id) + if self._joined: + async_err_cb(RuntimeError("Already shared activity %s" + % self.props.id)) + return + sigid = self._tp.connect('activity-shared', self._shared_cb) + self._tp.share_activity(self.props.id, (sigid, owner, async_cb, + async_err_cb)) + _logger.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 + + (sigid, async_cb, async_err_cb) = userdata + self._tp.disconnect(sigid) + + if exc: + async_err_cb(exc) + else: + self._handle_share_join(tp, text_channel) + 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 + sigid = self._tp.connect('activity-joined', self._joined_cb) + 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_members_changed_cb(self, message, added, removed, + local_pending, remote_pending, + actor, reason): + # Note: D-Bus calls this with list arguments, but after GetMembers() + # we call it with set and tuple arguments; we cope with any iterable. + + if (self._text_channel_group_flags & + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES): + map_chan = self._text_channel + else: + # we have global handles here + map_chan = None + + # disregard any who are already there + added = set(added) + added -= self._member_handles + self._member_handles |= added + + # for added people, we need a Buddy object + added_buddies = self._ps.map_handles_to_buddies(self._tp, + map_chan, + added) + self._add_buddies(added_buddies.itervalues()) + + # we treat all pending members as if they weren't there + removed = set(removed) + removed |= set(local_pending) + removed |= set(remote_pending) + # disregard any who aren't already there + removed &= self._member_handles + self._member_handles -= removed + + # for removed people, don't bother creating a Buddy just so we can + # say it left. If we don't already have a Buddy object for someone, + # then obviously they're not in self._buddies! + removed_buddies = self._ps.map_handles_to_buddies(self._tp, + map_chan, + removed, + create=False) + self._remove_buddies(removed_buddies.itervalues()) + + # if we were among those removed, we'll have to start believing + # the spoofable PEP-based activity tracking again. + if self._self_handle not in self._member_handles: + self._joined = False + + 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._self_handle = None + self._text_channel = None + + def send_properties(self): + """Tells the Telepathy server what the properties of this activity are. + + """ + props = {} + props['name'] = self._actname + props['color'] = self._color + props['type'] = self._type + + # Add custom properties + for (key, value) in self._custom_props.items(): + props[key] = value + + 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. + Called by the PresenceService on the local machine. + """ + changed = False + # split reserved properties from activity-custom properties + (rprops, cprops) = self._split_properties(properties) + if _PROP_NAME in rprops.keys(): + name = rprops[_PROP_NAME] + if name != self._actname: + self._actname = name + changed = True + + if _PROP_COLOR in rprops.keys(): + color = rprops[_PROP_COLOR] + if color != self._color: + self._color = color + changed = True + + if _PROP_TYPE in rprops.keys(): + type = rprops[_PROP_TYPE] + if type != self._type: + # Type can never be changed after first set + if self._type: + _logger.debug("Activity type changed by network; this " + "is illegal") + else: + self._type = type + changed = True + + # Set custom properties + if len(cprops.keys()) > 0: + self.props.custom_props = cprops + + if changed: + self._update_validity() + + def _split_properties(self, properties): + """Extracts reserved properties. + + properties - Dictionary object containing properties keyed by + property names + + returns a tuple of 2 dictionaries, reserved properties and custom + properties + """ + rprops = {} + cprops = {} + for (key, value) in properties.items(): + if key in self._RESERVED_PROPNAMES: + rprops[key] = value + else: + cprops[key] = value + return (rprops, cprops) diff --git a/src/buddy.py b/src/buddy.py new file mode 100644 index 0000000..82a9b44 --- /dev/null +++ b/src/buddy.py @@ -0,0 +1,638 @@ +"""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, + 'keyid/' + 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) diff --git a/src/buddyiconcache.py b/src/buddyiconcache.py new file mode 100644 index 0000000..9d355bb --- /dev/null +++ b/src/buddyiconcache.py @@ -0,0 +1,105 @@ +# 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 + +from sugar import env + +import os.path +import cPickle + +class BuddyIconCache(object): + """Caches icons on disk and finds them based on the jid of their owners.""" + def __init__(self): + ppath = env.get_profile_path() + self._cachepath = os.path.join(ppath, "cache", "buddy-icons", "cache") + + # Ensure cache directory exists + if not os.path.exists(os.path.dirname(self._cachepath)): + os.makedirs(os.path.dirname(self._cachepath)) + + if not os.path.exists(self._cachepath): + self._cache = {} + # md5 and server token of the last avatar uploaded + self._md5 = '' + self._token = '' + else: + self._load_cache() + + def _load_cache(self): + try: + self._cache, self._md5, self._token = cPickle.load(open(self._cachepath, "r")) + except: + self._cache, self._md5, self._token = {}, '', '' + + def _save_cache(self): + out = open(self._cachepath, "w") + cPickle.dump((self._cache, self._md5, self._token), out, protocol=2) + + def get_icon(self, jid, token): + hit = self._cache.get(jid) + + if hit: + t, icon = hit[0], hit[1] + if t == token: + return icon + + return None + + def store_icon(self, jid, token, data): + self._cache[jid] = (token, data) + self._save_cache() + + def check_avatar(self, md5, token): + return self._md5 == md5 and self._token == token + + def set_avatar(self, md5, token): + self._md5 = md5 + self._token = token + self._save_cache() + +if __name__ == "__main__": + my_cache = BuddyIconCache() + + # look for the icon in the cache + icon = my_cache.get_icon("test@olpc.collabora.co.uk", "aaaa") + print icon + + my_cache.store_icon("test@olpc.collabora.co.uk", "aaaa", "icon1") + + # now we're sure that the icon is in the cache + icon = my_cache.get_icon("test@olpc.collabora.co.uk", "aaaa") + print icon + + # new icon + my_cache.store_icon("test@olpc.collabora.co.uk", "bbbb", "icon2") + + # the icon in the cache is not valid now + icon = my_cache.get_icon("test@olpc.collabora.co.uk", "aaaa") + print icon + + + my_avatar_md5 = "111" + my_avatar_token = "222" + + if not my_cache.check_avatar(my_avatar_md5, my_avatar_token): + # upload of the new avatar + print "upload of the new avatar" + my_cache.set_avatar(my_avatar_md5, my_avatar_token) + else: + print "No need to upload the new avatar" + + if my_cache.check_avatar(my_avatar_md5, my_avatar_token): + print "No need to upload the new avatar" diff --git a/src/linklocal_plugin.py b/src/linklocal_plugin.py new file mode 100644 index 0000000..b8f6445 --- /dev/null +++ b/src/linklocal_plugin.py @@ -0,0 +1,27 @@ +# 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 gobject + +class LinkLocalPlugin(gobject.GObject): + def __init__(self, registry, owner): + gobject.GObject.__init__(self) + self._registry = registry + self._owner = owner + + def cleanup(self): + pass diff --git a/src/org.laptop.Sugar.Presence.service.in b/src/org.laptop.Sugar.Presence.service.in new file mode 100644 index 0000000..70ecda6 --- /dev/null +++ b/src/org.laptop.Sugar.Presence.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name = org.laptop.Sugar.Presence +Exec = @bindir@/sugar-presence-service + diff --git a/src/presenceservice.py b/src/presenceservice.py new file mode 100644 index 0000000..5bcfd45 --- /dev/null +++ b/src/presenceservice.py @@ -0,0 +1,517 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from weakref import WeakValueDictionary + +import dbus +import dbus.service +import gobject +from dbus.gobject_service import ExportedGObject +from dbus.mainloop.glib import DBusGMainLoop +from telepathy.client import ManagerRegistry, Connection +from telepathy.interfaces import (CONN_MGR_INTERFACE, CONN_INTERFACE) +from telepathy.constants import (CONNECTION_STATUS_CONNECTING, + CONNECTION_STATUS_CONNECTED, + CONNECTION_STATUS_DISCONNECTED) + +from sugar import util + +from server_plugin import ServerPlugin +from linklocal_plugin import LinkLocalPlugin +from buddy import Buddy, ShellOwner +from activity import Activity +from psutils import pubkey_to_keyid + +_PRESENCE_SERVICE = "org.laptop.Sugar.Presence" +_PRESENCE_INTERFACE = "org.laptop.Sugar.Presence" +_PRESENCE_PATH = "/org/laptop/Sugar/Presence" + + +_logger = logging.getLogger('s-p-s.presenceservice') + + +class NotFoundError(dbus.DBusException): + def __init__(self, msg): + dbus.DBusException.__init__(self, msg) + self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound' + +class PresenceService(ExportedGObject): + __gtype_name__ = "PresenceService" + + __gsignals__ = { + 'connection-status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_BOOLEAN])) + } + + def _create_owner(self): + # Overridden by TestPresenceService + return ShellOwner(self, self._session_bus) + + def __init__(self): + self._next_object_id = 0 + self._connected = False + + # all Buddy objects + # identifier -> Buddy, GC'd when no more refs exist + self._buddies = WeakValueDictionary() + + # the online buddies for whom we know the full public key + # base64 public key -> Buddy + self._buddies_by_pubkey = {} + + # The online buddies (those who're available via some CM) + # TP plugin -> (handle -> Buddy) + self._handles_buddies = {} + + # activity id -> Activity + self._activities = {} + + self._session_bus = dbus.SessionBus() + self._session_bus.add_signal_receiver(self._connection_disconnected_cb, + signal_name="Disconnected", + dbus_interface="org.freedesktop.DBus") + + # Create the Owner object + self._owner = self._create_owner() + key = self._owner.props.key + keyid = pubkey_to_keyid(key) + self._buddies['keyid/' + keyid] = self._owner + self._buddies_by_pubkey[key] = self._owner + + self._registry = ManagerRegistry() + self._registry.LoadManagers() + + # Set up the server connection + self._server_plugin = ServerPlugin(self._registry, self._owner) + self._handles_buddies[self._server_plugin] = {} + + self._server_plugin.connect('status', self._server_status_cb) + self._server_plugin.connect('contact-online', self._contact_online) + self._server_plugin.connect('contact-offline', self._contact_offline) + self._server_plugin.connect('avatar-updated', self._avatar_updated) + self._server_plugin.connect('buddy-properties-changed', + self._buddy_properties_changed) + self._server_plugin.connect('buddy-activities-changed', + self._buddy_activities_changed) + self._server_plugin.connect('activity-invitation', + self._activity_invitation) + self._server_plugin.connect('private-invitation', + self._private_invitation) + self._server_plugin.connect('activity-properties-changed', + self._activity_properties_changed) + self._server_plugin.start() + + # Set up the link local connection + self._ll_plugin = LinkLocalPlugin(self._registry, self._owner) + self._handles_buddies[self._ll_plugin] = {} + + ExportedGObject.__init__(self, self._session_bus, _PRESENCE_PATH) + + # for activation to work in a race-free way, we should really + # export the bus name only after we export our initial object; + # so this comes after the parent __init__ + self._bus_name = dbus.service.BusName(_PRESENCE_SERVICE, + bus=self._session_bus) + + def _connection_disconnected_cb(self, foo=None): + """Log event when D-Bus kicks us off the bus for some reason""" + _logger.debug("Disconnected from session bus!!!") + + 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: + self._connected = True + self._handles_buddies[plugin][plugin.self_handle] = self._owner + self._owner.add_telepathy_handle(plugin, plugin.self_handle) + else: + self._connected = False + if plugin.self_handle is not None: + self._handles_buddies.setdefault(plugin, {}).pop( + plugin.self_handle, None) + self._owner.remove_telepathy_handle(plugin, plugin.self_handle) + + if self._connected != old_status: + self.emit('connection-status', self._connected) + + def get_buddy(self, objid): + buddy = self._buddies.get(objid) + if buddy is None: + _logger.debug('Creating new buddy at .../%s', objid) + # we don't know yet this buddy + buddy = Buddy(self._session_bus, objid) + buddy.connect("validity-changed", self._buddy_validity_changed_cb) + buddy.connect("disappeared", self._buddy_disappeared_cb) + self._buddies[objid] = buddy + return buddy + + def _contact_online(self, tp, objid, handle, props): + _logger.debug('Handle %u, .../%s is now online', handle, objid) + buddy = self.get_buddy(objid) + + self._handles_buddies[tp][handle] = buddy + # store the handle of the buddy for this CM + buddy.add_telepathy_handle(tp, handle) + buddy.set_properties(props) + + def _buddy_validity_changed_cb(self, buddy, valid): + if valid: + self.BuddyAppeared(buddy.object_path()) + self._buddies_by_pubkey[buddy.props.key] = buddy + _logger.debug("New Buddy: %s (%s)", buddy.props.nick, + buddy.props.color) + else: + self.BuddyDisappeared(buddy.object_path()) + self._buddies_by_pubkey.pop(buddy.props.key, None) + _logger.debug("Buddy left: %s (%s)", buddy.props.nick, + buddy.props.color) + + def _buddy_disappeared_cb(self, buddy): + if buddy.props.valid: + self._buddy_validity_changed_cb(buddy, False) + + def _contact_offline(self, tp, handle): + if not self._handles_buddies[tp].has_key(handle): + return + + buddy = self._handles_buddies[tp].pop(handle) + # the handle of the buddy for this CM is not valid anymore + # (this might trigger _buddy_disappeared_cb if they are not visible + # via any CM) + buddy.remove_telepathy_handle(tp, handle) + + def _get_next_object_id(self): + """Increment and return the object ID counter.""" + self._next_object_id = self._next_object_id + 1 + return self._next_object_id + + def _avatar_updated(self, tp, handle, avatar): + buddy = self._handles_buddies[tp].get(handle) + if buddy and not buddy.props.owner: + _logger.debug("Buddy %s icon updated" % buddy.props.nick) + buddy.props.icon = avatar + + def _buddy_properties_changed(self, tp, handle, properties): + buddy = self._handles_buddies[tp].get(handle) + if buddy: + buddy.set_properties(properties) + _logger.debug("Buddy %s properties updated: %s", buddy.props.nick, + properties.keys()) + + def _new_activity(self, activity_id, tp): + try: + objid = self._get_next_object_id() + activity = Activity(self._session_bus, objid, self, tp, + id=activity_id) + except Exception: + # FIXME: catching bare Exception considered harmful + _logger.debug("Invalid activity:", exc_info=1) + return None + + activity.connect("validity-changed", + self._activity_validity_changed_cb) + activity.connect("disappeared", self._activity_disappeared_cb) + self._activities[activity_id] = activity + return activity + + def _activity_disappeared_cb(self, activity): + _logger.debug("activity %s disappeared" % activity.props.id) + + self.ActivityDisappeared(activity.object_path()) + del self._activities[activity.props.id] + + def _buddy_activities_changed(self, tp, contact_handle, activities): + acts = [] + for act in activities: + acts.append(str(act)) + _logger.debug("Handle %s activities changed: %s", contact_handle, acts) + buddies = self._handles_buddies[tp] + buddy = buddies.get(contact_handle) + + if not buddy: + # We don't know this buddy + # FIXME: What should we do here? + # FIXME: Do we need to check if the buddy is valid or something? + _logger.debug("contact_activities_changed: buddy unknown") + return + + old_activities = set() + for activity in buddy.get_joined_activities(): + old_activities.add(activity.props.id) + + new_activities = set(activities) + + activities_joined = new_activities - old_activities + for act in activities_joined: + _logger.debug("Handle %s joined activity %s", contact_handle, act) + activity = self._activities.get(act) + if activity is None: + # new activity, can fail + activity = self._new_activity(act, tp) + + if activity is not None: + activity.buddy_apparently_joined(buddy) + + activities_left = old_activities - new_activities + for act in activities_left: + _logger.debug("Handle %s left activity %s", contact_handle, act) + activity = self._activities.get(act) + if not activity: + continue + + activity.buddy_apparently_left(buddy) + + def _activity_invitation(self, tp, act_id): + activity = self._activities.get(act_id) + if activity: + self.ActivityInvitation(activity.object_path()) + + def _private_invitation(self, tp, chan_path): + conn = tp.get_connection() + self.PrivateInvitation(str(conn.service_name), conn.object_path, + chan_path) + + @dbus.service.signal(_PRESENCE_INTERFACE, signature="o") + def ActivityAppeared(self, activity): + pass + + @dbus.service.signal(_PRESENCE_INTERFACE, signature="o") + def ActivityDisappeared(self, activity): + pass + + @dbus.service.signal(_PRESENCE_INTERFACE, signature="o") + def BuddyAppeared(self, buddy): + pass + + @dbus.service.signal(_PRESENCE_INTERFACE, signature="o") + def BuddyDisappeared(self, buddy): + pass + + @dbus.service.signal(_PRESENCE_INTERFACE, signature="o") + def ActivityInvitation(self, activity): + pass + + @dbus.service.signal(_PRESENCE_INTERFACE, signature="soo") + def PrivateInvitation(self, bus_name, connection, channel): + pass + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature='', + out_signature="ao") + def GetActivities(self): + ret = [] + for act in self._activities.values(): + if act.props.valid: + ret.append(act.object_path()) + return ret + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature="s", + out_signature="o") + def GetActivityById(self, actid): + act = self._activities.get(actid, None) + if not act or not act.props.valid: + raise NotFoundError("The activity was not found.") + return act.object_path() + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature='', + out_signature="ao") + def GetBuddies(self): + # in the presence of an out_signature, dbus-python will convert + # this set into an Array automatically (because it's iterable), + # so it's easy to use for uniquification (we want to avoid returning + # buddies who're visible on both Salut and Gabble twice) + + # always include myself even if I have no handles + ret = set((self._owner,)) + + for handles_buddies in self._handles_buddies.itervalues(): + for buddy in handles_buddies.itervalues(): + if buddy.props.valid: + ret.add(buddy.object_path()) + return ret + + @dbus.service.method(_PRESENCE_INTERFACE, + in_signature="ay", out_signature="o", + byte_arrays=True) + def GetBuddyByPublicKey(self, key): + buddy = self._buddies_by_pubkey.get(key) + if buddy is not None: + if buddy.props.valid: + return buddy.object_path() + keyid = pubkey_to_keyid(key) + buddy = self._buddies.get('keyid/' + keyid) + if buddy is not None: + if buddy.props.valid: + return buddy.object_path() + raise NotFoundError("The buddy was not found.") + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature='sou', + out_signature='o') + def GetBuddyByTelepathyHandle(self, tp_conn_name, tp_conn_path, handle): + """Get the buddy corresponding to a Telepathy handle. + + :Parameters: + `tp_conn_name` : str + The well-known bus name of a Telepathy connection + `tp_conn_path` : dbus.ObjectPath + The object path of the Telepathy connection + `handle` : int or long + The handle of a Telepathy contact on that connection, + of type HANDLE_TYPE_CONTACT. This may not be a + channel-specific handle. + :Returns: the object path of a Buddy + :Raises NotFoundError: if the buddy is not found. + """ + for tp, handles in self._handles_buddies.iteritems(): + conn = tp.get_connection() + if conn is None: + continue + if (conn.service_name == tp_conn_name + and conn.object_path == tp_conn_path): + buddy = handles.get(handle) + if buddy is not None and buddy.props.valid: + return buddy.object_path() + # either the handle is invalid, or we don't have a Buddy + # object for that buddy because we don't have all their + # details yet + raise NotFoundError("The buddy %u was not found on the " + "connection to %s:%s" + % (handle, tp_conn_name, tp_conn_path)) + raise NotFoundError("The buddy %u was not found: we have no " + "connection to %s:%s" % (handle, tp_conn_name, + tp_conn_path)) + + def map_handles_to_buddies(self, tp, tp_chan, handles, create=True): + """ + + :Parameters: + `tp` : Telepathy plugin + The server or link-local plugin + `tp_chan` : telepathy.client.Channel or None + If not None, the channel in which these handles are + channel-specific + `handles` : iterable over int or long + The handles to be mapped to Buddy objects + `create` : bool + If true (default), if a corresponding `Buddy` object is not + found, create one. + :Returns: + A dict mapping handles from `handles` to `Buddy` objects. + If `create` is true, the dict's keys will be exactly the + items of `handles` in some order. If `create` is false, + the dict will contain no entry for handles for which no + `Buddy` is already available. + :Raises LookupError: if `tp` is not a plugin attached to this PS. + """ + handle_to_buddy = self._handles_buddies[tp] + + ret = {} + missing = [] + for handle in handles: + buddy = handle_to_buddy.get(handle) + if buddy is None: + missing.append(handle) + else: + ret[handle] = buddy + + if missing and create: + handle_to_objid = tp.identify_contacts(tp_chan, missing) + for handle, objid in handle_to_objid.iteritems(): + buddy = self.get_buddy(objid) + ret[handle] = buddy + if tp_chan is None: + handle_to_buddy[handle] = buddy + return ret + + @dbus.service.method(_PRESENCE_INTERFACE, + in_signature='', out_signature="o") + def GetOwner(self): + if not self._owner: + raise NotFoundError("The owner was not found.") + else: + return self._owner.object_path() + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature="sssa{sv}", + out_signature="o", async_callbacks=('async_cb', 'async_err_cb')) + def ShareActivity(self, actid, atype, name, properties, async_cb, + async_err_cb): + self._share_activity(actid, atype, name, properties, + (async_cb, async_err_cb)) + + @dbus.service.method(_PRESENCE_INTERFACE, + in_signature='', out_signature="so") + def GetPreferredConnection(self): + conn = self._server_plugin.get_connection() + return str(conn.service_name), conn.object_path + + def cleanup(self): + for tp in self._handles_buddies: + tp.cleanup() + + def _share_activity(self, actid, atype, name, properties, callbacks): + objid = self._get_next_object_id() + # FIXME check which tp client we should use to share the activity + color = self._owner.props.color + activity = Activity(self._session_bus, objid, self, + self._server_plugin, id=actid, type=atype, + name=name, color=color, local=True) + activity.connect("validity-changed", + self._activity_validity_changed_cb) + self._activities[actid] = activity + activity._share(callbacks, self._owner) + + # local activities are valid at creation by definition, but we can't + # connect to the activity's validity-changed signal until its already + # issued the signal, which happens in the activity's constructor + # for local activities. + self._activity_validity_changed_cb(activity, activity.props.valid) + + def _activity_validity_changed_cb(self, activity, valid): + if valid: + self.ActivityAppeared(activity.object_path()) + _logger.debug("New Activity: %s (%s)", activity.props.name, + activity.props.id) + else: + self.ActivityDisappeared(activity.object_path()) + _logger.debug("Activity disappeared: %s (%s)", activity.props.name, + activity.props.id) + + def _activity_properties_changed(self, tp, act_id, props): + activity = self._activities.get(act_id) + if activity: + activity.set_properties(props) + + +def main(test_num=0, randomize=False): + loop = gobject.MainLoop() + dbus_mainloop_wrapper = DBusGMainLoop(set_as_default=True) + + if test_num > 0: + from pstest import TestPresenceService + ps = TestPresenceService(test_num, randomize) + else: + ps = PresenceService() + + try: + loop.run() + except KeyboardInterrupt: + ps.cleanup() + _logger.debug('Ctrl+C pressed, exiting...') + +if __name__ == "__main__": + main() diff --git a/src/pstest.py b/src/pstest.py new file mode 100644 index 0000000..7715fd3 --- /dev/null +++ b/src/pstest.py @@ -0,0 +1,317 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import random +from ConfigParser import ConfigParser, NoOptionError + +import gobject + +from sugar import env, util + +from buddy import GenericOwner, _PROP_NICK, _PROP_CURACT, _PROP_COLOR +from presenceservice import PresenceService +from psutils import pubkey_to_keyid + + +_logger = logging.getLogger('s-p-s.pstest') + + +class TestOwner(GenericOwner): + """Class representing the owner of the machine. This test owner + changes random attributes periodically.""" + + __gtype_name__ = "TestOwner" + + def __init__(self, ps, bus, test_num, randomize): + 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) + + (pubkey, privkey, registered) = self._load_config() + if not pubkey or not len(pubkey) or not privkey or not len(privkey): + (pubkey, privkey) = _get_new_keypair(test_num) + + if not pubkey or not privkey: + raise RuntimeError("Couldn't get or create test buddy keypair") + + self._save_config(pubkey, privkey, registered) + privkey_hash = util.printable_hash(util._sha_data(privkey)) + + nick = _get_random_name() + from sugar.graphics import xocolor + color = xocolor.XoColor().to_string() + icon = _get_random_image() + + _logger.debug("pubkey is %s" % pubkey) + GenericOwner.__init__(self, ps, bus, + 'keyid/' + pubkey_to_keyid(pubkey), + key=pubkey, nick=nick, color=color, icon=icon, + registered=registered, key_hash=privkey_hash) + + # Only do the random stuff if randomize is true + if randomize: + 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: + _logger.debug("Couldn't find activity %s even though it was shared." % actid) + return + _logger.debug("Shared activity %s (%s)." % (actid, activity.props.name)) + self._test_activities.append(activity) + + def _share_error_cb(self, actid, err): + _logger.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 + if self._change_timeout == 0: + self._change_timeout = gobject.timeout_add(10000, self._update_something) + + def set_registered(self, value): + if value: + self._registered = True + + def _load_config(self): + if not os.path.exists(self._cfg_file): + return (None, None, False) + if not self._cp.read([self._cfg_file]): + return (None, None, False) + if not self._cp.has_section(self._section): + return (None, None, False) + + try: + pubkey = self._cp.get(self._section, "pubkey") + privkey = self._cp.get(self._section, "privkey") + registered = self._cp.get(self._section, "registered") + return (pubkey, privkey, registered) + except NoOptionError: + pass + + return (None, None, False) + + def _save_config(self, pubkey, privkey, registered): + # Save config again + if not self._cp.has_section(self._section): + self._cp.add_section(self._section) + self._cp.set(self._section, "pubkey", pubkey) + self._cp.set(self._section, "privkey", privkey) + self._cp.set(self._section, "registered", registered) + f = open(self._cfg_file, 'w') + self._cp.write(f) + f.close() + + def _update_something(self): + it = random.randint(0, 10000) % 4 + if it == 0: + self.props.icon = _get_random_image() + elif it == 1: + from sugar.graphics import xocolor + props = {_PROP_COLOR: xocolor.XoColor().to_string()} + self.set_properties(props) + elif it == 2: + props = {_PROP_NICK: _get_random_name()} + self.set_properties(props) + elif it == 3: + 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 + + +class TestPresenceService(PresenceService): + + def __init__(self, test_num=0, randomize=False): + self.__test_num = test_num + self.__randomize = randomize + PresenceService.__init__(self) + + def _create_owner(self): + return TestOwner(self, self._session_bus, + self.__test_num, self.__randomize) + + def internal_get_activity(self, actid): + return self._activities.get(actid, None) + + +def _extract_public_key(keyfile): + try: + f = open(keyfile, "r") + lines = f.readlines() + f.close() + except IOError, e: + _logger.error("Error reading public key: %s" % e) + return None + + # Extract the public key + magic = "ssh-dss " + key = "" + for l in lines: + l = l.strip() + if not l.startswith(magic): + continue + key = l[len(magic):] + break + if not len(key): + _logger.error("Error parsing public key.") + return None + 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") + lines = f.readlines() + f.close() + except IOError, e: + _logger.error("Error reading private key: %s" % e) + return None + + key = "" + for l in lines: + l = l.strip() + if l.startswith("-----BEGIN DSA PRIVATE KEY-----"): + continue + if l.startswith("-----END DSA PRIVATE KEY-----"): + continue + key += l + if not len(key): + _logger.error("Error parsing private key.") + return None + 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) + + # force-remove key files if they exist to ssh-keygen doesn't + # start asking questions + try: + os.remove(pubkeyfile) + os.remove(privkeyfile) + except OSError: + pass + + cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % privkeyfile + import commands + print "Generating new keypair..." + (s, o) = commands.getstatusoutput(cmd) + print "Done." + pubkey = privkey = None + if s != 0: + _logger.error("Could not generate key pair: %d (%s)" % (s, o)) + else: + pubkey = _extract_public_key(pubkeyfile) + privkey = _extract_private_key(privkeyfile) + + try: + os.remove(pubkeyfile) + os.remove(privkeyfile) + except OSError: + pass + 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, gtk + + def rand(): + return random.random() + + SIZE = 200 + + s = cairo.ImageSurface(cairo.FORMAT_ARGB32, SIZE, SIZE) + cr = cairo.Context(s) + + # background gradient + cr.save() + g = cairo.LinearGradient(0, 0, 1, 1) + g.add_color_stop_rgba(1, rand(), rand(), rand(), rand()) + g.add_color_stop_rgba(0, rand(), rand(), rand(), rand()) + cr.set_source(g) + cr.rectangle(0, 0, SIZE, SIZE); + cr.fill() + cr.restore() + + # random path + cr.set_line_width(10 * rand() + 5) + cr.move_to(SIZE * rand(), SIZE * rand()) + cr.line_to(SIZE * rand(), SIZE * rand()) + cr.rel_line_to(SIZE * rand() * -1, 0) + cr.close_path() + cr.stroke() + + # a circle + cr.set_source_rgba(rand(), rand(), rand(), rand()) + cr.arc(SIZE * rand(), SIZE * rand(), 100 * rand() + 30, 0, 2 * math.pi) + cr.fill() + + # another circle + cr.set_source_rgba(rand(), rand(), rand(), rand()) + cr.arc(SIZE * rand(), SIZE * rand(), 100 * rand() + 30, 0, 2 * math.pi) + cr.fill() + + def img_convert_func(buf, data): + data[0] += buf + return True + + data = [""] + pixbuf = gtk.gdk.pixbuf_new_from_data(s.get_data(), gtk.gdk.COLORSPACE_RGB, + True, 8, s.get_width(), s.get_height(), s.get_stride()) + pixbuf.save_to_callback(img_convert_func, "jpeg", {"quality": "90"}, data) + del pixbuf + + return str(data[0]) diff --git a/src/psutils.py b/src/psutils.py new file mode 100644 index 0000000..25b24b9 --- /dev/null +++ b/src/psutils.py @@ -0,0 +1,259 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from string import ascii_letters, digits +try: + from hashlib import sha1 +except ImportError: + # Python < 2.5 + from sha import new as sha1 + +import dbus +import gobject + + +_logger = logging.getLogger('s-p-s.psutils') + +_ASCII_ALNUM = ascii_letters + digits + + +def pubkey_to_keyid(key): + """Return the key ID for the given public key. This is currently its SHA-1 + in hex. + + :Parameters: + `key` : str + The public key as a Base64 string + :Returns: + The key ID as a string of hex digits + """ + return sha1(key).hexdigest() + + +def escape_identifier(identifier): + """Escape the given string to be a valid D-Bus object path or service + name component, using a reversible encoding to ensure uniqueness. + + The reversible encoding is as follows: + + * The empty string becomes '_' + * Otherwise, each non-alphanumeric character is replaced by '_' plus + two lower-case hex digits; the same replacement is carried out on + the first character, if it's a digit + """ + # '' -> '_' + if not identifier: + return '_' + + # A bit of a fast path for strings which are already OK. + # We deliberately omit '_' because, for reversibility, that must also + # be escaped. + if (identifier.strip(_ASCII_ALNUM) == '' and + identifier[0] in ascii_letters): + return identifier + + # The first character may not be a digit + if identifier[0] not in ascii_letters: + ret = ['_%02x' % ord(identifier[0])] + else: + ret = [identifier[0]] + + # Subsequent characters may be digits or ASCII letters + for c in identifier[1:]: + if c in _ASCII_ALNUM: + ret.append(c) + else: + ret.append('_%02x' % ord(c)) + + return ''.join(ret) + + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices' +NM_PATH = '/org/freedesktop/NetworkManager' + +_ip4am = None + +class IP4AddressMonitor(gobject.GObject): + """This class, and direct buddy IPv4 address access, will go away quite soon""" + + __gsignals__ = { + 'address-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + __gproperties__ = { + 'address' : (str, None, None, None, gobject.PARAM_READABLE) + } + + def get_instance(): + """Retrieve (or create) the IP4Address monitor singleton instance""" + global _ip4am + if not _ip4am: + _ip4am = IP4AddressMonitor() + return _ip4am + get_instance = staticmethod(get_instance) + + def __init__(self): + gobject.GObject.__init__(self) + self._nm_present = False + self._nm_has_been_present = False + self._matches = [] + self._addr = None + self._nm_obj = None + + sys_bus = dbus.SystemBus() + self._watch = sys_bus.watch_name_owner(NM_SERVICE, self._nm_owner_cb) + if not sys_bus.name_has_owner(NM_SERVICE): + addr = self._get_address_fallback() + self._update_address(addr) + + def do_get_property(self, pspec): + if pspec.name == "address": + return self._addr + + def _update_address(self, new_addr): + if new_addr == "0.0.0.0": + new_addr = None + if new_addr == self._addr: + return + + self._addr = new_addr + _logger.debug("IP4 address now '%s'" % new_addr) + self.emit('address-changed', new_addr) + + def _connect_to_nm(self): + """Connect to NM device state signals to tell when the IPv4 address changes""" + try: + sys_bus = dbus.SystemBus() + proxy = sys_bus.get_object(NM_SERVICE, NM_PATH) + self._nm_obj = dbus.Interface(proxy, NM_IFACE) + except dbus.DBusException, err: + _logger.debug("Error finding NetworkManager: %s" % err) + self._nm_present = False + return + + sys_bus = dbus.SystemBus() + match = sys_bus.add_signal_receiver(self._nm_device_active_cb, + signal_name="DeviceNowActive", + dbus_interface=NM_IFACE) + self._matches.append(match) + + match = sys_bus.add_signal_receiver(self._nm_device_no_longer_active_cb, + signal_name="DeviceNoLongerActive", + dbus_interface=NM_IFACE, + bus_name=NM_SERVICE) + self._matches.append(match) + + match = sys_bus.add_signal_receiver(self._nm_state_change_cb, + signal_name="StateChange", + dbus_interface=NM_IFACE, + bus_name=NM_SERVICE) + self._matches.append(match) + + state = self._nm_obj.state() + if state == 3: # NM_STATE_CONNECTED + self._query_devices() + + def _device_properties_cb(self, *props): + active = props[4] + if not active: + return + act_stage = props[5] + # HACK: OLPC NM has an extra stage, so activated == 8 on OLPC + # but 7 everywhere else + if act_stage != 8 and act_stage != 7: + # not activated + return + self._update_address(props[6]) + + def _device_properties_error_cb(self, err): + _logger.debug("Error querying device properties: %s" % err) + + def _query_device_properties(self, device): + sys_bus = dbus.SystemBus() + proxy = sys_bus.get_object(NM_SERVICE, device) + dev = dbus.Interface(proxy, NM_IFACE_DEVICES) + dev.getProperties(reply_handler=self._device_properties_cb, + error_handler=self._device_properties_error_cb) + + def _get_devices_cb(self, ops): + """Query each device's properties""" + for op in ops: + self._query_device_properties(op) + + def _get_devices_error_cb(self, err): + _logger.debug("Error getting NetworkManager devices: %s" % err) + + def _query_devices(self): + """Query NM for a list of network devices""" + self._nm_obj.getDevices(reply_handler=self._get_devices_cb, + error_handler=self._get_devices_error_cb) + + def _nm_device_active_cb(self, device, ssid=None): + self._query_device_properties(device) + + def _nm_device_no_longer_active_cb(self, device): + self._update_address(None) + + def _nm_state_change_cb(self, new_state): + if new_state == 4: # NM_STATE_DISCONNECTED + self._update_address(None) + + def _nm_owner_cb(self, unique_name): + """Clear state when NM goes away""" + if unique_name == '': + # NM went away, or isn't there at all + self._nm_present = False + for match in self._matches: + match.remove() + self._matches = [] + if self._nm_has_been_present: + self._update_address(None) + else: + addr = self._get_address_fallback() + self._update_address(addr) + elif not self._nm_present: + # NM started up + self._nm_present = True + self._nm_has_been_present = True + self._connect_to_nm() + + def _get_iface_address(self, iface): + import socket + import fcntl + import struct + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + fd = s.fileno() + SIOCGIFADDR = 0x8915 + addr = fcntl.ioctl(fd, SIOCGIFADDR, struct.pack('256s', iface[:15]))[20:24] + s.close() + return socket.inet_ntoa(addr) + + def _get_address_fallback(self): + import commands + (s, o) = commands.getstatusoutput("/sbin/route -n") + if s != 0: + return + for line in o.split('\n'): + fields = line.split(" ") + if fields[0] == "0.0.0.0": + iface = fields[len(fields) - 1] + return self._get_iface_address(iface) + return None diff --git a/src/server_plugin.py b/src/server_plugin.py new file mode 100644 index 0000000..548b41f --- /dev/null +++ b/src/server_plugin.py @@ -0,0 +1,1171 @@ +"""Telepathy-python presence server interface/implementation plugin""" +# 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 + +# Standard library +import logging +import os +import sys +from string import hexdigits +try: + # Python >= 2.5 + from hashlib import md5 +except ImportError: + from md5 import new as md5 + +# Other libraries +import dbus +import gobject +import gtk +from telepathy.client import (ConnectionManager, ManagerRegistry, Connection, + Channel) +from telepathy.interfaces import (CONN_MGR_INTERFACE, CONN_INTERFACE, + CHANNEL_TYPE_CONTACT_LIST, CHANNEL_INTERFACE_GROUP, + CONN_INTERFACE_ALIASING, CONN_INTERFACE_AVATARS, CONN_INTERFACE_PRESENCE, + CHANNEL_TYPE_TEXT, CHANNEL_TYPE_STREAMED_MEDIA, PROPERTIES_INTERFACE) +from telepathy.constants import (HANDLE_TYPE_CONTACT, + HANDLE_TYPE_LIST, HANDLE_TYPE_CONTACT, HANDLE_TYPE_ROOM, + CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, + CONNECTION_STATUS_CONNECTING, + CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED, + CONNECTION_STATUS_REASON_NONE_SPECIFIED, + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, + PROPERTY_FLAG_WRITE) +from sugar import util + +# Presence Service local modules +from buddyiconcache import BuddyIconCache +import psutils + + +CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' + +_PROTOCOL = "jabber" +_OBJ_PATH_PREFIX = "/org/freedesktop/Telepathy/Connection/gabble/jabber/" + +_logger = logging.getLogger('s-p-s.server_plugin') + +_RECONNECT_TIMEOUT = 5000 + +def _buddy_icon_save_cb(buf, data): + data[0] += buf + return True + +def _get_buddy_icon_at_size(icon, maxw, maxh, maxsize): + loader = gtk.gdk.PixbufLoader() + loader.write(icon) + loader.close() + unscaled_pixbuf = loader.get_pixbuf() + del loader + + pixbuf = unscaled_pixbuf.scale_simple(maxw, maxh, gtk.gdk.INTERP_BILINEAR) + del unscaled_pixbuf + + data = [""] + quality = 90 + img_size = maxsize + 1 + while img_size > maxsize: + del data + data = [""] + pixbuf.save_to_callback(_buddy_icon_save_cb, "jpeg", + {"quality":"%d" % quality}, data) + quality -= 10 + img_size = len(data[0]) + del pixbuf + + if img_size > maxsize: + del data + raise RuntimeError("could not size image less than %d bytes" % maxsize) + + return str(data[0]) + + +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': + # Contact has come online and we've discovered all their buddy + # properties. + # args: + # contact identification (based on key ID or JID): str + # contact handle: int or long + # dict {name: str => property: object} + (gobject.SIGNAL_RUN_FIRST, None, [str, object, object]), + 'contact-offline': + # Contact has gone offline. + # args: contact handle + (gobject.SIGNAL_RUN_FIRST, None, [object]), + 'status': + # Connection status changed. + # args: status, reason as for Telepathy StatusChanged + (gobject.SIGNAL_RUN_FIRST, None, [int, int]), + 'avatar-updated': + # Contact's avatar has changed + # args: contact handle: int; icon data: str + (gobject.SIGNAL_RUN_FIRST, None, [object, object]), + 'buddy-properties-changed': + # OLPC buddy properties changed; as for PropertiesChanged + # args: + # contact handle: int + # properties: dict {name: str => property: object} + # FIXME: are these all the properties or just those that changed? + (gobject.SIGNAL_RUN_FIRST, None, [object, object]), + 'buddy-activities-changed': + # OLPC activities changed + # args: + # contact handle: int + # activity IDs: list of str + (gobject.SIGNAL_RUN_FIRST, None, [object, object]), + 'activity-invitation': + # We were invited to join an activity + # args: activity ID: str + (gobject.SIGNAL_RUN_FIRST, None, [object]), + 'private-invitation': + # We were invited to join a chat or a media call + # args: channel object path + (gobject.SIGNAL_RUN_FIRST, None, [object]), + 'activity-properties-changed': + # An activity's properties changed; as for + # ActivityPropertiesChanged + # args: activity ID: str; properties: dict { str => object } + # FIXME: are these all the properties or just those that changed? + (gobject.SIGNAL_RUN_FIRST, None, [object, object]), + 'activity-shared': + # share_activity() succeeded + # args: + # activity ID: str + # channel: telepathy.client.Channel, or None on failure + # error: None, or Exception on failure + # userdata as passed to share_activity + (gobject.SIGNAL_RUN_FIRST, None, [object, object, object, object]), + 'activity-joined': + # join_activity() succeeded + # args: as for activity-shared + (gobject.SIGNAL_RUN_FIRST, None, [object, object, object, object]), + } + + 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._conn = None + self._icon_cache = BuddyIconCache() + + self._registry = registry + self._online_contacts = {} # handle -> jid + + # activity id -> handle + self._activities = {} + # (activity_id, handle of the activity channel) + self._joined_activities = [] + + self._owner = owner + self._owner.connect("property-changed", + self._owner_property_changed_cb) + self._owner.connect("icon-changed", self._owner_icon_changed_cb) + self.self_handle = None + + self._account = self._get_account_info() + self._conn_status = CONNECTION_STATUS_DISCONNECTED + self._reconnect_id = 0 + + # Monitor IPv4 address as an indicator of the network connection + self._ip4am = psutils.IP4AddressMonitor.get_instance() + self._ip4am.connect('address-changed', self._ip4_address_changed_cb) + + self._publish_channel = None + self._subscribe_channel = None + self._subscribe_members = set() + self._subscribe_local_pending = set() + self._subscribe_remote_pending = set() + + def _ip4_address_changed_cb(self, ip4am, address): + _logger.debug("::: IP4 address now %s", address) + if address: + _logger.debug("::: valid IP4 address, conn_status %s", + self._conn_status) + if self._conn_status == CONNECTION_STATUS_DISCONNECTED: + _logger.debug("::: will connect") + self.start() + else: + _logger.debug("::: invalid IP4 address, will disconnect") + self.cleanup() + + 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. + """ + _logger.debug("Owner properties changed: %s", properties) + + if properties.has_key("current-activity"): + self._set_self_current_activity() + + if properties.has_key("nick"): + self._set_self_alias() + # Hack; send twice to make sure the server gets it + gobject.timeout_add(1000, self._set_self_alias) + + if properties.has_key("color") or properties.has_key("ip4-address"): + if self._conn_status == CONNECTION_STATUS_CONNECTED: + self._set_self_olpc_properties() + + def _owner_icon_changed_cb(self, owner, icon): + """Owner has changed their icon, forward to network""" + _logger.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() + + khash = psutils.pubkey_to_keyid(self._owner.props.key) + account_info['account'] = "%s@%s" % (khash, account_info['server']) + + account_info['password'] = self._owner.get_key_hash() + account_info['register'] = not self._owner.get_registered() + + print "ACCT: %s" % account_info + 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 + connections = Connection.get_connections() + conn = None + for item in connections: + if not item.object_path.startswith(_OBJ_PATH_PREFIX): + continue + if item[CONN_INTERFACE].GetProtocol() != _PROTOCOL: + continue + if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_CONNECTED: + test_handle = item[CONN_INTERFACE].RequestHandles( + HANDLE_TYPE_CONTACT, [our_name])[0] + if item[CONN_INTERFACE].GetSelfHandle() != test_handle: + continue + return item + 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() + + # Create a new connection + gabble_mgr = self._registry.GetManager('gabble') + name, path = gabble_mgr[CONN_MGR_INTERFACE].RequestConnection( + _PROTOCOL, acct) + conn = Connection(name, path) + del acct + + conn[CONN_INTERFACE].connect_to_signal('StatusChanged', + self._status_changed_cb) + conn[CONN_INTERFACE].connect_to_signal('NewChannel', + self._new_channel_cb) + + # hack + conn._valid_interfaces.add(CONN_INTERFACE_PRESENCE) + conn._valid_interfaces.add(CONN_INTERFACE_BUDDY_INFO) + conn._valid_interfaces.add(CONN_INTERFACE_ACTIVITY_PROPERTIES) + conn._valid_interfaces.add(CONN_INTERFACE_AVATARS) + conn._valid_interfaces.add(CONN_INTERFACE_ALIASING) + + conn[CONN_INTERFACE_PRESENCE].connect_to_signal('PresenceUpdate', + self._presence_update_cb) + + self._conn = conn + status = self._conn[CONN_INTERFACE].GetStatus() + + if status == CONNECTION_STATUS_DISCONNECTED: + def connect_reply(): + _logger.debug('Connect() succeeded') + def connect_error(e): + _logger.debug('Connect() failed: %s', e) + if not self._reconnect_id: + self._reconnect_id = gobject.timeout_add(_RECONNECT_TIMEOUT, + self._reconnect_cb) + + self._conn[CONN_INTERFACE].Connect(reply_handler=connect_reply, + error_handler=connect_error) + + self._handle_connection_status_change(status, + CONNECTION_STATUS_REASON_NONE_SPECIFIED) + + def _connected_cb(self): + """Callback on successful connection to a server + """ + + if self._account['register']: + # we successfully register this account + self._owner.set_registered(True) + + # request both handles at the same time to reduce round-trips + pub_handle, sub_handle = self._conn[CONN_INTERFACE].RequestHandles( + HANDLE_TYPE_LIST, ['publish', 'subscribe']) + + # the group of contacts who may receive your presence + publish = self._conn.request_channel(CHANNEL_TYPE_CONTACT_LIST, + HANDLE_TYPE_LIST, pub_handle, True) + self._publish_channel = publish + publish[CHANNEL_INTERFACE_GROUP].connect_to_signal('MembersChanged', + self._publish_members_changed_cb) + publish_handles, local_pending, remote_pending = \ + publish[CHANNEL_INTERFACE_GROUP].GetAllMembers() + + # the group of contacts for whom you wish to receive presence + subscribe = self._conn.request_channel(CHANNEL_TYPE_CONTACT_LIST, + HANDLE_TYPE_LIST, sub_handle, True) + self._subscribe_channel = subscribe + subscribe[CHANNEL_INTERFACE_GROUP].connect_to_signal('MembersChanged', + self._subscribe_members_changed_cb) + subscribe_handles, subscribe_lp, subscribe_rp = \ + subscribe[CHANNEL_INTERFACE_GROUP].GetAllMembers() + self._subscribe_members = set(subscribe_handles) + self._subscribe_local_pending = set(subscribe_lp) + self._subscribe_remote_pending = set(subscribe_rp) + + if local_pending: + # accept pending subscriptions + publish[CHANNEL_INTERFACE_GROUP].AddMembers(local_pending, '') + + self.self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() + self._online_contacts[self.self_handle] = self._account['account'] + + # request subscriptions from people subscribed to us if we're not + # subscribed to them + not_subscribed = list(set(publish_handles) - set(subscribe_handles)) + subscribe[CHANNEL_INTERFACE_GROUP].AddMembers(not_subscribed, '') + + if CONN_INTERFACE_BUDDY_INFO not in self._conn.get_valid_interfaces(): + _logger.debug('OLPC information not available') + return False + + self._conn[CONN_INTERFACE_BUDDY_INFO].connect_to_signal( + 'PropertiesChanged', self._buddy_properties_changed_cb) + self._conn[CONN_INTERFACE_BUDDY_INFO].connect_to_signal( + 'ActivitiesChanged', self._buddy_activities_changed_cb) + self._conn[CONN_INTERFACE_BUDDY_INFO].connect_to_signal( + 'CurrentActivityChanged', + self._buddy_current_activity_changed_cb) + + self._conn[CONN_INTERFACE_AVATARS].connect_to_signal('AvatarUpdated', + self._avatar_updated_cb) + self._conn[CONN_INTERFACE_ALIASING].connect_to_signal('AliasesChanged', + self._alias_changed_cb) + self._conn[CONN_INTERFACE_ACTIVITY_PROPERTIES].connect_to_signal( + 'ActivityPropertiesChanged', + self._activity_properties_changed_cb) + + # Set initial buddy properties, avatar, and activities + self._set_self_olpc_properties() + self._set_self_alias() + # Hack; send twice to make sure the server gets it + gobject.timeout_add(1000, self._set_self_alias) + self._set_self_activities() + self._set_self_current_activity() + self._set_self_avatar() + + # Request presence for everyone we're subscribed to + self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) + return True + + def _set_self_avatar_cb(self, token): + self._icon_cache.set_avatar(hash, token) + + def _set_self_avatar(self, icon_data=None): + if not icon_data: + icon_data = self._owner.props.icon + + m = md5() + m.update(icon_data) + hash = m.hexdigest() + + self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() + token = self._conn[CONN_INTERFACE_AVATARS].GetAvatarTokens( + [self_handle])[0] + + if self._icon_cache.check_avatar(hash, token): + # avatar is up to date + return + + types, minw, minh, maxw, maxh, maxsize = \ + self._conn[CONN_INTERFACE_AVATARS].GetAvatarRequirements() + if not "image/jpeg" in types: + _logger.debug("server does not accept JPEG format avatars.") + return + + img_data = _get_buddy_icon_at_size(icon_data, min(maxw, 96), + min(maxh, 96), maxsize) + self._conn[CONN_INTERFACE_AVATARS].SetAvatar(img_data, "image/jpeg", + reply_handler=self._set_self_avatar_cb, + error_handler=lambda e: self._log_error_cb("setting avatar", e)) + + def _join_activity_channel_props_set_cb(self, activity_id, signal, handle, + channel, userdata): + self._joined_activities.append((activity_id, handle)) + self._set_self_activities() + self.emit(signal, activity_id, channel, None, userdata) + + def _join_activity_channel_props_listed_cb(self, activity_id, signal, + handle, channel, userdata, + props, prop_specs): + + props_to_set = [] + for ident, name, sig, flags in prop_specs: + value = props.pop(name, None) + if value is not None: + if flags & PROPERTY_FLAG_WRITE: + props_to_set.append((ident, value)) + # FIXME: else error, but only if we're creating the room? + # FIXME: if props is nonempty, then we want to set props that aren't + # supported here - raise an error? + + if props_to_set: + channel[PROPERTIES_INTERFACE].SetProperties(props_to_set, + reply_handler=lambda: self._join_activity_channel_props_set_cb( + activity_id, signal, handle, channel, userdata), + error_handler=lambda e: self._join_error_cb( + activity_id, signal, userdata, + 'SetProperties(%r)' % props_to_set, e)) + else: + self._join_activity_channel_props_set_cb(activity_id, signal, + handle, channel, userdata) + + def _join_activity_create_channel_cb(self, activity_id, signal, handle, + userdata, chan_path): + channel = Channel(self._conn.service_name, chan_path) + props = { + 'anonymous': False, # otherwise buddy resolution breaks + 'invite-only': False, # XXX: should be True in future + #'name': ... # XXX: set from activity name? + 'persistent': False, # vanish when there are no members + 'private': False, # XXX: should be True unless public + } + channel[PROPERTIES_INTERFACE].ListProperties( + reply_handler=lambda prop_specs: self._join_activity_channel_props_listed_cb( + activity_id, signal, handle, channel, userdata, props, prop_specs), + error_handler=lambda e: self._join_error_cb( + activity_id, signal, userdata, 'ListProperties', e)) + + def _join_activity_get_channel_cb(self, activity_id, signal, userdata, + handles): + if not self._activities.has_key(activity_id): + self._activities[activity_id] = handles[0] + + if (activity_id, handles[0]) in self._joined_activities: + e = RuntimeError("Already joined activity %s" % activity_id) + _logger.debug('%s', e) + self.emit(signal, activity_id, None, e, userdata) + return + + self._conn[CONN_INTERFACE].RequestChannel(CHANNEL_TYPE_TEXT, + HANDLE_TYPE_ROOM, handles[0], True, + reply_handler=lambda *args: self._join_activity_create_channel_cb( + activity_id, signal, handles[0], userdata, *args), + error_handler=lambda e: self._join_error_cb(activity_id, signal, + userdata, 'RequestChannel(TEXT, ROOM, %r, True)' % handles[0], + e)) + + def _join_error_cb(self, activity_id, signal, userdata, where, err): + e = Exception("Error joining/sharing activity %s: (%s): %s" + % (activity_id, where, err)) + _logger.debug('%s', e) + self.emit(signal, activity_id, None, e, userdata) + + def _internal_join_activity(self, activity_id, signal, userdata): + handle = self._activities.get(activity_id) + if not handle: + # 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(HANDLE_TYPE_ROOM, + [room_jid], + reply_handler=lambda *args: self._join_activity_get_channel_cb( + activity_id, signal, userdata, *args), + error_handler=lambda e: self._join_error_cb(activity_id, + signal, userdata, 'RequestHandles([%u])' % room_jid, + e)) + else: + self._join_activity_get_channel_cb(activity_id, signal, 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): + """Ignore an event (null-operation)""" + + def _log_error_cb(self, msg, err): + """Log a message (error) at debug level with prefix msg""" + _logger.debug("Error %s: %s", msg, err) + + def _set_self_olpc_properties(self): + """Set color and key on our Telepathy server identity""" + props = {} + props['color'] = self._owner.props.color + props['key'] = dbus.ByteArray(self._owner.props.key) + addr = self._owner.props.ip4_address + if not addr: + props['ip4-address'] = "" + else: + props['ip4-address'] = addr + self._conn[CONN_INTERFACE_BUDDY_INFO].SetProperties(props, + reply_handler=self._ignore_success_cb, + error_handler=lambda e: self._log_error_cb("setting properties", e)) + + 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}, + reply_handler=self._ignore_success_cb, + error_handler=lambda e: self._log_error_cb("setting alias", e)) + return False + + 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 e: self._log_error_cb("setting activities", e)) + + 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: + cur_activity = "" + else: + cur_activity_handle = self._get_handle_for_activity(cur_activity) + if not cur_activity_handle: + # dont advertise a current activity that's not shared + cur_activity = "" + + _logger.debug("Setting current activity to '%s' (handle %s)", + cur_activity, cur_activity_handle) + self._conn[CONN_INTERFACE_BUDDY_INFO].SetCurrentActivity(cur_activity, + cur_activity_handle, + reply_handler=self._ignore_success_cb, + error_handler=lambda e: self._log_error_cb("setting current activity", e)) + + 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 _reconnect_cb(self): + """Attempt to reconnect to the server""" + self.start() + return False + + def _handle_connection_status_change(self, status, reason): + if status == self._conn_status: + return + + if status == CONNECTION_STATUS_CONNECTING: + self._conn_status = status + _logger.debug("status: connecting...") + elif status == CONNECTION_STATUS_CONNECTED: + if self._connected_cb(): + _logger.debug("status: connected") + self._conn_status = status + else: + self.cleanup() + _logger.debug("status: was connected, but an error occurred") + elif status == CONNECTION_STATUS_DISCONNECTED: + self.cleanup() + _logger.debug("status: disconnected (reason %r)", reason) + if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED: + # FIXME: handle connection failure; retry later? + pass + else: + # If disconnected, but still have a network connection, retry + # If disconnected and no network connection, do nothing here + # and let the IP4AddressMonitor address-changed signal handle + # reconnection + if self._ip4am.props.address and not self._reconnect_id: + self._reconnect_id = gobject.timeout_add(_RECONNECT_TIMEOUT, + self._reconnect_cb) + + self.emit('status', self._conn_status, int(reason)) + return False + + def _status_changed_cb(self, status, reason): + """Handle notification of connection-status change + + status -- CONNECTION_STATUS_* + reason -- integer code describing the reason... + """ + _logger.debug("::: connection status changed to %s", status) + self._handle_connection_status_change(status, reason) + + 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 + """ + _logger.debug("Starting up...") + + if self._reconnect_id > 0: + gobject.source_remove(self._reconnect_id) + self._reconnect_id = 0 + + # Only init connection if we have a valid IP address + if self._ip4am.props.address: + _logger.debug("::: Have IP4 address %s, will connect", + self._ip4am.props.address) + self._init_connection() + else: + _logger.debug("::: No IP4 address, postponing connection") + + def cleanup(self): + """If we still have a connection, disconnect it""" + if self._conn: + try: + self._conn[CONN_INTERFACE].Disconnect() + except: + pass + self._conn = None + self._conn_status = CONNECTION_STATUS_DISCONNECTED + + for handle in self._online_contacts.keys(): + self._contact_offline(handle) + self._online_contacts = {} + self._joined_activites = [] + self._activites = {} + + if self._reconnect_id > 0: + gobject.source_remove(self._reconnect_id) + self._reconnect_id = 0 + + 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]: + self.emit("contact-offline", handle) + 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""" + _logger.debug("Handle %s - Error getting activities: %s", + handle, err) + # Don't drop the buddy if we can't get their activities, for now + #self._contact_offline(handle) + + def _contact_online_aliases_cb(self, handle, props, aliases): + """Handle contact's alias being received (do further queries)""" + if not self._conn or not aliases or not len(aliases): + _logger.debug("Handle %s - No aliases", handle) + self._contact_offline(handle) + return + + props['nick'] = aliases[0] + + jid = self._conn[CONN_INTERFACE].InspectHandles(HANDLE_TYPE_CONTACT, + [handle])[0] + self._online_contacts[handle] = jid + objid = self.identify_contacts(None, [handle])[handle] + + self.emit("contact-online", objid, handle, props) + + self._conn[CONN_INTERFACE_BUDDY_INFO].GetActivities(handle, + reply_handler=lambda *args: self._contact_online_activities_cb( + handle, *args), + error_handler=lambda e: self._contact_online_activities_error_cb( + handle, e)) + + def _contact_online_aliases_error_cb(self, handle, props, retry, err): + """Handle failure to retrieve given user's alias/information""" + if retry: + _logger.debug("Handle %s - Error getting nickname (will retry):" + "%s", handle, err) + self._conn[CONN_INTERFACE_ALIASING].RequestAliases([handle], + reply_handler=lambda *args: self._contact_online_aliases_cb( + handle, props, *args), + error_handler=lambda e: self._contact_online_aliases_error_cb( + handle, props, False, e)) + else: + _logger.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'): + _logger.debug("Handle %s - invalid key.", handle) + self._contact_offline(handle) + return + if not props.has_key('color'): + _logger.debug("Handle %s - invalid color.", handle) + self._contact_offline(handle) + return + + self._conn[CONN_INTERFACE_ALIASING].RequestAliases([handle], + reply_handler=lambda *args: self._contact_online_aliases_cb( + handle, props, *args), + error_handler=lambda e: self._contact_online_aliases_error_cb( + handle, props, True, e)) + + def _contact_online_request_properties(self, handle, tries): + self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties(handle, + byte_arrays=True, + reply_handler=lambda *args: self._contact_online_properties_cb( + handle, *args), + error_handler=lambda e: self._contact_online_properties_error_cb( + handle, tries, e)) + return False + + def _contact_online_properties_error_cb(self, handle, tries, err): + """Handle error retrieving property-set for a user (handle)""" + if tries <= 3: + _logger.debug("Handle %s - Error getting properties (will retry):" + " %s", handle, err) + tries += 1 + gobject.timeout_add(1000, self._contact_online_request_properties, + handle, tries) + else: + _logger.debug("Handle %s - Error getting properties: %s", + handle, err) + self._contact_offline(handle) + + def _contact_online(self, handle): + """Handle a contact coming online""" + if (handle not in self._subscribe_members and + handle not in self._subscribe_local_pending and + handle not in self._subscribe_remote_pending): + # it's probably a channel-specific handle - can't create a Buddy + # object for those yet + return + + self._online_contacts[handle] = None + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): + jid = self._conn[CONN_INTERFACE].InspectHandles( + HANDLE_TYPE_CONTACT, [handle])[0] + self._online_contacts[handle] = jid + # ignore network events for Owner property changes since those + # are handled locally + return + + self._contact_online_request_properties(handle, 1) + + def _subscribe_members_changed_cb(self, message, added, removed, + local_pending, remote_pending, + actor, reason): + + added = set(added) + removed = set(removed) + local_pending = set(local_pending) + remote_pending = set(remote_pending) + + affected = added|removed + affected |= local_pending + affected |= remote_pending + + self._subscribe_members -= affected + self._subscribe_members |= added + self._subscribe_local_pending -= affected + self._subscribe_local_pending |= local_pending + self._subscribe_remote_pending -= affected + self._subscribe_remote_pending |= remote_pending + + def _publish_members_changed_cb(self, added, removed, local_pending, + remote_pending, actor, reason): + + if local_pending: + # accept all requested subscriptions + self._publish_channel[CHANNEL_INTERFACE_GROUP].AddMembers( + local_pending, '') + + # subscribe to people who've subscribed to us, if necessary + added = list(set(added) - self._subscribe_members + - self._subscribe_remote_pending) + if added: + self._subscribe_channel[CHANNEL_INTERFACE_GROUP].AddMembers( + added, '') + + 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 + for status, params in statuses.items(): + if not online and status == "offline": + # weren't online in the first place... + continue + jid = self._conn[CONN_INTERFACE].InspectHandles( + HANDLE_TYPE_CONTACT, [handle])[0] + olstr = "ONLINE" + if not online: olstr = "OFFLINE" + _logger.debug("Handle %s (%s) was %s, status now '%s'.", + handle, jid, olstr, status) + if not online and status in ["available", "away", "brb", + "busy", "dnd", "xa"]: + self._contact_online(handle) + elif status in ["offline", "invisible"]: + self._contact_offline(handle) + + def _request_avatar_cb(self, handle, new_avatar_token, avatar, mime_type): + jid = self._online_contacts[handle] + if not jid: + logging.debug("Handle %s not valid yet..." % handle) + return + icon = ''.join(map(chr, avatar)) + self._icon_cache.store_icon(jid, new_avatar_token, icon) + self.emit("avatar-updated", handle, icon) + + 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): + _logger.debug("Handle %s unknown.", handle) + return + + jid = self._online_contacts[handle] + if not jid: + _logger.debug("Handle %s not valid yet...", handle) + return + + icon = self._icon_cache.get_icon(jid, new_avatar_token) + if not icon: + # cache miss + self._conn[CONN_INTERFACE_AVATARS].RequestAvatar(handle, + reply_handler=lambda *args: self._request_avatar_cb(handle, + new_avatar_token, *args), + error_handler=lambda e: self._log_error_cb( + "getting avatar", e)) + else: + 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) + if (self._online_contacts.has_key(handle) and + self._online_contacts[handle]): + 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) + + 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 + return + if (not self._online_contacts.has_key(handle) or + not self._online_contacts[handle]): + return + + for act_id, act_handle in activities: + self._activities[act_id] = act_handle + activities_id = map(lambda x: x[0], activities) + 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 + return + if (not self._online_contacts.has_key(handle) or + not self._online_contacts[handle]): + return + + if not len(activity) or not util.validate_activity_id(activity): + activity = None + prop = {'current-activity': activity} + _logger.debug("Handle %s: current activity now %s", handle, activity) + 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 == HANDLE_TYPE_ROOM and + channel_type == CHANNEL_TYPE_TEXT): + def ready(channel): + + for act_id, act_handle in self._activities.iteritems(): + if handle == act_handle: + break + else: + return + + def got_all_members(current, local_pending, remote_pending): + if local_pending: + for act_id, act_handle in self._activities.iteritems(): + if handle == act_handle: + self.emit('activity-invitation', act_id) + def got_all_members_err(e): + logger.debug('Unable to get channel members for %s:', + object_path, exc_info=1) + + group = channel[CHANNEL_INTERFACE_GROUP] + group.GetAllMembers(reply_handler=got_all_members, + error_handler=got_all_members_err) + + # we throw away the channel as soon as ready() finishes + Channel(self._conn.service_name, object_path, + ready_handler=ready) + + elif (handle_type == HANDLE_TYPE_CONTACT and + channel_type in (CHANNEL_TYPE_TEXT, + CHANNEL_TYPE_STREAMED_MEDIA)): + 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.") + + self._conn[CONN_INTERFACE_ACTIVITY_PROPERTIES].GetProperties(handle, + reply_handler=lambda *args: self._activity_properties_changed_cb( + handle, *args), + error_handler=lambda e: self._log_error_cb( + "getting activity properties", e)) + + 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.") + + self._conn[CONN_INTERFACE_ACTIVITY_PROPERTIES].SetProperties(handle, + props, reply_handler=self._ignore_success_cb, + error_handler=lambda e: self._log_error_cb( + "setting activity properties", e)) + + 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) + return + + def _server_is_trusted(self, hostname): + """Return True if the server with the given hostname is trusted to + verify public-key ownership correctly, and only allows users to + register JIDs whose username part is either a public key fingerprint, + or of the wrong form to be a public key fingerprint (to allow for + ejabberd's admin@example.com address). + + If we trust the server, we can skip verifying the key ourselves, + which leads to simplifications. In the current implementation we + never verify that people actually own the key they claim to, so + we will always give contacts on untrusted servers a JID- rather than + key-based identity. + + For the moment we assume that the test server, olpc.collabora.co.uk, + does this verification. + """ + return (hostname == 'olpc.collabora.co.uk') + + def identify_contacts(self, tp_chan, handles): + """Work out the "best" unique identifier we can for the given handles, + in the context of the given channel (which may be None), using only + 'fast' connection manager API (that does not involve network + round-trips). + + For the XMPP server case, we proceed as follows: + + * Find the owners of the given handles, if the channel has + channel-specific handles + * If the owner (globally-valid JID) is on a trusted server, return + 'keyid/' plus the 'key fingerprint' (the user part of their JID, + currently implemented as the SHA-1 of the Base64 blob in + owner.key.pub) + * If the owner (globally-valid JID) cannot be found or is on an + untrusted server, return 'xmpp/' plus an escaped form of the JID + + The idea is that we identify buddies by key-ID (i.e. by key, assuming + no collisions) if we can find it without making network round-trips, + but if that's not possible we just use their JIDs. + + :Parameters: + `tp_chan` : telepathy.client.Channel or None + The channel in which the handles were found, or None if they + are known to be channel-specific handles + `handles` : iterable over (int or long) + The contacts' handles in that channel + :Returns: + A dict mapping the provided handles to the best available + unique identifier, which is a string that could be used as a + suffix to an object path + """ + # we need to be able to index into handles, so force them to + # be a sequence + if not isinstance(handles, (tuple, list)): + handles = tuple(handles) + + owners = handles + + if tp_chan is not None and CHANNEL_INTERFACE_GROUP in tp_chan: + group = tp_chan[CHANNEL_INTERFACE_GROUP] + if (group.GetGroupFlags() & + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES): + owners = group.GetHandleOwners(handles) + for i, owner in enumerate(owners): + if owner == 0: + owners[i] = handles[i] + else: + group = None + + jids = self._conn[CONN_INTERFACE].InspectHandles(HANDLE_TYPE_CONTACT, + owners) + + ret = {} + for handle, jid in zip(handles, jids): + # special-case the Owner - we always know who we are + if (handle == self.self_handle or + (group is not None and handle == group.GetSelfHandle())): + ret[handle] = self._owner.props.objid + continue + + if '/' in jid: + # the contact is unidentifiable (in an anonymous MUC) - create + # a temporary identity for them, based on their room-JID + ret[handle] = 'xmpp/' + psutils.escape_identifier(jid) + else: + user, host = jid.split('@', 1) + if (self._server_is_trusted(host) and len(user) == 40 and + user.strip(hexdigits) == ''): + # they're on a trusted server and their username looks + # like a key-ID + ret[handle] = 'keyid/' + user.lower() + else: + # untrusted server, or not the right format to be a + # key-ID - identify the contact by their JID + ret[handle] = 'xmpp/' + psutils.escape_identifier(jid) + + return ret diff --git a/src/sugar-presence-service b/src/sugar-presence-service new file mode 100755 index 0000000..6ab871c --- /dev/null +++ b/src/sugar-presence-service @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# vi: ts=4 ai noet +# +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import sys +import os + +from sugar import logger +from sugar import env + + +_logger = logging.getLogger('s-p-s') + + +def usage(): + _logger.debug("Usage: sugar-presence-service [<test buddy number (1 - 10)>] [randomize]") + +sys.path.append(env.get_service_path('presence')) + +test_num = 0 +randomize = False +if len(sys.argv) in [2, 3]: + try: + test_num = int(sys.argv[1]) + except ValueError: + _logger.debug("Bad test user number.") + if test_num < 1 or test_num > 10: + _logger.debug("Bad test user number.") + + if len(sys.argv) == 3 and sys.argv[2] == "randomize": + randomize = True +elif len(sys.argv) == 1: + pass +else: + usage() + os._exit(1) + +if test_num > 0: + logger.start('test-%d-presenceservice' % test_num) +else: + logger.start('presenceservice') + +import presenceservice + +_logger.info('Starting presence service...') + +presenceservice.main(test_num, randomize) diff --git a/src/test_psutils.py b/src/test_psutils.py new file mode 100644 index 0000000..7436d98 --- /dev/null +++ b/src/test_psutils.py @@ -0,0 +1,12 @@ +print "Running test_psutils..." + +from psutils import escape_identifier, pubkey_to_keyid + +assert pubkey_to_keyid('abc') == 'a9993e364706816aba3e25717850c26c9cd0d89d' + +assert escape_identifier('') == '_' +assert escape_identifier('_') == '_5f' +assert escape_identifier('1') == '_31' +assert escape_identifier('a1') == 'a1' +assert escape_identifier('1a') == '_31a' +assert escape_identifier("0123abc_xyz\x01\xff") == '_30123abc_5fxyz_01_ff' |