diff options
author | Marco Pesenti Gritti <marco@localhost.localdomain> | 2006-09-15 11:54:16 (GMT) |
---|---|---|
committer | Marco Pesenti Gritti <marco@localhost.localdomain> | 2006-09-15 11:54:16 (GMT) |
commit | 5f99dcf9a5375847c970cfd769b3c85b84cf66b0 (patch) | |
tree | 69ebda2095a13a9fdef14eda2daa0b08da24a268 /services/presence/Buddy.py | |
parent | bcc1740f7f514f04629cecb7536c93d4feaf83cf (diff) |
Move the presence service out of the shell
Diffstat (limited to 'services/presence/Buddy.py')
-rw-r--r-- | services/presence/Buddy.py | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/services/presence/Buddy.py b/services/presence/Buddy.py new file mode 100644 index 0000000..5d13c29 --- /dev/null +++ b/services/presence/Buddy.py @@ -0,0 +1,402 @@ +import base64 +import logging + +import gobject +import dbus, dbus.service +from sugar import env + + +PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp" +BUDDY_DBUS_OBJECT_PATH = "/org/laptop/Presence/Buddies/" +BUDDY_DBUS_INTERFACE = "org.laptop.Presence.Buddy" + +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 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="s", out_signature="o") + def getServiceOfType(self, stype): + service = self._parent.get_service_of_type(stype) + 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['color'] = self._parent.get_color() + return props + + +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): + if not bus_name: + raise ValueError("DBus bus name must be valid") + if not object_id or type(object_id) != type(1): + 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._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._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) + + 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) + logging.debug("Buddy icon for '%s' is size %d" % (self._nick_name, len(icon))) + self._set_icon(icon) + + if (result_status == network.RESULT_FAILED or not icon) and self._icon_tries < 3: + self._icon_tries = self._icon_tries + 1 + logging.debug("Failed to retrieve buddy icon for '%s' on try %d of %d" % (self._nick_name, \ + self._icon_tries, 3)) + gobject.timeout_add(1000, self._request_buddy_icon, service) + return False + + def _request_buddy_icon(self, service): + """Contact the buddy to retrieve the buddy icon.""" + 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._request_buddy_icon, service) + 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 + + 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: + # A buddy isn't valid until its official presence + # service has been found and resolved + self._valid = True + print 'Requesting buddy icon %s' % self._nick_name + self._request_buddy_icon(service) + self._color = service.get_one_property('color') + if self._color: + self._dbus_helper.PropertyChanged(['color']) + + if self._valid: + self._dbus_helper.ServiceAppeared(service.object_path()) + return True + + def add_activity(self, activity): + actid = activity.get_id() + if activity in self._activities.values(): + raise RuntimeError("Tried to add activity twice") + found = False + for serv in self._services.values(): + if serv.get_activity_id() == activity.get_id(): + found = True + break + if not found: + 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()) + + 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 + 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 + + 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()) + + 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=None, 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 _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): + Buddy.__init__(self, bus_name, object_id, None) + self._nick_name = env.get_nick_name() + self._color = env.get_color() + 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() |