diff options
author | Dan Williams <dcbw@redhat.com> | 2007-04-09 18:40:50 (GMT) |
---|---|---|
committer | Dan Williams <dcbw@redhat.com> | 2007-04-09 18:40:50 (GMT) |
commit | 2509d990e4837f4810a372fda40d260f32014712 (patch) | |
tree | bfe5df48a74d9a4362be906002879c61b1132f9a /services/presence | |
parent | 2f2f756939c381baff8089fb99d604b2bb49aadf (diff) |
Cut over to new PresenceService
Diffstat (limited to 'services/presence')
-rw-r--r-- | services/presence/Activity.py | 176 | ||||
-rw-r--r-- | services/presence/Buddy.py | 533 | ||||
-rw-r--r-- | services/presence/BuddyIconCache.py | 77 | ||||
-rw-r--r-- | services/presence/Makefile.am | 34 | ||||
-rw-r--r-- | services/presence/PresenceService.py | 876 | ||||
-rw-r--r-- | services/presence/Service.py | 602 | ||||
-rw-r--r-- | services/presence/activity.py | 258 | ||||
-rw-r--r-- | services/presence/buddy.py | 396 | ||||
-rw-r--r-- | services/presence/buddyiconcache.py | 102 | ||||
-rw-r--r-- | services/presence/linklocal_plugin.py | 27 | ||||
-rw-r--r-- | services/presence/org.laptop.Sugar.Presence.service.in (renamed from services/presence/org.laptop.Presence.service.in) | 0 | ||||
-rw-r--r-- | services/presence/presenceservice.py | 332 | ||||
-rw-r--r-- | services/presence/server_plugin.py | 518 | ||||
-rwxr-xr-x | services/presence/sugar-presence-service | 6 |
14 files changed, 1649 insertions, 2288 deletions
diff --git a/services/presence/Activity.py b/services/presence/Activity.py deleted file mode 100644 index 031c036..0000000 --- a/services/presence/Activity.py +++ /dev/null @@ -1,176 +0,0 @@ -# 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 dbus - -PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp" -ACTIVITY_DBUS_OBJECT_PATH = "/org/laptop/Presence/Activities/" -ACTIVITY_DBUS_INTERFACE = "org.laptop.Presence.Activity" - - -class ActivityDBusHelper(dbus.service.Object): - def __init__(self, parent, bus_name, object_path): - self._parent = parent - self._bus_name = bus_name - self._object_path = object_path - dbus.service.Object.__init__(self, bus_name, self._object_path) - - @dbus.service.method(ACTIVITY_DBUS_INTERFACE, - in_signature="s", out_signature="ao") - def getServicesOfType(self, stype): - ret = [] - for serv in self._parent.get_services_of_type(stype): - ret.append(serv.object_path()) - return ret - - @dbus.service.method(ACTIVITY_DBUS_INTERFACE, - in_signature="", out_signature="ao") - def getServices(self): - ret = [] - for serv in self._parent.get_services(): - ret.append(serv.object_path()) - return ret - - @dbus.service.method(ACTIVITY_DBUS_INTERFACE, - in_signature="", out_signature="s") - def getId(self): - return self._parent.get_id() - - @dbus.service.method(ACTIVITY_DBUS_INTERFACE, - in_signature="", out_signature="s") - def getColor(self): - return self._parent.get_color() - - @dbus.service.method(ACTIVITY_DBUS_INTERFACE, - in_signature="", out_signature="ao") - def getJoinedBuddies(self): - ret = [] - for buddy in self._parent.get_joined_buddies(): - ret.append(buddy.object_path()) - return ret - - @dbus.service.signal(ACTIVITY_DBUS_INTERFACE, - signature="o") - def ServiceAppeared(self, object_path): - pass - - @dbus.service.signal(ACTIVITY_DBUS_INTERFACE, - signature="o") - def ServiceDisappeared(self, object_path): - pass - - @dbus.service.signal(ACTIVITY_DBUS_INTERFACE, - signature="o") - def BuddyJoined(self, object_path): - pass - - @dbus.service.signal(ACTIVITY_DBUS_INTERFACE, - signature="o") - def BuddyLeft(self, object_path): - pass - - -class Activity(object): - def __init__(self, bus_name, object_id, initial_service): - if not initial_service.get_activity_id(): - raise ValueError("Service must have a valid Activity ID") - self._activity_id = initial_service.get_activity_id() - - self._buddies = [] - self._services = {} # service type -> list of Services - self._color = None - self._valid = False - - self._object_id = object_id - self._object_path = "/org/laptop/Presence/Activities/%d" % self._object_id - self._dbus_helper = ActivityDBusHelper(self, bus_name, self._object_path) - - self.add_service(initial_service) - - def object_path(self): - return dbus.ObjectPath(self._object_path) - - def is_valid(self): - """An activity is only valid when it's color is available.""" - return self._valid - - def get_id(self): - return self._activity_id - - def get_color(self): - return self._color - - def get_services(self): - ret = [] - for serv_list in self._services.values(): - for service in serv_list: - if service not in ret: - ret.append(service) - return ret - - def get_services_of_type(self, stype): - if self._services.has_key(stype): - return self._services[stype] - return [] - - def get_joined_buddies(self): - buddies = [] - for serv_list in self._services.values(): - for serv in serv_list: - owner = serv.get_owner() - if owner and not owner in buddies and owner.is_valid(): - buddies.append(owner) - return buddies - - def add_service(self, service): - stype = service.get_type() - if not self._services.has_key(stype): - self._services[stype] = [] - - if not self._color: - color = service.get_one_property('color') - if color: - self._color = color - self._valid = True - - # Send out the BuddyJoined signal if this is the first - # service from the buddy that we've seen - buddies = self.get_joined_buddies() - serv_owner = service.get_owner() - if serv_owner and serv_owner not in buddies and serv_owner.is_valid(): - self._dbus_helper.BuddyJoined(serv_owner.object_path()) - serv_owner.add_activity(self) - - if not service in self._services[stype]: - self._services[stype].append(service) - self._dbus_helper.ServiceAppeared(service.object_path()) - - def remove_service(self, service): - stype = service.get_type() - if not self._services.has_key(stype): - return - self._services[stype].remove(service) - self._dbus_helper.ServiceDisappeared(service.object_path()) - if len(self._services[stype]) == 0: - del self._services[stype] - - # Send out the BuddyLeft signal if this is the last - # service from the buddy - buddies = self.get_joined_buddies() - serv_owner = service.get_owner() - if serv_owner and serv_owner not in buddies and serv_owner.is_valid(): - serv_owner.remove_activity(self) - self._dbus_helper.BuddyLeft(serv_owner.object_path()) diff --git a/services/presence/Buddy.py b/services/presence/Buddy.py deleted file mode 100644 index f433fec..0000000 --- a/services/presence/Buddy.py +++ /dev/null @@ -1,533 +0,0 @@ -# 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 base64 -import logging - -import gobject -import dbus, dbus.service -from sugar import profile -from sugar.graphics import xocolor - - -PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp" -BUDDY_DBUS_OBJECT_PATH = "/org/laptop/Presence/Buddies/" -BUDDY_DBUS_INTERFACE = "org.laptop.Presence.Buddy" - -_BUDDY_KEY_COLOR = 'color' -_BUDDY_KEY_CURACT = 'curact' - -class NotFoundError(Exception): - pass - -class BuddyDBusHelper(dbus.service.Object): - def __init__(self, parent, bus_name, object_path): - self._parent = parent - self._bus_name = bus_name - self._object_path = object_path - dbus.service.Object.__init__(self, bus_name, self._object_path) - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="o") - def ServiceAppeared(self, object_path): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="o") - def ServiceDisappeared(self, object_path): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="") - def Disappeared(self): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="ao") - def CurrentActivityChanged(self, activities): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="") - def IconChanged(self): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="o") - def JoinedActivity(self, object_path): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="o") - def LeftActivity(self, object_path): - pass - - @dbus.service.signal(BUDDY_DBUS_INTERFACE, - signature="as") - def PropertyChanged(self, prop_list): - pass - - @dbus.service.method(BUDDY_DBUS_INTERFACE, - in_signature="", out_signature="ay") - def getIcon(self): - icon = self._parent.get_icon() - if not icon: - return "" - return icon - - @dbus.service.method(BUDDY_DBUS_INTERFACE, - in_signature="so", out_signature="o") - def getServiceOfType(self, stype, activity_op): - activity = None - # "/" is the placeholder for None - if activity_op != "/": - for act in self._parent.get_joined_activities(): - if act.object_path() == activity_op: - activity = act - if not activity: - raise NotFoundError("Not found") - - service = self._parent.get_service_of_type(stype, activity) - if not service: - raise NotFoundError("Not found") - return service.object_path() - - @dbus.service.method(BUDDY_DBUS_INTERFACE, - in_signature="", out_signature="ao") - def getJoinedActivities(self): - acts = [] - for act in self._parent.get_joined_activities(): - acts.append(act.object_path()) - return acts - - @dbus.service.method(BUDDY_DBUS_INTERFACE, - in_signature="", out_signature="a{sv}") - def getProperties(self): - props = {} - props['name'] = self._parent.get_name() - addr = self._parent.get_address() - if addr: - props['ip4_address'] = addr - props['owner'] = self._parent.is_owner() - color = self._parent.get_color() - if color: - props[_BUDDY_KEY_COLOR] = color - return props - - @dbus.service.method(BUDDY_DBUS_INTERFACE, - in_signature="", out_signature="o") - def getCurrentActivity(self): - activity = self._parent.get_current_activity() - if not activity: - raise NotFoundError() - return activity.object_path() - -class Buddy(object): - """Represents another person on the network and keeps track of the - activities and resources they make available for sharing.""" - - def __init__(self, bus_name, object_id, service, icon_cache): - if not bus_name: - raise ValueError("DBus bus name must be valid") - if not object_id or not isinstance(object_id, int): - raise ValueError("object id must be a valid number") - # Normal Buddy objects must be created with a valid service, - # owner objects do not - if not isinstance(self, Owner): - if not isinstance(service, Service.Service): - raise ValueError("service must be a valid service object") - - self._services = {} - self._activities = {} - - self._icon_cache = icon_cache - - self._nick_name = None - self._address = None - if service is not None: - self._nick_name = service.get_name() - self._address = service.get_source_address() - self._color = None - self._current_activity = None - self._valid = False - self._icon = None - self._icon_tries = 0 - - self._object_id = object_id - self._object_path = BUDDY_DBUS_OBJECT_PATH + str(self._object_id) - self._dbus_helper = BuddyDBusHelper(self, bus_name, self._object_path) - - self._buddy_presence_service = None - if service is not None: - self.add_service(service) - - def object_path(self): - return dbus.ObjectPath(self._object_path) - - def _request_buddy_icon_cb(self, result_status, response, user_data): - """Callback when icon request has completed.""" - from sugar.p2p import network - icon = response - service = user_data - if result_status == network.RESULT_SUCCESS: - if icon and len(icon): - icon = base64.b64decode(icon) - self._set_icon(icon) - self._icon_cache.add_icon(icon) - - if (result_status == network.RESULT_FAILED or not icon) and self._icon_tries < 3: - self._icon_tries = self._icon_tries + 1 - if self._icon_tries >= 3: - logging.debug("Failed to retrieve buddy icon for '%s'." % self._nick_name) - gobject.timeout_add(1000, self._get_buddy_icon, service, True) - return False - - def _get_buddy_icon(self, service, retry=False): - """Get the buddy's icon. Check the cache first, if its - not there get the icon from the buddy over the network.""" - if retry != True: - # Only hit the cache once - icon_hash = service.get_one_property('icon-hash') - if icon_hash is not None: - icon = self._icon_cache.get_icon(icon_hash) - if icon: - logging.debug("%s: icon cache hit for %s." % (self._nick_name, icon_hash)) - self._set_icon(icon) - return False - logging.debug("%s: icon cache miss, fetching icon from buddy..." % self._nick_name) - - from sugar.p2p import Stream - buddy_stream = Stream.Stream.new_from_service(service, start_reader=False) - writer = buddy_stream.new_writer(service) - success = writer.custom_request("get_buddy_icon", self._request_buddy_icon_cb, service) - if not success: - del writer, buddy_stream - gobject.timeout_add(1000, self._get_buddy_icon, service, True) - return False - - def _get_service_key(self, service): - return (service.get_type(), service.get_activity_id()) - - def add_service(self, service): - """Adds a new service to this buddy's service list, returning - True if the service was successfully added, and False if it was not.""" - if service.get_name() != self._nick_name: - logging.error("Service and buddy nick names doesn't match: " \ - "%s %s" % (service.get_name(), self._nick_name)) - return False - - source_addr = service.get_source_address() - if source_addr != self._address: - logging.error("Service source and buddy address doesn't " \ - "match: %s %s" % (source_addr, self._address)) - return False - return self._internal_add_service(service) - - def _internal_add_service(self, service): - service_key = self._get_service_key(service) - if service_key in self._services.keys(): - logging.error("Service already known: %s %s" % (service_key[0], - service_key[1])) - return False - - if service.get_type() == PRESENCE_SERVICE_TYPE and self._buddy_presence_service: - # already have a presence service for this buddy - logging.debug("!!! Tried to add a buddy presence service when " \ - "one already existed.") - return False - - logging.debug("Buddy %s added service type %s id %s" % (self._nick_name, - service.get_type(), service.get_activity_id())) - self._services[service_key] = service - service.set_owner(self) - - if service.get_type() == PRESENCE_SERVICE_TYPE: - self._buddy_presence_service = service - # A buddy isn't valid until its official presence - # service has been found and resolved - self._valid = True - ### TRIAL1: disable - #self._get_buddy_icon(service) - color = service.get_one_property(_BUDDY_KEY_COLOR) - if xocolor.is_valid(color): - self._color = color - self._current_activity = service.get_one_property(_BUDDY_KEY_CURACT) - # Monitor further buddy property changes, like current activity - # and color - service.connect('property-changed', - self.__buddy_presence_service_property_changed_cb) - - if self._valid: - self._dbus_helper.ServiceAppeared(service.object_path()) - return True - - def __buddy_presence_service_property_changed_cb(self, service, keys): - if _BUDDY_KEY_COLOR in keys: - new_color = service.get_one_property(_BUDDY_KEY_COLOR) - if new_color and self._color != new_color and xocolor.is_valid(new_color): - self._color = new_color - self._dbus_helper.PropertyChanged([_BUDDY_KEY_COLOR]) - if _BUDDY_KEY_CURACT in keys: - # Three cases here: - # 1) Buddy didn't publish a 'curact' key at all; we do nothing - # 2) Buddy published a blank/zero-length 'curact' key; we send - # a current-activity-changed signal for no activity - # 3) Buddy published a non-zero-length 'curact' key; we send - # a current-activity-changed signal if we know about the - # activity already, if not we postpone until the activity - # is found on the network and added to the buddy - new_curact = service.get_one_property(_BUDDY_KEY_CURACT) - if new_curact and self._current_activity != new_curact: - if not len(new_curact): - new_curact = None - self._current_activity = new_curact - if self._activities.has_key(self._current_activity): - # Case (3) above, valid activity id - activity = self._activities[self._current_activity] - if activity.is_valid(): - self._dbus_helper.CurrentActivityChanged([activity.object_path()]) - elif not self._current_activity: - # Case (2) above, no current activity - self._dbus_helper.CurrentActivityChanged([]) - - def __find_service_by_activity_id(self, actid): - for serv in self._services.values(): - if serv.get_activity_id() == actid: - return serv - return None - - def add_activity(self, activity): - if activity in self._activities.values(): - return - actid = activity.get_id() - if not self.__find_service_by_activity_id(actid): - raise RuntimeError("Tried to add activity for which we had no service") - self._activities[actid] = activity - if activity.is_valid(): - self._dbus_helper.JoinedActivity(activity.object_path()) - - # If when we received a current activity update from the buddy, - # but didn't know about that activity yet, and now we do know about - # it, we need to send out the changed activity signal - if actid == self._current_activity: - self._dbus_helper.CurrentActivityChanged([activity.object_path()]) - - def remove_service(self, service): - """Remove a service from a buddy; ie, the activity was closed - or the buddy went away.""" - if service.get_source_address() != self._address: - return - if service.get_name() != self._nick_name: - return - - if service.get_type() == PRESENCE_SERVICE_TYPE \ - and self._buddy_presence_service \ - and service != self._buddy_presence_service: - logging.debug("!!! Tried to remove a spurious buddy presence service.") - return - - service_key = self._get_service_key(service) - if self._services.has_key(service_key): - if self._valid: - self._dbus_helper.ServiceDisappeared(service.object_path()) - del self._services[service_key] - - if service.get_type() == PRESENCE_SERVICE_TYPE: - self._valid = False - self._dbus_helper.Disappeared() - - def remove_activity(self, activity): - actid = activity.get_id() - if not self._activities.has_key(actid): - return - del self._activities[actid] - if activity.is_valid(): - self._dbus_helper.LeftActivity(activity.object_path()) - - # If we just removed the buddy's current activity, - # send out a signal - if actid == self._current_activity: - self._current_activity = None - self._dbus_helper.CurrentActivityChanged([]) - - def get_joined_activities(self): - acts = [] - for act in self._activities.values(): - if act.is_valid(): - acts.append(act) - return acts - - def get_service_of_type(self, stype, activity=None): - """Return a service of a certain type, or None if the buddy - doesn't provide that service.""" - if not stype: - raise RuntimeError("Need to specify a service type.") - - if activity and not activity.is_valid(): - raise RuntimeError("Activity is not yet valid.") - - if activity: - key = (stype, activity.get_id()) - else: - key = (stype, None) - if self._services.has_key(key): - return self._services[key] - return None - - def is_valid(self): - """Return whether the buddy is valid or not. A buddy is - not valid until its official presence service has been found - and successfully resolved.""" - return self._valid - - def get_icon(self): - """Return the buddies icon, if any.""" - return self._icon - - def get_address(self): - return self._address - - def get_name(self): - return self._nick_name - - def get_color(self): - return self._color - - def get_current_activity(self): - if not self._current_activity: - return None - if not self._activities.has_key(self._current_activity): - return None - return self._activities[self._current_activity] - - def _set_icon(self, icon): - """Can only set icon for other buddies. The Owner - takes care of setting it's own icon.""" - if icon != self._icon: - self._icon = icon - self._dbus_helper.IconChanged() - - def is_owner(self): - return False - - -class Owner(Buddy): - """Class representing the owner of the machine. This is the client - portion of the Owner, paired with the server portion in Owner.py.""" - def __init__(self, ps, bus_name, object_id, icon_cache): - Buddy.__init__(self, bus_name, object_id, None, icon_cache) - self._nick_name = profile.get_nick_name() - self._color = profile.get_color().to_string() - self._ps = ps - - def add_service(self, service): - """Adds a new service to this buddy's service list, returning - True if the service was successfully added, and False if it was not.""" - if service.get_name() != self._nick_name: - logging.error("Service and buddy nick names doesn't match: " \ - "%s %s" % (service.get_name(), self._nick_name)) - return False - - # The Owner initially doesn't have an address, so the first - # service added to the Owner determines the owner's address - source_addr = service.get_source_address() - if self._address is None and service.is_local(): - self._address = source_addr - self._dbus_helper.PropertyChanged(['ip4_address']) - - # The owner bypasses address checks and only cares if - # avahi says the service is a local service - if not service.is_local(): - logging.error("Cannot add remote service to owner object.") - return False - - logging.debug("Adding owner service %s.%s at %s:%d." % (service.get_name(), - service.get_type(), service.get_source_address(), - service.get_port())) - return self._internal_add_service(service) - - def is_owner(self): - return True - - -################################################################# -# Tests -################################################################# - -import unittest -import Service - -__objid_seq = 0 -def _next_objid(): - global __objid_seq - __objid_seq = __objid_seq + 1 - return __objid_seq - - -class BuddyTestCase(unittest.TestCase): - _DEF_NAME = u"Tommy" - _DEF_STYPE = unicode(PRESENCE_SERVICE_TYPE) - _DEF_DOMAIN = u"local" - _DEF_ADDRESS = u"1.1.1.1" - _DEF_PORT = 1234 - - def __init__(self, name): - self._bus = dbus.SessionBus() - self._bus_name = dbus.service.BusName('org.laptop.Presence', bus=self._bus) - unittest.TestCase.__init__(self, name) - - def __del__(self): - del self._bus_name - del self._bus - - def _test_init_fail(self, service, fail_msg): - """Test something we expect to fail.""" - try: - objid = _next_objid() - buddy = Buddy(self._bus_name, objid, service, owner=False) - except ValueError, exc: - pass - else: - self.fail("expected a ValueError for %s." % fail_msg) - - def testService(self): - service = None - self._test_init_fail(service, "invalid service") - - def testGoodInit(self): - objid = _next_objid() - service = Service.Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, - self._DEF_ADDRESS, self._DEF_PORT) - objid = _next_objid() - buddy = Buddy(self._bus_name, objid, service) - assert buddy.get_name() == self._DEF_NAME, "buddy name wasn't correct after init." - assert buddy.get_address() == self._DEF_ADDRESS, "buddy address wasn't correct after init." - assert buddy.object_path() == BUDDY_DBUS_OBJECT_PATH + str(objid) - - def addToSuite(suite): - suite.addTest(BuddyTestCase("testService")) - suite.addTest(BuddyTestCase("testGoodInit")) - addToSuite = staticmethod(addToSuite) - - -def main(): - suite = unittest.TestSuite() - BuddyTestCase.addToSuite(suite) - runner = unittest.TextTestRunner() - runner.run(suite) - -if __name__ == "__main__": - main() diff --git a/services/presence/BuddyIconCache.py b/services/presence/BuddyIconCache.py deleted file mode 100644 index 7f9952a..0000000 --- a/services/presence/BuddyIconCache.py +++ /dev/null @@ -1,77 +0,0 @@ -# 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 os, time, md5 -from sugar import env -from sugar import util - -class BuddyIconCache(object): - """Caches icons on disk and finds them based on md5 hash.""" - def __init__(self): - ppath = env.get_profile_path() - self._cachepath = os.path.join(ppath, "cache", "buddy-icons") - if not os.path.exists(self._cachepath): - os.makedirs(self._cachepath) - - self._cache = {} - - # Read all cached icons and their sums - for fname in os.listdir(self._cachepath): - m = md5.new() - data = self._get_icon_data(fname) - if len(data) == 0: - continue - m.update(data) - printable_hash = util.printable_hash(m.digest()) - self._cache[printable_hash] = fname - del m - - def _get_icon_data(self, fname): - fd = open(os.path.join(self._cachepath, fname), "r") - data = fd.read() - fd.close() - del fd - return data - - def get_icon(self, printable_hash): - if not isinstance(printable_hash, unicode): - raise RuntimeError("printable_hash must be a unicode string.") - try: - fname = self._cache[printable_hash] - return self._get_icon_data(fname) - except KeyError: - pass - return None - - def add_icon(self, icon_data): - if len(icon_data) == 0: - return - - m = md5.new() - m.update(icon_data) - printable_hash = util.printable_hash(m.digest()) - if self._cache.has_key(printable_hash): - del m - return - - # Write the icon to disk and add an entry to our cache for it - m.update(time.asctime()) - fname = util.printable_hash(m.digest()) - fd = open(os.path.join(self._cachepath, fname), "w") - fd.write(icon_data) - fd.close() - self._cache[printable_hash] = fname - del m diff --git a/services/presence/Makefile.am b/services/presence/Makefile.am index 9b40ed3..fbd3125 100644 --- a/services/presence/Makefile.am +++ b/services/presence/Makefile.am @@ -1,21 +1,13 @@ -servicedir = $(datadir)/dbus-1/services -service_in_files = org.laptop.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 \ - PresenceService.py \ - Service.py - -bin_SCRIPTS = sugar-presence-service - -DISTCLEANFILES = $(service_DATA) - -EXTRA_DIST = $(service_in_files) $(bin_SCRIPTS) +sugardir = $(pkgdatadir)/services/presence2 +sugar_PYTHON = \ + __init__.py \ + activity.py \ + buddy.py \ + buddyiconcache.py \ + linklocal_plugin.py \ + presenceservice.py \ + server_plugin.py + +bin_SCRIPTS = sugar-presence-service2 + +EXTRA_DIST = $(bin_SCRIPTS) diff --git a/services/presence/PresenceService.py b/services/presence/PresenceService.py deleted file mode 100644 index a5ce68e..0000000 --- a/services/presence/PresenceService.py +++ /dev/null @@ -1,876 +0,0 @@ -# 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 avahi, dbus, dbus.glib, gobject -import Buddy -import Service -import Activity -import random -import logging -from sugar import util -from sugar import env -from sugar import profile -import BuddyIconCache - - -_SA_UNRESOLVED = 0 -_SA_RESOLVE_PENDING = 1 -_SA_RESOLVED = 2 -class ServiceAdv(object): - """Wrapper class to track services from Avahi.""" - def __init__(self, interface, protocol, name, stype, domain, local): - self._interface = interface - self._protocol = protocol - if not isinstance(name, unicode): - raise ValueError("service advertisement name must be unicode.") - self._name = name - if not isinstance(stype, unicode): - raise ValueError("service advertisement type must be unicode.") - self._stype = stype - if not isinstance(domain, unicode): - raise ValueError("service advertisement domain must be unicode.") - self._domain = domain - self._service = None - if not isinstance(local, bool): - raise ValueError("local must be a bool.") - self._local = local - self._state = _SA_UNRESOLVED - self._resolver = None - self._resolv_tries = 0 - - def __del__(self): - if self._resolver: - del self._resolver - - def interface(self): - return self._interface - def protocol(self): - return self._protocol - def name(self): - return self._name - def stype(self): - return self._stype - def domain(self): - return self._domain - def is_local(self): - return self._local - def resolv_tries(self): - return self._resolv_tries - def inc_resolv_tries(self): - self._resolv_tries += 1 - def service(self): - return self._service - def set_service(self, service): - if not isinstance(service, Service.Service): - raise ValueError("must be a valid service.") - if service != self._service: - self._service = service - def resolver(self): - return self._resolver - def set_resolver(self, resolver): - if resolver and not isinstance(resolver, dbus.Interface): - raise ValueError("'resolver' must be a valid dbus object") - if not resolver and self._resolver: - del self._resolver - self._resolver = resolver - def state(self): - return self._state - def set_state(self, state): - if state == _SA_RESOLVE_PENDING: - if self._state == _SA_RESOLVED: - raise ValueError("Can't reset to resolve pending from resolved.") - if state == _SA_UNRESOLVED: - self._resolv_tries = 0 - self._state = state - -class RegisteredServiceType(object): - def __init__(self, stype): - self._stype = stype - self._refcount = 1 - - def get_type(self): - return self._stype - - def ref(self): - self._refcount += 1 - - def unref(self): - self._refcount -= 1 - return self._refcount - -def _txt_to_dict(txt): - """Convert an avahi-returned TXT record formatted - as nested arrays of integers (from dbus) into a dict - of key/value string pairs.""" - prop_dict = {} - props = avahi.txt_array_to_string_array(txt) - for item in props: - key = value = None - if '=' not in item: - # No = means a boolean value of true - key = item - value = True - else: - (key, value) = item.split('=', 1) - prop_dict[key] = value - return prop_dict - - -_PRESENCE_SERVICE = "org.laptop.Presence" -_PRESENCE_DBUS_INTERFACE = "org.laptop.Presence" -_PRESENCE_OBJECT_PATH = "/org/laptop/Presence" - -class NotFoundError(Exception): - pass - -class PresenceServiceDBusHelper(dbus.service.Object): - def __init__(self, parent, bus_name): - self._parent = parent - self._bus_name = bus_name - dbus.service.Object.__init__(self, bus_name, _PRESENCE_OBJECT_PATH) - - @dbus.service.signal(_PRESENCE_DBUS_INTERFACE, - signature="o") - def BuddyAppeared(self, object_path): - pass - - @dbus.service.signal(_PRESENCE_DBUS_INTERFACE, - signature="o") - def BuddyDisappeared(self, object_path): - pass - - @dbus.service.signal(_PRESENCE_DBUS_INTERFACE, - signature="o") - def ServiceAppeared(self, object_path): - pass - - @dbus.service.signal(_PRESENCE_DBUS_INTERFACE, - signature="o") - def ServiceDisappeared(self, object_path): - pass - - @dbus.service.signal(_PRESENCE_DBUS_INTERFACE, - signature="o") - def ActivityAppeared(self, object_path): - pass - - @dbus.service.signal(_PRESENCE_DBUS_INTERFACE, - signature="o") - def ActivityDisappeared(self, object_path): - pass - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="", out_signature="ao") - def getServices(self): - ret = [] - for serv in self._parent.get_services(): - ret.append(serv.object_path()) - return ret - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="s", out_signature="ao") - def getServicesOfType(self, stype): - ret = [] - for serv in self._parent.get_services_of_type(stype): - ret.append(serv.object_path()) - return ret - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="", out_signature="ao") - def getActivities(self): - ret = [] - for act in self._parent.get_activities(): - ret.append(act.object_path()) - return ret - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="s", out_signature="o") - def getActivity(self, actid): - act = self._parent.get_activity(actid) - if not act: - raise NotFoundError("Not found") - return act.object_path() - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="", out_signature="ao") - def getBuddies(self): - ret = [] - for buddy in self._parent.get_buddies(): - ret.append(buddy.object_path()) - return ret - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="s", out_signature="o") - def getBuddyByName(self, name): - buddy = self._parent.get_buddy_by_name(name) - if not buddy: - raise NotFoundError("Not found") - return buddy.object_path() - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="s", out_signature="o") - def getBuddyByAddress(self, addr): - buddy = self._parent.get_buddy_by_address(addr) - if not buddy: - raise NotFoundError("Not found") - return buddy.object_path() - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="", out_signature="o") - def getOwner(self): - owner = self._parent.get_owner() - if not owner: - raise NotFoundError("Not found") - return owner.object_path() - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="os", out_signature="o", - sender_keyword="sender") - def joinActivity(self, activity_op, stype, sender): - found_activity = None - acts = self._parent.get_activities() - for act in acts: - if act.object_path() == activity_op: - found_activity = act - break - if not found_activity: - raise NotFoundError("The activity %s was not found." % activity_op) - return self._parent.join_activity(found_activity, stype, sender) - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="ssa{ss}sis", out_signature="o", - sender_keyword="sender") - def shareActivity(self, activity_id, stype, properties, address, port, - domain, sender=None): - if not len(address): - address = None - service = self._parent.share_activity(activity_id, stype, properties, address, - port, domain, sender) - return service.object_path() - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="ssa{ss}sis", out_signature="o", - sender_keyword="sender") - def registerService(self, name, stype, properties, address, port, domain, - sender=None): - if not len(address): - address = None - service = self._parent.register_service(name, stype, properties, address, - port, domain, sender) - return service.object_path() - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="o", out_signature="", - sender_keyword="sender") - def unregisterService(self, service_op, sender): - found_serv = None - services = self._parent.get_services() - for serv in services: - if serv.object_path() == service_op: - found_serv = serv - break - if not found_serv: - raise NotFoundError("The service %s was not found." % service_op) - return self._parent.unregister_service(found_serv, sender) - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="s", out_signature="") - def registerServiceType(self, stype): - self._parent.register_service_type(stype) - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE, - in_signature="s", out_signature="") - def unregisterServiceType(self, stype): - self._parent.unregister_service_type(stype) - - @dbus.service.method(_PRESENCE_DBUS_INTERFACE) - def start(self): - self._parent.start() - -class PresenceService(object): - def __init__(self): - # interface -> IP address: interfaces we've gotten events on so far - self._started = False - self._local_addrs = {} - - self._next_object_id = 0 - - self._buddies = {} # nick -> Buddy - self._services = {} # (name, type) -> Service - self._activities = {} # activity id -> Activity - - self._service_blacklist = {} - - # Keep track of stuff we're already browsing - self._service_type_browsers = {} - self._service_browsers = {} - - # Resolved service list - self._service_advs = [] - - # Service types we care about resolving - self._registered_service_types = [] - - # Set up the dbus service we provide - self._session_bus = dbus.SessionBus() - self._bus_name = dbus.service.BusName(_PRESENCE_SERVICE, bus=self._session_bus) - self._dbus_helper = PresenceServiceDBusHelper(self, self._bus_name) - - self._icon_cache = BuddyIconCache.BuddyIconCache() - - # Our owner object - if profile.get_nick_name(): - objid = self._get_next_object_id() - self._owner = Buddy.Owner(self, self._bus_name, - objid, self._icon_cache) - self._buddies[self._owner.get_name()] = self._owner - else: - self._owner = None - - def start(self): - if self._started: - return - self._started = True - - # Connect to Avahi for mDNS stuff - self._system_bus = dbus.SystemBus() - self._mdns_service = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, - avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) - - # Always browse .local - self._new_domain_cb(avahi.IF_UNSPEC, avahi.PROTO_INET, "local") - - # Connect to Avahi and start looking for stuff - domain_browser = self._mdns_service.DomainBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_INET, - "", avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0)) - db = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, domain_browser), avahi.DBUS_INTERFACE_DOMAIN_BROWSER) - db.connect_to_signal('ItemNew', self._new_domain_cb_glue) - - 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 get_services(self): - return self._services.values() - - def get_services_of_type(self, stype): - ret = [] - for serv in self._services.values(): - if serv.get_type() == stype: - ret.append(serv) - return ret - - def get_activities(self): - # Only return valid activities - ret = [] - for act in self._activities.values(): - if act.is_valid(): - ret.append(act) - return ret - - def get_activity(self, actid): - if self._activities.has_key(actid): - act = self._activities[actid] - if act.is_valid(): - return act - return None - - def get_buddies(self): - buddies = [] - for buddy in self._buddies.values(): - if buddy.is_valid(): - buddies.append(buddy) - return buddies - - def get_buddy_by_name(self, name): - if self._buddies.has_key(name): - if self._buddies[name].is_valid(): - return self._buddies[name] - return None - - def get_buddy_by_address(self, address): - for buddy in self._buddies.values(): - if buddy.get_address() == address and buddy.is_valid(): - return buddy - return None - - def get_owner(self): - return self._owner - - def _find_service_adv(self, interface=None, protocol=None, name=None, - stype=None, domain=None, local=None): - """Search a list of service advertisements for ones matching - certain criteria.""" - adv_list = [] - for adv in self._service_advs: - if interface and adv.interface() != interface: - continue - if protocol and adv.protocol() != protocol: - continue - if name and adv.name() != name: - continue - if stype and adv.stype() != stype: - continue - if domain and adv.domain() != domain: - continue - if local is not None and adv.is_local() != local: - continue - adv_list.append(adv) - return adv_list - - def _find_registered_service_type(self, stype): - for item in self._registered_service_types: - if item.get_type() == stype: - return item - return None - - def _handle_new_service_for_buddy(self, service, local): - """Deal with a new discovered service object.""" - # Once a service is resolved, we match it up to an existing buddy, - # or create a new Buddy if this is the first service known about the buddy - buddy_was_valid = False - name = service.get_name() - buddy = None - try: - buddy = self._buddies[name] - buddy_was_valid = buddy.is_valid() - service_added = buddy.add_service(service) - if service_added: - self._dbus_helper.ServiceAppeared(service.object_path()) - except KeyError: - source_addr = service.get_source_address() - objid = self._get_next_object_id() - buddy = Buddy.Buddy(self._bus_name, objid, service, self._icon_cache) - self._buddies[name] = buddy - self._dbus_helper.ServiceAppeared(service.object_path()) - if not buddy_was_valid and buddy.is_valid(): - self._dbus_helper.BuddyAppeared(buddy.object_path()) - return buddy - - def _handle_new_activity_service(self, service): - # If the serivce is an activity service, merge it into our activities list - actid = service.get_activity_id() - if not actid: - return - activity = None - was_valid = False - if not self._activities.has_key(actid): - objid = self._get_next_object_id() - activity = Activity.Activity(self._bus_name, objid, service) - self._activities[actid] = activity - else: - activity = self._activities[actid] - was_valid = activity.is_valid() - - if activity: - activity.add_service(service) - - # Add the activity to its buddy - # FIXME: use something other than name to attribute to buddy - try: - buddy = self._buddies[service.get_name()] - buddy.add_activity(activity) - except KeyError: - pass - - if not was_valid and activity.is_valid(): - self._dbus_helper.ActivityAppeared(activity.object_path()) - - def _handle_remove_activity_service(self, service): - actid = service.get_activity_id() - if not actid: - return - if not self._activities.has_key(actid): - return - - activity = self._activities[actid] - - activity.remove_service(service) - if len(activity.get_services()) == 0: - # Remove the activity from its buddy - # FIXME: use something other than name to attribute to buddy - try: - buddy = self._buddies[service.get_name()] - buddy.remove_activity(activity) - except KeyError: - pass - - # Kill the activity - self._dbus_helper.ActivityDisappeared(activity.object_path()) - del self._activities[actid] - - def _service_resolved_cb(self, adv, interface, protocol, full_name, - stype, domain, host, aprotocol, address, port, txt, flags, - updated): - """When the service discovery finally gets here, we've got enough - information about the service to assign it to a buddy.""" - if updated == False: - logging.debug("Resolved service '%s' type '%s' domain '%s' to " \ - " %s:%s" % (full_name, stype, domain, address, port)) - - if not adv in self._service_advs: - return False - if adv.state() != _SA_RESOLVED: - return False - - # See if we know about this service already - service = None - key = (full_name, stype) - props = _txt_to_dict(txt) - if not self._services.has_key(key): - objid = self._get_next_object_id() - service = Service.Service(self._bus_name, objid, name=full_name, - stype=stype, domain=domain, address=address, port=port, - properties=props, source_address=address) - self._services[key] = service - else: - # Already tracking this service; either: - # a) we were the one that shared it in the first place, - # and therefore the source address would not have - # been set yet - # b) the service has been updated - service = self._services[key] - if not service.get_source_address(): - service.set_source_address(address) - if not service.get_address(): - service.set_address(address) - - adv.set_service(service) - - if service and updated: - service.set_properties(props, from_network=True) - return False - - # Merge the service into our buddy and activity lists, if needed - buddy = self._handle_new_service_for_buddy(service, adv.is_local()) - if buddy and service.get_activity_id(): - self._handle_new_activity_service(service) - - return False - - def _service_resolved_cb_glue(self, adv, interface, protocol, name, - stype, domain, host, aprotocol, address, port, txt, flags): - # Avahi doesn't flag updates to existing services, so we have - # to determine that here - updated = False - if adv.state() == _SA_RESOLVED: - updated = True - - adv.set_state(_SA_RESOLVED) - gobject.idle_add(self._service_resolved_cb, adv, interface, - protocol, name, stype, domain, host, aprotocol, address, - port, txt, flags, updated) - - def _service_resolved_failure_cb(self, adv, err): - retried = False - adv.set_resolver(None) - if adv.stype() == Buddy.PRESENCE_SERVICE_TYPE: - # Retry the presence service type a few times - if adv.resolv_tries() < 4: - adv.set_state(_SA_RESOLVE_PENDING) - gobject.timeout_add(250, self._resolve_service, adv) - retried = True - logging.error("Retrying resolution of service %s.%s: %s" % (adv.name(), - adv.stype(), err)) - else: - key = adv.name() + "." + adv.stype() - if not self._service_blacklist.has_key(key): - logging.error("Adding service %s to blacklist" % key) - self._service_blacklist[key] = 1 - - if not retried: - logging.error("Error resolving service %s.%s: %s" % (adv.name(), - adv.stype(), err)) - adv.set_state(_SA_UNRESOLVED) - - def _resolve_service(self, adv): - """Resolve and lookup a ZeroConf service to obtain its address and TXT records.""" - key = adv.name() + "." + adv.stype() - if self._service_blacklist.has_key(key): - return False - - # Ask avahi to resolve this particular service - try: - path = self._mdns_service.ServiceResolverNew(dbus.Int32(adv.interface()), - dbus.Int32(adv.protocol()), adv.name(), adv.stype(), adv.domain(), - avahi.PROTO_INET, dbus.UInt32(0)) - except dbus.DBusException, e: - if str(e).find("TooManyObjectsError") >= 0: - return False - raise e - - resolver = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, path), - avahi.DBUS_INTERFACE_SERVICE_RESOLVER) - resolver.connect_to_signal('Found', lambda *args: self._service_resolved_cb_glue(adv, *args)) - resolver.connect_to_signal('Failure', lambda *args: self._service_resolved_failure_cb(adv, *args)) - adv.inc_resolv_tries() - adv.set_resolver(resolver) - return False - - def _service_appeared_cb(self, interface, protocol, full_name, stype, domain, flags): - local = flags & avahi.LOOKUP_RESULT_OUR_OWN > 0 - adv_list = self._find_service_adv(interface=interface, protocol=protocol, - name=full_name, stype=stype, domain=domain, local=local) - adv = None - if not adv_list: - adv = ServiceAdv(interface=interface, protocol=protocol, name=full_name, - stype=stype, domain=domain, local=local) - self._service_advs.append(adv) - else: - adv = adv_list[0] - - # Decompose service name if we can - (actid, buddy_name) = Service.decompose_service_name(full_name) - - # If we care about the service right now, resolve it - resolve = False - item = self._find_registered_service_type(stype) - if actid is not None or item is not None: - resolve = True - if resolve and adv.state() == _SA_UNRESOLVED: - logging.debug("Found '%s' (%d) of type '%s' in domain" \ - " '%s' on %i.%i; will resolve." % (full_name, flags, stype, - domain, interface, protocol)) - adv.set_state(_SA_RESOLVE_PENDING) - gobject.idle_add(self._resolve_service, adv) - - return False - - def _service_appeared_cb_glue(self, interface, protocol, name, stype, domain, flags): - gobject.idle_add(self._service_appeared_cb, interface, protocol, name, stype, domain, flags) - - def _service_disappeared_cb(self, interface, protocol, full_name, stype, domain, flags): - local = flags & avahi.LOOKUP_RESULT_OUR_OWN > 0 - # If it's an unresolved service, remove it from our unresolved list - adv_list = self._find_service_adv(interface=interface, protocol=protocol, - name=full_name, stype=stype, domain=domain, local=local) - if not adv_list: - return False - - # Get the service object; if none, we have nothing left to do - adv = adv_list[0] - service = adv.service() - self._service_advs.remove(adv) - del adv - if not service: - return False - - logging.debug("Service %s.%s in domain %s on %i.%i disappeared." % (full_name, - stype, domain, interface, protocol)) - - self._dbus_helper.ServiceDisappeared(service.object_path()) - self._handle_remove_activity_service(service) - - # Decompose service name if we can - (actid, buddy_name) = Service.decompose_service_name(full_name) - - # Remove the service from the buddy - try: - buddy = self._buddies[buddy_name] - except KeyError: - pass - else: - buddy.remove_service(service) - if not buddy.is_valid(): - self._dbus_helper.BuddyDisappeared(buddy.object_path()) - del self._buddies[buddy_name] - key = (service.get_full_name(), service.get_type()) - del self._services[key] - return False - - def _service_disappeared_cb_glue(self, interface, protocol, name, stype, domain, flags): - gobject.idle_add(self._service_disappeared_cb, interface, protocol, name, stype, domain, flags) - - def _new_service_type_cb(self, interface, protocol, stype, domain, flags): - # Are we already browsing this domain for this type? - if self._service_browsers.has_key((interface, protocol, stype, domain)): - return - - # Start browsing for all services of this type in this domain - try: - s_browser = self._mdns_service.ServiceBrowserNew(interface, - protocol, stype, domain, dbus.UInt32(0)) - browser_obj = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, s_browser), - avahi.DBUS_INTERFACE_SERVICE_BROWSER) - browser_obj.connect_to_signal('ItemNew', self._service_appeared_cb_glue) - browser_obj.connect_to_signal('ItemRemove', self._service_disappeared_cb_glue) - - self._service_browsers[(interface, protocol, stype, domain)] = browser_obj - except dbus.DBusException: - logging.debug("Error browsing service type '%s'" % stype) - return False - - def _new_service_type_cb_glue(self, interface, protocol, stype, domain, flags): - if len(stype) > 0: - gobject.idle_add(self._new_service_type_cb, interface, protocol, - stype, domain, flags) - - def _new_domain_cb(self, interface, protocol, domain, flags=0): - """Callback from Avahi when a new domain has been found. Start - browsing the new domain.""" - # Only use .local for now... - if domain != "local": - return - - # Are we already browsing this domain? - if self._service_type_browsers.has_key((interface, protocol, domain)): - return - - # Start browsing this domain for the services its members offer - try: - st_browser = self._mdns_service.ServiceTypeBrowserNew(interface, protocol, domain, dbus.UInt32(0)) - browser_obj = dbus.Interface(self._system_bus.get_object(avahi.DBUS_NAME, st_browser), - avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER) - except dbus.DBusException, exc: - str_exc = str(exc) - logging.error("got exception %s while attempting to browse domain %s on %i.%i" % (str_exc, domain, interface, protocol)) - if str_exc.find("The name org.freedesktop.Avahi was not provided by any .service files") >= 0: - raise Exception("Avahi does not appear to be running. '%s'" % str_exc) - else: - raise exc - logging.debug("Browsing domain '%s' on %i.%i ..." % (domain, interface, protocol)) - browser_obj.connect_to_signal('ItemNew', self._new_service_type_cb_glue) - self._service_type_browsers[(interface, protocol, domain)] = browser_obj - return False - - def _new_domain_cb_glue(self, interface, protocol, domain, flags=0): - gobject.idle_add(self._new_domain_cb, interface, protocol, domain, flags) - - def join_activity(self, activity, stype, sender): - services = activity.get_services_of_type(stype) - if not len(services): - raise NotFoundError("The service type %s was not present within " \ - "the activity %s" % (stype, activity.object_path())) - act_service = services[0] - props = act_service.get_properties() - color = activity.get_color() - if color: - props['color'] = color - return self._share_activity(activity.get_id(), stype, properties, - act_service.get_address(), act_service.get_port(), - act_service.get_domain(), sender) - - def share_activity(self, activity_id, stype, properties=None, address=None, - port=-1, domain=u"local", sender=None): - """Convenience function to share an activity with other buddies.""" - if not util.validate_activity_id(activity_id): - raise ValueError("invalid activity id") - owner_nick = self._owner.get_name() - real_name = Service.compose_service_name(owner_nick, activity_id) - if address and not isinstance(address, unicode): - raise ValueError("address must be a unicode string.") - if address == None and stype.endswith('_udp'): - # Use random currently unassigned multicast address - address = u"232.%d.%d.%d" % (random.randint(0, 254), random.randint(1, 254), - random.randint(1, 254)) - properties['address'] = address - properties['port'] = port - if port and port != -1 and (not isinstance(port, int) or port <= 1024 or port >= 65535): - raise ValueError("port must be a number between 1024 and 65535") - - color = self._owner.get_color() - if color: - properties['color'] = color - - logging.debug('Share activity %s, type %s, address %s, port %d, " \ - "properties %s' % (activity_id, stype, address, port, - properties)) - return self.register_service(real_name, stype, properties, address, - port, domain, sender) - - def register_service(self, name, stype, properties={}, address=None, - port=-1, domain=u"local", sender=None): - """Register a new service, advertising it to other Buddies on the network.""" - # Refuse to register if we can't get the dbus connection this request - # came from for some reason - if not sender: - raise RuntimeError("Service registration request must have a sender.") - - (actid, person_name) = Service.decompose_service_name(name) - if self.get_owner() and person_name != self.get_owner().get_name(): - raise RuntimeError("Tried to register a service that didn't have" \ - " Owner nick as the service name!") - if not domain or not len(domain): - domain = u"local" - if not port or port == -1: - port = random.randint(4000, 65000) - - objid = self._get_next_object_id() - service = Service.Service(self._bus_name, objid, name=name, - stype=stype, domain=domain, address=address, port=port, - properties=properties, source_address=None, - local_publisher=sender) - self._services[(name, stype)] = service - self.register_service_type(stype) - service.register(self._system_bus, self._mdns_service) - return service - - def unregister_service(self, service, sender=None): - service.unregister(sender) - - def register_service_type(self, stype): - """Requests that the Presence service look for and recognize - a certain mDNS service types.""" - if not isinstance(stype, unicode): - raise ValueError("service type must be a unicode string.") - - # If we've already registered it as a service type, ref it and return - item = self._find_registered_service_type(stype) - if item is not None: - item.ref() - return - - # Otherwise track this type now - obj = RegisteredServiceType(stype) - self._registered_service_types.append(obj) - - # Find unresolved services that match the service type - # we're now interested in, and resolve them - resolv_list = [] - - # Find services of this type - resolv_list = self._find_service_adv(stype=stype) - # Request resolution for them if they aren't in-process already - for adv in resolv_list: - if adv.state() == _SA_UNRESOLVED: - adv.set_state(_SA_RESOLVE_PENDING) - gobject.idle_add(self._resolve_service, adv) - - def unregister_service_type(self, stype): - """Stop tracking a certain mDNS service.""" - if not isinstance(stype, unicode): - raise ValueError("service type must be a unicode string.") - - # if it was found, unref it and possibly remove it - item = self._find_registered_service_type(stype) - if not item: - return - if item.unref() <= 0: - self._registered_service_types.remove(item) - del item - - -def main(): - loop = gobject.MainLoop() - ps = PresenceService() - try: - loop.run() - except KeyboardInterrupt: - print 'Ctrl+C pressed, exiting...' - -if __name__ == "__main__": - main() diff --git a/services/presence/Service.py b/services/presence/Service.py deleted file mode 100644 index 878a917..0000000 --- a/services/presence/Service.py +++ /dev/null @@ -1,602 +0,0 @@ -# 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 avahi -import sys, os -sys.path.insert(0, os.path.abspath("../../")) -from sugar import util -import dbus, dbus.service -import random -import logging -import gobject - -def compose_service_name(name, activity_id): - if isinstance(name, str): - name = unicode(name) - if not name: - raise ValueError("name must be a valid string.") - if not activity_id: - return name - if not isinstance(name, unicode): - raise ValueError("name must be in unicode.") - composed = "%s [%s]" % (name, activity_id) - return composed - -def decompose_service_name(name): - """Break a service name into the name and activity ID, if we can.""" - if not isinstance(name, unicode): - raise ValueError("name must be a valid unicode string.") - name_len = len(name) - if name_len < util.ACTIVITY_ID_LEN + 5: - return (None, name) - # check for activity id end marker - if name[name_len - 1] != "]": - return (None, name) - start = name_len - 1 - util.ACTIVITY_ID_LEN - end = name_len - 1 - # check for activity id start marker - if name[start - 1] != "[" or name[start - 2] != " ": - return (None, name) - activity_id = name[start:end] - if not util.validate_activity_id(activity_id): - return (None, name) - return (activity_id, name[:start - 2]) - -def _one_dict_differs(dict1, dict2): - diff_keys = [] - for key, value in dict1.items(): - if not dict2.has_key(key) or dict2[key] != value: - diff_keys.append(key) - return diff_keys - -def _dicts_differ(dict1, dict2): - diff_keys = [] - diff1 = _one_dict_differs(dict1, dict2) - diff2 = _one_dict_differs(dict2, dict1) - for key in diff2: - if key not in diff1: - diff_keys.append(key) - diff_keys += diff1 - return diff_keys - -def _convert_properties_to_dbus_byte_array(props): - # Ensure properties are converted to ByteArray types - # because python sometimes can't figure that out - info = dbus.Array([], signature="aay") - for k, v in props.items(): - info.append(dbus.types.ByteArray("%s=%s" % (k, v))) - return info - - -_ACTIVITY_ID_TAG = "ActivityID" -SERVICE_DBUS_INTERFACE = "org.laptop.Presence.Service" -SERVICE_DBUS_OBJECT_PATH = "/org/laptop/Presence/Services/" - -class ServiceDBusHelper(dbus.service.Object): - """Handle dbus requests and signals for Service objects""" - def __init__(self, parent, bus_name, object_path): - self._parent = parent - self._bus_name = bus_name - self._object_path = object_path - dbus.service.Object.__init__(self, bus_name, self._object_path) - - @dbus.service.signal(SERVICE_DBUS_INTERFACE, - signature="as") - def PublishedValueChanged(self, keylist): - pass - - @dbus.service.method(SERVICE_DBUS_INTERFACE, - in_signature="", out_signature="a{sv}") - def getProperties(self): - """Return service properties.""" - pary = {} - pary['name'] = self._parent.get_name() - pary['type'] = self._parent.get_type() - pary['domain'] = self._parent.get_domain() - actid = self._parent.get_activity_id() - if actid: - pary['activityId'] = actid - port = self._parent.get_port() - if port: - pary['port'] = self._parent.get_port() - addr = self._parent.get_address() - if addr: - pary['address'] = addr - source_addr = self._parent.get_source_address() - if source_addr: - pary['sourceAddress'] = source_addr - return pary - - @dbus.service.method(SERVICE_DBUS_INTERFACE, - in_signature="s") - def getPublishedValue(self, key): - """Return the value belonging to the requested key from the - service's TXT records.""" - val = self._parent.get_one_property(key) - if not val: - raise KeyError("Value was not found.") - return val - - @dbus.service.method(SERVICE_DBUS_INTERFACE, - in_signature="", out_signature="a{sv}") - def getPublishedValues(self): - pary = {} - props = self._parent.get_properties() - for key, value in props.items(): - pary[key] = str(value) - return dbus.Dictionary(pary) - - @dbus.service.method(SERVICE_DBUS_INTERFACE, - sender_keyword="sender") - def setPublishedValue(self, key, value, sender): - self._parent.set_property(key, value, sender) - - @dbus.service.method(SERVICE_DBUS_INTERFACE, - in_signature="a{sv}", sender_keyword="sender") - def setPublishedValues(self, values, sender): - if not self._parent.is_local(): - raise ValueError("Service was not not registered by requesting process!") - self._parent.set_properties(values, sender) - - -class Service(gobject.GObject): - """Encapsulates information about a specific ZeroConf/mDNS - service as advertised on the network.""" - - __gsignals__ = { - 'property-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) - } - - def __init__(self, bus_name, object_id, name, stype, domain=u"local", - address=None, port=-1, properties=None, source_address=None, - local_publisher=None): - gobject.GObject.__init__(self) - if not bus_name: - raise ValueError("DBus bus name must be valid") - if not object_id or not isinstance(object_id, int): - raise ValueError("object id must be a valid number") - - # Validate immutable options - if name and not isinstance(name, unicode): - raise ValueError("name must be unicode.") - if not name or not len(name): - raise ValueError("must specify a valid service name.") - - if stype and not isinstance(stype, unicode): - raise ValueError("service type must be in unicode.") - if not stype or not len(stype): - raise ValueError("must specify a valid service type.") - if not stype.endswith("._tcp") and not stype.endswith("._udp"): - raise ValueError("must specify a TCP or UDP service type.") - - if not isinstance(domain, unicode): - raise ValueError("domain must be in unicode.") - if domain and domain != "local": - raise ValueError("must use the 'local' domain (for now).") - - # ID of the D-Bus connection that published this service, if any. - # We only let the local publisher modify the service. - self._local_publisher = local_publisher - - self._avahi_entry_group = None - - (actid, real_name) = decompose_service_name(name) - self._name = real_name - self._full_name = name - self._stype = stype - self._domain = domain - self._port = -1 - self.set_port(port) - self._properties = {} - self._dbus_helper = None - self._internal_set_properties(properties) - - # Source address is the unicast source IP - self._source_address = None - if source_address is not None: - self.set_source_address(source_address) - - # Address is the published address, could be multicast or unicast - self._address = None - if self._properties.has_key('address'): - self.set_address(self._properties['address']) - elif address is not None: - self.set_address(address) - self._properties['address'] = address - - # Ensure that an ActivityID tag, if given, matches - # what we expect from the service type - if self._properties.has_key(_ACTIVITY_ID_TAG): - prop_actid = self._properties[_ACTIVITY_ID_TAG] - if (prop_actid and not actid) or (prop_actid != actid): - raise ValueError("ActivityID property specified, but the " \ - "service names's activity ID didn't match it: %s," \ - " %s" % (prop_actid, actid)) - self._activity_id = actid - if actid and not self._properties.has_key(_ACTIVITY_ID_TAG): - self._properties[_ACTIVITY_ID_TAG] = actid - - self._owner = None - - # register ourselves with dbus - self._object_id = object_id - self._object_path = SERVICE_DBUS_OBJECT_PATH + str(self._object_id) - self._dbus_helper = ServiceDBusHelper(self, bus_name, self._object_path) - - def object_path(self): - return dbus.ObjectPath(self._object_path) - - def get_owner(self): - return self._owner - - def set_owner(self, owner): - if self._owner is not None: - raise RuntimeError("Can only set a service's owner once") - self._owner = owner - - def is_local(self): - if self._local_publisher is not None: - return True - return False - - def get_name(self): - """Return the service's name, usually that of the - buddy who provides it.""" - return self._name - - def get_full_name(self): - return self._full_name - - def get_one_property(self, key): - """Return one property of the service, or None - if the property was not found. Cannot distinguish - between lack of a property, and a property value that - actually is None.""" - if key in self._properties.keys(): - return self._properties[key] - return None - - def get_properties(self): - """Return a python dictionary of all the service's - properties.""" - return self._properties - - def __emit_properties_changed_signal(self, keys): - if self._dbus_helper: - self._dbus_helper.PublishedValueChanged(keys) - self.emit('property-changed', keys) - - def set_property(self, key, value, sender=None): - """Set one service property""" - if not self._local_publisher: - raise ValueError("Service was not not registered by requesting process!") - if sender is not None and self._local_publisher != sender: - raise ValueError("Service was not not registered by requesting process!") - - if not isinstance(key, unicode): - raise ValueError("Key must be a unicode string.") - if not isinstance(value, unicode) and not isinstance(value, bool): - raise ValueError("Key must be a unicode string or a boolean.") - - # Ignore setting the key to it's current value - if self._properties.has_key(key): - if self._properties[key] == value: - return - - # Blank value means remove key - remove = False - if isinstance(value, unicode) and len(value) == 0: - remove = True - if isinstance(value, bool) and value == False: - remove = True - - if remove: - # If the key wasn't present, return without error - if self._properties.has_key(key): - del self._properties[key] - else: - # Otherwise set it - if isinstance(value, bool): - value = "" - self._properties[key] = value - - # if the service is locally published already, update the TXT records - if self._local_publisher and self._avahi_entry_group: - self.__internal_update_avahi_properties() - - self.__emit_properties_changed_signal([key]) - - def set_properties(self, properties, sender=None, from_network=False): - """Set all service properties in one call""" - if sender is not None and self._local_publisher != sender: - raise ValueError("Service was not not registered by requesting process!") - - self._internal_set_properties(properties, from_network) - - def _internal_set_properties(self, properties, from_network=False): - """Set the service's properties from either an Avahi - TXT record (a list of lists of integers), or a - python dictionary.""" - if not isinstance (properties, dict): - raise ValueError("Properties must be a dictionary.") - - # Make sure the properties are actually different - diff_keys = _dicts_differ(self._properties, properties) - if len(diff_keys) == 0: - return - - self._properties = {} - # Set key/value pairs on internal property list - for key, value in properties.items(): - if len(key) == 0: - continue - tmp_key = key - tmp_val = value - if not isinstance(tmp_key, unicode): - tmp_key = unicode(tmp_key) - if not isinstance(tmp_val, unicode): - tmp_val = unicode(tmp_val) - self._properties[tmp_key] = tmp_val - - # if the service is locally published already, update the TXT records - if self._local_publisher and self._avahi_entry_group and not from_network: - self.__internal_update_avahi_properties() - - self.__emit_properties_changed_signal(diff_keys) - - def __internal_update_avahi_properties(self): - info = _convert_properties_to_dbus_byte_array(self._properties) - self._avahi_entry_group.UpdateServiceTxt(avahi.IF_UNSPEC, - avahi.PROTO_UNSPEC, 0, - dbus.String(self._full_name), dbus.String(self._stype), - dbus.String(self._domain), info) - - def get_type(self): - """Return the service's service type.""" - return self._stype - - def get_activity_id(self): - """Return the activity ID this service is associated with, if any.""" - return self._activity_id - - def get_port(self): - return self._port - - def set_port(self, port): - if not isinstance(port, int) or (port <= 1024 and port > 65536): - raise ValueError("must specify a valid port number between 1024 and 65536.") - self._port = port - - def get_source_address(self): - return self._source_address - - def set_source_address(self, address): - if not address or not isinstance(address, unicode): - raise ValueError("address must be unicode") - self._source_address = address - - def get_address(self): - return self._address - - def set_address(self, address): - if not address or not isinstance(address, unicode): - raise ValueError("address must be a unicode string") - self._address = address - self._properties['address'] = address - - def get_domain(self): - """Return the ZeroConf/mDNS domain the service was found in.""" - return self._domain - - def register(self, system_bus, avahi_service): - if self._avahi_entry_group is not None: - raise RuntimeError("Service already registered!") - - obj = system_bus.get_object(avahi.DBUS_NAME, avahi_service.EntryGroupNew()) - self._avahi_entry_group = dbus.Interface(obj, avahi.DBUS_INTERFACE_ENTRY_GROUP) - - info = _convert_properties_to_dbus_byte_array(self._properties) - logging.debug("Will register service with name='%s', stype='%s'," \ - " domain='%s', address='%s', port=%d, info='%s'" % (self._full_name, - self._stype, self._domain, self._address, self._port, info)) - - self._avahi_entry_group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, - dbus.String(self._full_name), dbus.String(self._stype), - dbus.String(self._domain), dbus.String(""), # let Avahi figure the 'host' out - dbus.UInt16(self._port), info) - - self._avahi_entry_group.connect_to_signal('StateChanged', self.__entry_group_changed_cb) - self._avahi_entry_group.Commit() - - def __entry_group_changed_cb(self, state, error): - pass -# logging.debug("** %s.%s Entry group changed: state %s, error %s" % (self._full_name, self._stype, state, error)) - - def unregister(self, sender): - # Refuse to unregister if we can't get the dbus connection this request - # came from for some reason - if not sender: - raise RuntimeError("Service registration request must have a sender.") - if not self._local_publisher: - raise ValueError("Service was not a local service provided by this laptop!") - if sender is not None and self._local_publisher != sender: - raise ValueError("Service was not registered by requesting process!") - if not self._avahi_entry_group: - raise ValueError("Service was not registered by requesting process!") - self._avahi_entry_group.Free() - del self._avahi_entry_group - self._avahi_entry_group = None - - -################################################################# -# Tests -################################################################# - -import unittest - -__objid_seq = 0 -def _next_objid(): - global __objid_seq - __objid_seq = __objid_seq + 1 - return __objid_seq - - -class ServiceTestCase(unittest.TestCase): - _DEF_NAME = u"foobar" - _DEF_STYPE = u"_foo._bar._tcp" - _DEF_DOMAIN = u"local" - _DEF_ADDRESS = u"1.1.1.1" - _DEF_PORT = 1234 - _DEF_PROPS = {'foobar': 'baz'} - _STR_TEST_ARGS = [None, 0, [], {}] - - def __init__(self, name): - self._bus = dbus.SessionBus() - self._bus_name = dbus.service.BusName('org.laptop.Presence', bus=self._bus) - unittest.TestCase.__init__(self, name) - - def __del__(self): - del self._bus_name - del self._bus - - def _test_init_fail(self, name, stype, domain, address, port, properties, fail_msg): - """Test something we expect to fail.""" - try: - objid = _next_objid() - service = Service(self._bus_name, objid, name, stype, domain, address, - port, properties) - except ValueError, exc: - pass - else: - self.fail("expected a ValueError for %s." % fail_msg) - - def testName(self): - for item in self._STR_TEST_ARGS: - self._test_init_fail(item, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, - self._DEF_PORT, self._DEF_PROPS, "invalid name") - - def testType(self): - for item in self._STR_TEST_ARGS: - self._test_init_fail(self._DEF_NAME, item, self._DEF_DOMAIN, self._DEF_ADDRESS, - self._DEF_PORT, self._DEF_PROPS, "invalid service type") - self._test_init_fail(self._DEF_NAME, u"_bork._foobar", self._DEF_DOMAIN, self._DEF_ADDRESS, - self._DEF_PORT, self._DEF_PROPS, "invalid service type") - - def testDomain(self): - for item in self._STR_TEST_ARGS: - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, item, self._DEF_ADDRESS, - self._DEF_PORT, self._DEF_PROPS, "invalid domain") - # Only accept local for now - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, u"foobar", self._DEF_ADDRESS, - self._DEF_PORT, self._DEF_PROPS, "invalid domain") - # Make sure "" works - objid = _next_objid() - service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, u"", - self._DEF_ADDRESS, self._DEF_PORT, self._DEF_PROPS) - assert service, "Empty domain was not accepted!" - - def testAddress(self): - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, [], - self._DEF_PORT, self._DEF_PROPS, "invalid address") - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, {}, - self._DEF_PORT, self._DEF_PROPS, "invalid address") - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, 1234, - self._DEF_PORT, self._DEF_PROPS, "invalid address") - - def testPort(self): - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, - [], self._DEF_PROPS, "invalid port") - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, - {}, self._DEF_PROPS, "invalid port") - self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, - "adf", self._DEF_PROPS, "invalid port") - - def testGoodInit(self): - objid = _next_objid() - service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, - self._DEF_ADDRESS, self._DEF_PORT, self._DEF_PROPS) - assert service.get_name() == self._DEF_NAME, "service name wasn't correct after init." - assert service.get_type() == self._DEF_STYPE, "service type wasn't correct after init." - assert service.get_domain() == "local", "service domain wasn't correct after init." - assert service.get_address() == self._DEF_ADDRESS, "service address wasn't correct after init." - assert service.get_port() == self._DEF_PORT, "service port wasn't correct after init." - assert service.object_path() == SERVICE_DBUS_OBJECT_PATH + str(objid) - value = service.get_one_property('foobar') - assert value and value == 'baz', "service property wasn't correct after init." - - def testAvahiProperties(self): - props = [[111, 114, 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 65, 118, 97, 104, 105, 46, 99, 111, 111, 107, 105, 101, 61, 50, 54, 48, 49, 53, 52, 51, 57, 53, 50]] - key = "org.freedesktop.Avahi.cookie" - expected_value = "2601543952" - objid = _next_objid() - service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, - self._DEF_ADDRESS, self._DEF_PORT, props) - value = service.get_one_property(key) - assert value and value == expected_value, "service properties weren't correct after init." - value = service.get_one_property('bork') - assert not value, "service properties weren't correct after init." - - def testBoolProperty(self): - props = [[111, 114, 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 65, 118, 97, 104, 105, 46, 99, 111, 111, 107, 105, 101]] - key = "org.freedesktop.Avahi.cookie" - expected_value = True - objid = _next_objid() - service = Service(self._bus_name, objid, self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, - self._DEF_PORT, props) - value = service.get_one_property(key) - assert value is not None and value == expected_value, "service properties weren't correct after init." - - def testActivityService(self): - # Valid group service type, non-multicast address - actid = "4569a71b80805aa96a847f7ac1c407327b3ec2b4" - name = compose_service_name("Tommy", actid) - - # Valid activity service name, None address - objid = _next_objid() - service = Service(self._bus_name, objid, name, self._DEF_STYPE, self._DEF_DOMAIN, None, - self._DEF_PORT, self._DEF_PROPS) - assert service.get_address() == None, "address was not None as expected!" - assert service.get_activity_id() == actid, "activity id was different than expected!" - - # Valid activity service name and multicast address, ensure it works - mc_addr = u"224.0.0.34" - objid = _next_objid() - service = Service(self._bus_name, objid, name, self._DEF_STYPE, self._DEF_DOMAIN, mc_addr, - self._DEF_PORT, self._DEF_PROPS) - assert service.get_address() == mc_addr, "address was not expected address!" - - def addToSuite(suite): - suite.addTest(ServiceTestCase("testName")) - suite.addTest(ServiceTestCase("testType")) - suite.addTest(ServiceTestCase("testDomain")) - suite.addTest(ServiceTestCase("testAddress")) - suite.addTest(ServiceTestCase("testPort")) - suite.addTest(ServiceTestCase("testGoodInit")) - suite.addTest(ServiceTestCase("testAvahiProperties")) - suite.addTest(ServiceTestCase("testBoolProperty")) - suite.addTest(ServiceTestCase("testActivityService")) - addToSuite = staticmethod(addToSuite) - - -def main(): - suite = unittest.TestSuite() - ServiceTestCase.addToSuite(suite) - runner = unittest.TextTestRunner() - runner.run(suite) - -if __name__ == "__main__": - main() diff --git a/services/presence/activity.py b/services/presence/activity.py new file mode 100644 index 0000000..7711daf --- /dev/null +++ b/services/presence/activity.py @@ -0,0 +1,258 @@ +# 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, dbus.service +from sugar import util + +from telepathy.interfaces import (CHANNEL_INTERFACE) + +_ACTIVITY_PATH = "/org/laptop/Sugar/Presence/Activities/" +_ACTIVITY_INTERFACE = "org.laptop.Sugar.Presence.Activity" + +class DBusGObjectMetaclass(gobject.GObjectMeta, dbus.service.InterfaceType): pass +class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGObjectMetaclass + + +class Activity(DBusGObject): + __gtype_name__ = "Activity" + + __gsignals__ = { + 'validity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_BOOLEAN])) + } + + __gproperties__ = { + 'id' : (str, None, None, None, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), + 'name' : (str, None, None, None, gobject.PARAM_READWRITE), + 'color' : (str, None, None, None, gobject.PARAM_READWRITE), + 'type' : (str, None, None, None, gobject.PARAM_READWRITE), + 'valid' : (bool, None, None, False, gobject.PARAM_READABLE), + 'local' : (bool, None, None, False, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), + 'joined' : (bool, None, None, False, gobject.PARAM_READABLE) + } + + def __init__(self, bus_name, object_id, tp, **kwargs): + if not bus_name: + raise ValueError("DBus bus name must be valid") + 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._object_id = object_id + self._object_path = _ACTIVITY_PATH + str(self._object_id) + dbus.service.Object.__init__(self, bus_name, self._object_path) + + self._buddies = [] + self._joined = False + + # the telepathy client + self._tp = tp + self._activity_text_channel = None + + self._valid = False + self._id = None + self._color = None + self._local = False + self._type = None + + if not kwargs.get("id"): + raise ValueError("activity id is required") + if not util.validate_activity_id(kwargs['id']): + raise ValueError("Invalid activity id '%s'" % kwargs['id']) + + gobject.GObject.__init__(self, **kwargs) + if self.props.local and not self.props.valid: + raise RuntimeError("local activities require color, type, and name") + + def do_get_property(self, pspec): + if pspec.name == "id": + return self._id + elif pspec.name == "name": + return self._name + elif pspec.name == "color": + return self._color + elif pspec.name == "type": + return self._type + elif pspec.name == "valid": + return self._valid + elif pspec.name == "joined": + return self._joined + elif pspec.name == "local": + return self._local + + def do_set_property(self, pspec, value): + if pspec.name == "id": + self._id = value + elif pspec.name == "name": + self._name = value + elif pspec.name == "color": + self._color = value + elif pspec.name == "type": + if self._type: + raise RuntimeError("activity type is already set") + self._type = value + elif pspec.name == "joined": + self._joined = value + elif pspec.name == "local": + self._local = value + + self._update_validity() + + def _update_validity(self): + try: + old_valid = self._valid + if self._color and self._name 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): + pass + + @dbus.service.signal(_ACTIVITY_INTERFACE, + signature="o") + def BuddyLeft(self, buddy_path): + pass + + @dbus.service.signal(_ACTIVITY_INTERFACE, + signature="o") + def NewChannel(self, channel_path): + pass + + # dbus methods + @dbus.service.method(_ACTIVITY_INTERFACE, + in_signature="", out_signature="s") + def GetId(self): + return self.props.id + + @dbus.service.method(_ACTIVITY_INTERFACE, + in_signature="", out_signature="s") + def GetColor(self): + return self.props.color + + @dbus.service.method(_ACTIVITY_INTERFACE, + in_signature="", out_signature="s") + def GetType(self): + return self.props.type + + @dbus.service.method(_ACTIVITY_INTERFACE, + in_signature="", out_signature="") + def Join(self): + self.join() + + @dbus.service.method(_ACTIVITY_INTERFACE, + in_signature="", out_signature="ao") + def GetJoinedBuddies(self): + 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): + return self.get_channels() + + @dbus.service.method(_ACTIVITY_INTERFACE, + in_signature="", out_signature="s") + def GetName(self): + return self.props.name + + # methods + def object_path(self): + return dbus.ObjectPath(self._object_path) + + def get_joined_buddies(self): + ret = [] + for buddy in self._buddies: + if buddy.props.valid: + ret.append(buddy) + return ret + + def buddy_joined(self, buddy): + if buddy not in self._buddies: + self._buddies.append(buddy) + if self.props.valid: + self.BuddyJoined(buddy.object_path()) + + def buddy_left(self, buddy): + if buddy in self._buddies: + self._buddies.remove(buddy) + if self.props.valid: + self.BuddyLeft(buddy.object_path()) + + def join(self): + if not self._joined: + self._activity_text_channel = self._tp.join_activity(self.props.id) + self._activity_text_channel[CHANNEL_INTERFACE].connect_to_signal('Closed', self._activity_text_channel_closed_cb) + self._joined = True + + def get_channels(self): + conn = self._tp.get_connection() + # FIXME add tubes and others channels + return str(conn.service_name), conn.object_path, [self._activity_text_channel.object_path] + + def leave(self): + if self._joined: + self._activity_text_channel[CHANNEL_INTERFACE].Close() + + def _activity_text_channel_closed_cb(self): + self._joined = False + self._activity_text_channel = None + + def send_properties(self): + props = {} + props['name'] = self._name + props['color'] = self._color + props['type'] = self._type + self._tp.set_activity_properties(self.props.id, props) + + def set_properties(self, properties): + changed = False + if "name" in properties.keys(): + name = properties["name"] + if name != self._name: + self._name = name + changed = True + + if "color" in properties.keys(): + color = properties["color"] + if color != self._color: + self._color = color + changed = True + + if "type" in properties.keys(): + type = properties["type"] + if type != self._type: + self._type = type + changed = True + + if changed: + self._update_validity() diff --git a/services/presence/buddy.py b/services/presence/buddy.py new file mode 100644 index 0000000..4b6031d --- /dev/null +++ b/services/presence/buddy.py @@ -0,0 +1,396 @@ +# 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, dbus.service + +from sugar import profile +from sugar import env + +_BUDDY_PATH = "/org/laptop/Sugar/Presence/Buddies/" +_BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy" +_OWNER_INTERFACE = "org.laptop.Sugar.Presence.Buddy.Owner" + +class NotFoundError(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound' + +class DBusGObjectMetaclass(gobject.GObjectMeta, dbus.service.InterfaceType): pass +class DBusGObject(dbus.service.Object, gobject.GObject): __metaclass__ = DBusGObjectMetaclass + + +class Buddy(DBusGObject): + """Represents another person on the network and keeps track of the + activities and resources they make available for sharing.""" + + __gtype_name__ = "Buddy" + + __gsignals__ = { + 'validity-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_BOOLEAN])), + 'property-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + __gproperties__ = { + 'key' : (str, None, None, None, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), + 'icon' : (object, None, None, gobject.PARAM_READWRITE), + 'nick' : (str, None, None, None, gobject.PARAM_READWRITE), + 'color' : (str, None, None, None, gobject.PARAM_READWRITE), + 'current-activity' : (str, None, None, None, gobject.PARAM_READWRITE), + 'valid' : (bool, None, None, False, gobject.PARAM_READABLE), + 'owner' : (bool, None, None, False, gobject.PARAM_READABLE) + } + + def __init__(self, bus_name, object_id, **kwargs): + if not bus_name: + raise ValueError("DBus bus name must be valid") + if not object_id or not isinstance(object_id, int): + raise ValueError("object id must be a valid number") + + self._bus_name = bus_name + self._object_id = object_id + self._object_path = _BUDDY_PATH + str(self._object_id) + dbus.service.Object.__init__(self, self._bus_name, self._object_path) + + self._activities = {} # Activity ID -> Activity + 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 + + if not kwargs.get("key"): + raise ValueError("key required") + + gobject.GObject.__init__(self, **kwargs) + + def do_get_property(self, pspec): + if pspec.name == "key": + return self._key + elif pspec.name == "icon": + return self._icon + elif pspec.name == "nick": + return self._nick + elif pspec.name == "color": + return self._color + elif pspec.name == "current-activity": + if not self._current_activity: + return None + if not self._activities.has_key(self._current_activity): + return None + return self._current_activity + elif pspec.name == "valid": + return self._valid + elif pspec.name == "owner": + return self._owner + + def do_set_property(self, pspec, value): + if pspec.name == "icon": + if str(value) != self._icon: + self._icon = str(value) + self.IconChanged(self._icon) + self.emit('icon-changed', self._icon) + elif pspec.name == "nick": + self._nick = value + elif pspec.name == "color": + self._color = value + elif pspec.name == "current-activity": + self._current_activity = value + elif pspec.name == "key": + self._key = value + + self._update_validity() + + # dbus signals + @dbus.service.signal(_BUDDY_INTERFACE, + signature="ay") + def IconChanged(self, icon_data): + pass + + @dbus.service.signal(_BUDDY_INTERFACE, + signature="o") + def JoinedActivity(self, activity_path): + pass + + @dbus.service.signal(_BUDDY_INTERFACE, + signature="o") + def LeftActivity(self, activity_path): + pass + + @dbus.service.signal(_BUDDY_INTERFACE, + signature="a{sv}") + def PropertyChanged(self, updated): + pass + + # dbus methods + @dbus.service.method(_BUDDY_INTERFACE, + in_signature="", out_signature="ay") + def GetIcon(self): + 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): + acts = [] + for act in self.get_joined_activities(): + acts.append(act.object_path()) + return acts + + @dbus.service.method(_BUDDY_INTERFACE, + in_signature="", out_signature="a{sv}") + def GetProperties(self): + props = {} + props['nick'] = self.props.nick + props['owner'] = self.props.owner + props['key'] = self.props.key + props['color'] = self.props.color + return props + + # methods + def object_path(self): + return dbus.ObjectPath(self._object_path) + + def add_activity(self, activity): + actid = activity.props.id + if self._activities.has_key(actid): + return + self._activities[actid] = activity + if activity.props.valid: + self.JoinedActivity(activity.object_path()) + + def remove_activity(self, activity): + actid = activity.props.id + if not self._activities.has_key(actid): + return + del self._activities[actid] + if activity.props.valid: + self.LeftActivity(activity.object_path()) + + def get_joined_activities(self): + acts = [] + for act in self._activities.values(): + if act.props.valid: + acts.append(act) + return acts + + def set_properties(self, properties): + changed = False + if "nick" in properties.keys(): + nick = properties["nick"] + if nick != self._nick: + self._nick = nick + changed = True + if "color" in properties.keys(): + color = properties["color"] + if color != self._color: + self._color = color + changed = True + if "current-activity" in properties.keys(): + curact = properties["current-activity"] + if curact != self._current_activity: + self._current_activity = curact + changed = True + + if not changed: + return + + # Try emitting PropertyChanged before updating validity + # to avoid leaking a PropertyChanged signal before the buddy is + # actually valid the first time after creation + if self._valid: + self.PropertyChanged(properties) + self.emit('property-changed', properties) + + self._update_validity() + + def _update_validity(self): + 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 Owner(Buddy): + """Class representing the owner of the machine. This is the client + portion of the Owner, paired with the server portion in Owner.py.""" + + _SHELL_SERVICE = "org.laptop.Shell" + _SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner" + _SHELL_PATH = "/org/laptop/Shell" + + def __init__(self, bus_name, object_id): + 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() + + self._bus = dbus.SessionBus() + self._bus.add_signal_receiver(self._name_owner_changed_handler, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + # Connect to the shell to get notifications on Owner object + # property changes + try: + self._connect_to_shell() + except dbus.DBusException: + pass + + Buddy.__init__(self, bus_name, object_id, key=key, nick=nick, color=color, + icon=icon) + self._owner = True + + # enable this to change random buddy properties + if False: + gobject.timeout_add(5000, self._update_something) + + def _update_something(self): + import random + it = random.randint(0, 3) + if it == 0: + data = get_random_image() + self._icon_changed_cb(data) + elif it == 1: + from sugar.graphics import xocolor + xo = xocolor.XoColor() + self._color_changed_cb(xo.to_string()) + elif it == 2: + names = ["Liam", "Noel", "Guigsy", "Whitey", "Bonehead"] + foo = random.randint(0, len(names) - 1) + self._nick_changed_cb(names[foo]) + elif it == 3: + bork = random.randint(25, 65) + it = "" + for i in range(0, bork): + it += chr(random.randint(40, 127)) + from sugar import util + self._cur_activity_changed_cb(util.unique_id(it)) + return True + + def _name_owner_changed_handler(self, name, old, new): + if name != self._SHELL_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # shell went away + self._shell_owner = None + elif (not old and not len(old)) and (new and len(new)): + # shell started + self._connect_to_shell() + + def _connect_to_shell(self): + obj = self._bus.get_object(self._SHELL_SERVICE, self._SHELL_PATH) + self._shell_owner = dbus.Interface(obj, self._SHELL_OWNER_INTERFACE) + self._shell_owner.connect_to_signal('IconChanged', self._icon_changed_cb) + self._shell_owner.connect_to_signal('ColorChanged', self._color_changed_cb) + self._shell_owner.connect_to_signal('NickChanged', self._nick_changed_cb) + self._shell_owner.connect_to_signal('CurrentActivityChanged', + self._cur_activity_changed_cb) + + def _icon_changed_cb(self, icon): + self.props.icon = icon + + def _color_changed_cb(self, color): + props = {'color': color} + self.set_properties(props) + + def _nick_changed_cb(self, nick): + props = {'nick': nick} + self.set_properties(props) + + def _cur_activity_changed_cb(self, activity_id): + if not self._activities.has_key(activity_id): + # This activity is local-only + activity_id = None + props = {'current-activity': activity_id} + self.set_properties(props) + + + +def get_random_image(): + import cairo, math, random, 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/services/presence/buddyiconcache.py b/services/presence/buddyiconcache.py new file mode 100644 index 0000000..80c6813 --- /dev/null +++ b/services/presence/buddyiconcache.py @@ -0,0 +1,102 @@ +# 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 +from sugar import util + +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") + + 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/services/presence/linklocal_plugin.py b/services/presence/linklocal_plugin.py new file mode 100644 index 0000000..b8f6445 --- /dev/null +++ b/services/presence/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/services/presence/org.laptop.Presence.service.in b/services/presence/org.laptop.Sugar.Presence.service.in index 3739d9e..3739d9e 100644 --- a/services/presence/org.laptop.Presence.service.in +++ b/services/presence/org.laptop.Sugar.Presence.service.in diff --git a/services/presence/presenceservice.py b/services/presence/presenceservice.py new file mode 100644 index 0000000..d4ad38c --- /dev/null +++ b/services/presence/presenceservice.py @@ -0,0 +1,332 @@ +# Copyright (C) 2007, 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 gobject +import dbus, dbus.service, dbus.glib +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, CONNECTION_HANDLE_TYPE_CONTACT) + +from server_plugin import ServerPlugin +from linklocal_plugin import LinkLocalPlugin +from sugar import util + +from buddy import Buddy, Owner +from activity import Activity + +_PRESENCE_SERVICE = "org.laptop.Sugar.Presence" +_PRESENCE_INTERFACE = "org.laptop.Sugar.Presence" +_PRESENCE_PATH = "/org/laptop/Sugar/Presence" + + +class NotFoundError(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound' + + +class PresenceService(dbus.service.Object): + def __init__(self): + self._next_object_id = 0 + + self._buddies = {} # key -> Buddy + self._handles_buddies = {} # tp client -> (handle -> Buddy) + self._activities = {} # activity id -> Activity + + bus = dbus.SessionBus() + self._bus_name = dbus.service.BusName(_PRESENCE_SERVICE, bus=bus) + + # Create the Owner object + objid = self._get_next_object_id() + self._owner = Owner(self._bus_name, objid) + self._buddies[self._owner.props.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] = {} + + dbus.service.Object.__init__(self, self._bus_name, _PRESENCE_PATH) + + def _server_status_cb(self, plugin, status, reason): + if status == CONNECTION_STATUS_CONNECTED: + pass + # TEST + id = util.unique_id() + self._share_activity(id, "org.laptop.Sugar.Test", + "Chat of %s" % self._owner.props.nick, []) + + def _contact_online(self, tp, handle, props): + new_buddy = False + key = props['key'] + buddy = self._buddies.get(key) + if not buddy: + # we don't know yet this buddy + objid = self._get_next_object_id() + buddy = Buddy(self._bus_name, objid, key=key) + buddy.connect("validity-changed", self._buddy_validity_changed_cb) + self._buddies[key] = buddy + + buddies = self._handles_buddies[tp] + buddies[handle] = buddy + # store the handle of the buddy for this CM + buddy.handles[tp] = handle + + buddy.set_properties(props) + + def _buddy_validity_changed_cb(self, buddy, valid): + if valid: + self.BuddyAppeared(buddy.object_path()) + print "New Buddy: %s (%s)" % (buddy.props.nick, buddy.props.color) + else: + self.BuddyDisappeared(buddy.object_path()) + print "Buddy left: %s (%s)" % (buddy.props.nick, buddy.props.color) + + def _contact_offline(self, tp, handle): + buddy = self._handles_buddies[tp].pop(handle) + key = buddy.props.key + + # the handle of the buddy for this CM is not valid anymore + buddy.handles.pop(tp) + if not buddy.handles: + if buddy.props.valid: + self.BuddyDisappeared(buddy.object_path()) + print "Buddy left: %s (%s)" % (buddy.props.nick, buddy.props.color) + self._buddies.pop(key) + + 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: + print "Buddy %s icon updated" % buddy.props.key + buddy.props.icon = avatar + + def _buddy_properties_changed(self, tp, handle, prop): + buddy = self._handles_buddies[tp].get(handle) + if buddy: + buddy.set_properties(prop) + #print "Buddy %s properties updated" % buddy.props.key + + def _new_activity(self, activity_id, tp): + try: + objid = self._get_next_object_id() + activity = Activity(self._bus_name, objid, tp, id=activity_id) + except Exception, e: + print "Invalid activity: %s" % e + return None + + activity.connect("validity-changed", self._activity_validity_changed_cb) + + self._activities[activity_id] = activity + + # FIXME + # Use values from the network + #import random + #names = ["Tommy", "Susie", "Jill", "Bryan", "Nathan", "Sophia", "Haley", "Jimmy"] + #name = names[random.randint(0, len(names) - 1)] + #activity.props.name = "Chat with %s" % name + #activity.props.type = "org.laptop.Sugar.Chat" + #from sugar.graphics import xocolor + #color = xocolor.XoColor().to_string() + #activity.props.color = color + + return activity + + def _remove_activity(self, activity): + print "remove activity", activity.props.id + + self.ActivityDisappeared(activity.object_path()) + del self._activities[activity.props.id] + + def _buddy_activities_changed(self, tp, contact_handle, activities): + print "------------activities changed-------------" + 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? + print "contact_activities_changed: buddy unknow" + 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: + print "buddy", contact_handle, "joined", act + activity = self._activities.get(act) + if not activity: + # new activity, can fail + activity = self._new_activity(act, tp) + + if activity: + activity.buddy_joined(buddy) + buddy.add_activity(activity) + + activities_left = old_activities - new_activities + for act in activities_left: + print "buddy", contact_handle, "left", act + activity = self._activities.get(act) + if not activity: + continue + + activity.buddy_left(buddy) + buddy.remove_activity(activity) + + if not activity.get_joined_buddies(): + self._remove_activity(activity) + + 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, out_signature="ao") + def GetActivities(self): + ret = [] + for act in self._activities.values(): + ret.append(act.object_path()) + return ret + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature="s", out_signature="o") + def GetActivityById(self, actid): + if self._activities.has_key(actid): + return self._activities[actid].object_path() + raise NotFoundError("The activity was not found.") + + @dbus.service.method(_PRESENCE_INTERFACE, out_signature="ao") + def GetBuddies(self): + ret = [] + for buddy in self._buddies.values(): + ret.append(buddy.object_path()) + return ret + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature="ay", out_signature="o") + def GetBuddyByPublicKey(self, key): + if self._buddies.has_key(key): + return self._buddies[key].object_path() + raise NotFoundError("The buddy was not found.") + + @dbus.service.method(_PRESENCE_INTERFACE, out_signature="o") + def GetOwner(self): + if not self._owner: + raise NotFoundError("The owner was not found.") + else: + return self._owner.get_object_path() + + @dbus.service.method(_PRESENCE_INTERFACE, in_signature="sssa{sv}", out_signature="o") + def ShareActivity(self, actid, atype, name, properties): + activity = self._share_activity(actid, atype, name, properties) + return activity.object_path() + + def cleanup(self): + for tp in self._handles_buddies: + tp.cleanup() + + def _share_activity(self, actid, atype, name, properties): + 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._bus_name, objid, 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.join() + activity.send_properties() + + return activity + + def _activity_validity_changed_cb(self, activity, valid): + if valid: + self.ActivityAppeared(activity.object_path()) + print "New Activity: %s (%s)" % (activity.props.name, activity.props.id) + else: + self.ActivityDisappeared(activity.object_path()) + print "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(): + loop = gobject.MainLoop() + ps = PresenceService() + try: + loop.run() + except KeyboardInterrupt: + ps.cleanup() + print 'Ctrl+C pressed, exiting...' + +if __name__ == "__main__": + main() diff --git a/services/presence/server_plugin.py b/services/presence/server_plugin.py new file mode 100644 index 0000000..6e309a8 --- /dev/null +++ b/services/presence/server_plugin.py @@ -0,0 +1,518 @@ +# 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 +from sugar import profile +from sugar import util +from sugar import env +import gtk +from buddyiconcache import BuddyIconCache +import logging +import os +import hashlib + +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) +from telepathy.constants import ( + CONNECTION_HANDLE_TYPE_NONE, CONNECTION_HANDLE_TYPE_CONTACT, + CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_CONNECTING, + CONNECTION_HANDLE_TYPE_LIST, CONNECTION_HANDLE_TYPE_CONTACT, CONNECTION_HANDLE_TYPE_ROOM, + CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED) + +CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' + +_PROTOCOL = "jabber" + +class InvalidBuddyError(Exception): + pass + +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): + __gsignals__ = { + 'contact-online': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'contact-offline': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_INT, gobject.TYPE_INT])), + 'avatar-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'buddy-properties-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'buddy-activities-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-properties-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + def __init__(self, registry, owner): + gobject.GObject.__init__(self) + + self._icon_cache = BuddyIconCache() + + self._gabble_mgr = registry.GetManager('gabble') + self._online_contacts = {} # handle -> jid + + self._activities = {} # activity id -> handle + self._joined_activities = [] # (activity_id, handle of the activity channel) + + self._owner = owner + self._owner.connect("property-changed", self._owner_property_changed_cb) + self._owner.connect("icon-changed", self._owner_icon_changed_cb) + + self._account = self._get_account_info() + + self._conn = self._init_connection() + + self._reconnect_id = 0 + + def _owner_property_changed_cb(self, owner, properties): + print "Owner properties changed: %s" % properties + self._set_self_buddy_info() + + def _owner_icon_changed_cb(self, owner, icon): + print "Owner icon changed to size %d" % len(str(icon)) + self._upload_avatar() + + def _get_account_info(self): + account_info = {} + + pubkey = self._owner.props.key + + server = profile.get_server() + if not server: + account_info['server'] = 'olpc.collabora.co.uk' + else: + account_info['server'] = server + + registered = profile.get_server_registered() + account_info['register'] = not registered + + khash = util.printable_hash(util._sha_data(pubkey)) + account_info['account'] = "%s@%s" % (khash, account_info['server']) + + account_info['password'] = profile.get_private_key_hash() + print account_info + return account_info + + def _find_existing_connection(self): + 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("/org/freedesktop/Telepathy/Connection/gabble/jabber/"): + continue + if item[CONN_INTERFACE].GetProtocol() != _PROTOCOL: + continue + if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_CONNECTED: + test_handle = item[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_CONTACT, [our_name])[0] + if item[CONN_INTERFACE].GetSelfHandle() != test_handle: + continue + return item + return None + + def get_connection(self): + return self._conn + + def _init_connection(self): + conn = self._find_existing_connection() + if not conn: + acct = self._account.copy() + + # Create a new connection + name, path = self._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) + + return conn + + def _request_list_channel(self, name): + handle = self._conn[CONN_INTERFACE].RequestHandles( + CONNECTION_HANDLE_TYPE_LIST, [name])[0] + chan_path = self._conn[CONN_INTERFACE].RequestChannel( + CHANNEL_TYPE_CONTACT_LIST, CONNECTION_HANDLE_TYPE_LIST, + handle, True) + channel = Channel(self._conn._dbus_object._named_service, chan_path) + # hack + channel._valid_interfaces.add(CHANNEL_INTERFACE_GROUP) + return channel + + def _connected_cb(self): + if self._account['register']: + # we successfully register this account + profile.set_server_registered() + + # the group of contacts who may receive your presence + publish = self._request_list_channel('publish') + publish_handles, local_pending, remote_pending = publish[CHANNEL_INTERFACE_GROUP].GetAllMembers() + + # the group of contacts for whom you wish to receive presence + subscribe = self._request_list_channel('subscribe') + subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() + + if local_pending: + # accept pending subscriptions + #print 'pending: %r' % local_pending + publish[CHANNEL_INTERFACE_GROUP].AddMembers(local_pending, '') + + not_subscribed = list(set(publish_handles) - set(subscribe_handles)) + self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() + self._online_contacts[self_handle] = self._account['account'] + + for handle in not_subscribed: + # request subscriptions from people subscribed to us if we're not subscribed to them + subscribe[CHANNEL_INTERFACE_GROUP].AddMembers([self_handle], '') + + if CONN_INTERFACE_BUDDY_INFO not in self._conn.get_valid_interfaces(): + print 'OLPC information not available' + self.cleanup() + return + + 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) + + try: + self._set_self_buddy_info() + except RuntimeError, e: + print e + self.cleanup() + return + + # Request presence for everyone on the channel + self._conn[CONN_INTERFACE_PRESENCE].GetPresence(subscribe_handles) + + def _upload_avatar(self): + icon_data = self._owner.props.icon + + md5 = hashlib.md5() + md5.update(icon_data) + hash = md5.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: + print "server does not accept JPEG format avatars." + return + + img_data = _get_buddy_icon_at_size(icon_data, min(maxw, 96), min(maxh, 96), maxsize) + token = self._conn[CONN_INTERFACE_AVATARS].SetAvatar(img_data, "image/jpeg") + self._icon_cache.set_avatar(hash, token) + + def join_activity(self, act): + handle = self._activities.get(act) + + if not handle: + handle = self._conn[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_ROOM, [act])[0] + self._activities[act] = handle + + if (act, handle) in self._joined_activities: + print "%s already joined" % act + return + + chan_path = self._conn[CONN_INTERFACE].RequestChannel( + CHANNEL_TYPE_TEXT, CONNECTION_HANDLE_TYPE_ROOM, + handle, True) + channel = Channel(self._conn._dbus_object._named_service, chan_path) + + self._joined_activities.append((act, handle)) + self._conn[CONN_INTERFACE_BUDDY_INFO].SetActivities(self._joined_activities) + + return channel + + def _set_self_buddy_info(self): + # Set our OLPC buddy properties + props = {} + props['color'] = self._owner.props.color + props['key'] = self._owner.props.key + try: + self._conn[CONN_INTERFACE_BUDDY_INFO].SetProperties(props) + except dbus.DBusException, e: + if str(e).find("Server does not support PEP") >= 0: + raise RuntimeError("Server does not support PEP") + + name = self._owner.props.nick + self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() + self._conn[CONN_INTERFACE_ALIASING].SetAliases( {self_handle : name} ) + + self._conn[CONN_INTERFACE_BUDDY_INFO].SetActivities(self._joined_activities) + + 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 = "" + self._conn[CONN_INTERFACE_BUDDY_INFO].SetCurrentActivity(cur_activity, cur_activity_handle) + + self._upload_avatar() + + def _get_handle_for_activity(self, activity_id): + for (act, handle) in self._joined_activities: + if activity_id == act: + return handle + return None + + def _status_changed_cb(self, state, reason): + if state == CONNECTION_STATUS_CONNECTING: + print 'connecting: %r' % reason + elif state == CONNECTION_STATUS_CONNECTED: + print 'connected: %r' % reason + self._connected_cb() + self.emit('status', state, int(reason)) + elif state == CONNECTION_STATUS_DISCONNECTED: + print 'disconnected: %r' % reason + self.emit('status', state, int(reason)) + self._conn = None + if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED: + # FIXME: handle connection failure; retry later? + pass + return False + + def start(self): + print "Trying to connect..." + # If the connection is already connected query initial contacts + conn_status = self._conn[CONN_INTERFACE].GetStatus() + if conn_status == CONNECTION_STATUS_CONNECTED: + self._connected_cb() + subscribe = self._request_list_channel('subscribe') + subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() + self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) + elif conn_status == CONNECTION_STATUS_CONNECTING: + pass + else: + self._conn[CONN_INTERFACE].Connect(reply_handler=self._connect_reply_cb, + error_handler=self._connect_error_cb) + + def _connect_reply_cb(self): + if self._reconnect_id > 0: + gobject.source_remove(self._reconnect_id) + + def _reconnect(self): + self._reconnect_id = 0 + self.start() + return False + + def _connect_error_cb(self, exception): + print "Connect error: %s" % exception + if not self._reconnect_id: + self._reconnect_id = gobject.timeout_add(10000, self._reconnect) + + def cleanup(self): + if not self._conn: + return + self._conn[CONN_INTERFACE].Disconnect() + + def _contact_offline(self, handle): + self.emit("contact-offline", handle) + del self._online_contacts[handle] + + def _contact_online(self, handle): + try: + props = self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties(handle) + except dbus.DBusException, e: + if str(e).startswith("org.freedesktop.DBus.Error.NoReply"): + raise InvalidBuddyError("couldn't get properties") + props = {} + logging.debug("Error getting buddy properties: %s" % e) + + if not props.has_key('color'): + raise InvalidBuddyError("no color") + if not props.has_key('key'): + raise InvalidBuddyError("no key") + + jid = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] + nick = self._conn[CONN_INTERFACE_ALIASING].RequestAliases([handle])[0] + if not nick: + raise InvalidBuddyError("no name") + props['nick'] = nick + + self._online_contacts[handle] = jid + self.emit("contact-online", handle, props) + + activities = self._conn[CONN_INTERFACE_BUDDY_INFO].GetActivities(handle) + self._buddy_activities_changed_cb(handle, activities) + + def _presence_update_cb(self, presence): + for handle in presence: + timestamp, statuses = presence[handle] + online = handle in self._online_contacts + for status, params in statuses.items(): + jid = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] + olstr = "ONLINE" + if not online: olstr = "OFFLINE" + print "Handle %s (%s) was %s, status now '%s'." % (handle, jid, olstr, status) + if not online and status in ["available", "away", "brb", "busy", "dnd", "xa"]: + try: + self._contact_online(handle) + except InvalidBuddyError, e: + print "Not adding %s because %s" % (handle, e) + elif online and status in ["offline", "invisible"]: + self._contact_offline(handle) + + def _avatar_updated_cb(self, handle, new_avatar_token): + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): + # ignore network events for Owner property changes since those + # are handled locally + return + + jid = self._online_contacts[handle] + icon = self._icon_cache.get_icon(jid, new_avatar_token) + if not icon: + # cache miss + avatar, mime_type = self._conn[CONN_INTERFACE_AVATARS].RequestAvatar(handle) + icon = ''.join(map(chr, avatar)) + self._icon_cache.store_icon(jid, new_avatar_token, icon) + + self.emit("avatar-updated", handle, icon) + + def _alias_changed_cb(self, aliases): + for handle, alias in aliases: + prop = {'nick': alias} + #print "Buddy %s alias changed to %s" % (handle, alias) + self._buddy_properties_changed_cb(handle, prop) + + def _buddy_properties_changed_cb(self, handle, properties): + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): + # ignore network events for Owner property changes since those + # are handled locally + return + self.emit("buddy-properties-changed", handle, properties) + + def _buddy_activities_changed_cb(self, handle, activities): + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): + # ignore network events for Owner activity changes since those + # are handled locally + 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): + if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): + # ignore network events for Owner current activity changes since those + # are handled locally + return + + if not len(activity) or not util.validate_activity_id(activity): + activity = None + prop = {'current-activity': activity} + logging.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): + if handle_type == CONNECTION_HANDLE_TYPE_ROOM and channel_type == CHANNEL_TYPE_TEXT: + channel = Channel(self._conn._dbus_object._named_service, object_path) + + # hack + channel._valid_interfaces.add(CHANNEL_INTERFACE_GROUP) + + current, local_pending, remote_pending = channel[CHANNEL_INTERFACE_GROUP].GetAllMembers() + + if local_pending: + for act_id, act_handle in self._activities.items(): + if handle == act_handle: + self.emit("activity-invitation", act_id) + + elif handle_type == CONNECTION_HANDLE_TYPE_CONTACT and \ + channel_type in [CHANNEL_TYPE_TEXT, CHANNEL_TYPE_STREAMED_MEDIA]: + self.emit("private-invitation", object_path) + + def set_activity_properties(self, act_id, props): + handle = self._activities.get(act_id) + + if not handle: + print "set_activity_properties: handle unkown" + return + + self._conn[CONN_INTERFACE_ACTIVITY_PROPERTIES].SetProperties(handle, props) + + def _activity_properties_changed_cb(self, room, properties): + for act_id, act_handle in self._activities.items(): + if room == act_handle: + self.emit("activity-properties-changed", act_id, properties) diff --git a/services/presence/sugar-presence-service b/services/presence/sugar-presence-service index be9c0e0..83ff58d 100755 --- a/services/presence/sugar-presence-service +++ b/services/presence/sugar-presence-service @@ -24,12 +24,12 @@ import os from sugar import logger from sugar import env -sys.path.insert(0, env.get_service_path('presence')) +sys.path.insert(0, env.get_service_path('presence2')) logger.start('presenceservice') -import PresenceService +import presenceservice logging.info('Starting presence service') -PresenceService.main() +presenceservice.main() |