diff --git a/services/presence/buddy.py b/services/presence/buddy.py index f43bcabe..e1775957 100644 --- a/services/presence/buddy.py +++ b/services/presence/buddy.py @@ -18,9 +18,11 @@ import os import gobject import dbus, dbus.service +from ConfigParser import ConfigParser, NoOptionError -from sugar import profile -from sugar import env +from sugar import env, profile, util +import logging +import random _BUDDY_PATH = "/org/laptop/Sugar/Presence/Buddies/" _BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy" @@ -39,8 +41,6 @@ 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])), @@ -246,16 +246,58 @@ class Buddy(DBusGObject): except AttributeError: self._valid = False +class GenericOwner(Buddy): + __gtype_name__ = "GenericOwner" -class Owner(Buddy): + __gproperties__ = { + 'registered' : (bool, None, None, False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), + 'server' : (str, None, None, None, gobject.PARAM_READABLE | gobject.PARAM_CONSTRUCT), + 'key-hash' : (str, None, None, None, gobject.PARAM_READABLE | gobject.PARAM_CONSTRUCT) + } + + def __init__(self, bus_name, object_id, **kwargs): + self._server = 'olpc.collabora.co.uk' + self._key_hash = None + self._registered = False + if kwargs.has_key("server"): + self._server = kwargs["server"] + del kwargs["server"] + if kwargs.has_key("key_hash"): + self._key_hash = kwargs["key_hash"] + del kwargs["key_hash"] + if kwargs.has_key("registered"): + self._registered = kwargs["registered"] + del kwargs["registered"] + + Buddy.__init__(self, bus_name, object_id, **kwargs) + self._owner = True + + def get_registered(self): + return self._registered + + def get_server(self): + return self._server + + def get_key_hash(self): + return self._key_hash + + def set_registered(self, registered): + raise RuntimeError("Subclasses must implement") + +class ShellOwner(GenericOwner): """Class representing the owner of the machine. This is the client portion of the Owner, paired with the server portion in Owner.py.""" + __gtype_name__ = "ShellOwner" + _SHELL_SERVICE = "org.laptop.Shell" _SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner" _SHELL_PATH = "/org/laptop/Shell" - def __init__(self, bus_name, object_id): + def __init__(self, bus_name, object_id, test=False): + server = profile.get_server() + key_hash = profile.get_private_key_hash() + registered = profile.get_server_registered() key = profile.get_pubkey() nick = profile.get_nick_name() color = profile.get_color().to_string() @@ -265,10 +307,14 @@ class Owner(Buddy): icon = f.read() f.close() + GenericOwner.__init__(self, bus_name, object_id, key=key, nick=nick, + color=color, icon=icon, server=server, key_hash=key_hash, + registered=registered) + self._bus = dbus.SessionBus() self._bus.add_signal_receiver(self._name_owner_changed_handler, - signal_name="NameOwnerChanged", - dbus_interface="org.freedesktop.DBus") + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") # Connect to the shell to get notifications on Owner object # property changes @@ -277,36 +323,9 @@ class Owner(Buddy): 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 set_registered(self, value): + if value: + profile.set_server_registered() def _name_owner_changed_handler(self, name, old, new): if name != self._SHELL_SERVICE: @@ -346,8 +365,204 @@ class Owner(Buddy): self.set_properties(props) +class TestOwner(GenericOwner): + """Class representing the owner of the machine. This test owner + changes random attributes periodically.""" -def get_random_image(): + __gtype_name__ = "TestOwner" + + def __init__(self, bus_name, object_id, test_num): + self._cp = ConfigParser() + self._section = "Info" + + self._cfg_file = os.path.join(env.get_profile_path(), 'test-buddy-%d' % test_num) + + (pubkey, privkey, registered) = self._load_config() + if not pubkey or not len(pubkey) or not privkey or not len(privkey): + (pubkey, privkey) = _get_new_keypair(test_num) + + if not pubkey or not privkey: + raise RuntimeError("Couldn't get or create test buddy keypair") + + self._save_config(pubkey, privkey, registered) + privkey_hash = util.printable_hash(util._sha_data(privkey)) + + nick = _get_random_name() + from sugar.graphics import xocolor + color = xocolor.XoColor().to_string() + icon = _get_random_image() + + GenericOwner.__init__(self, bus_name, object_id, key=pubkey, nick=nick, + color=color, icon=icon, registered=registered, key_hash=privkey_hash) + + # Change a random property ever 10 seconds + gobject.timeout_add(10000, self._update_something) + + def set_registered(self, value): + if value: + self._registered = True + + def _load_config(self): + if not os.path.exists(self._cfg_file): + return (None, None, False) + if not self._cp.read([self._cfg_file]): + return (None, None, False) + if not self._cp.has_section(self._section): + return (None, None, False) + + try: + pubkey = self._cp.get(self._section, "pubkey") + privkey = self._cp.get(self._section, "privkey") + registered = self._cp.get(self._section, "registered") + return (pubkey, privkey, registered) + except NoOptionError: + pass + + return (None, None, False) + + def _save_config(self, pubkey, privkey, registered): + # Save config again + if not self._cp.has_section(self._section): + self._cp.add_section(self._section) + self._cp.set(self._section, "pubkey", pubkey) + self._cp.set(self._section, "privkey", privkey) + self._cp.set(self._section, "registered", registered) + f = open(self._cfg_file, 'w') + self._cp.write(f) + f.close() + + def _update_something(self): + it = random.randint(0, 3) + if it == 0: + self.props.icon = _get_random_image() + elif it == 1: + from sugar.graphics import xocolor + props = {'color': xocolor.XoColor().to_string()} + self.set_properties(props) + elif it == 2: + props = {'nick': _get_random_name()} + self.set_properties(props) + elif it == 3: + bork = random.randint(25, 65) + it = "" + for i in range(0, bork): + it += chr(random.randint(40, 127)) + from sugar import util + props = {'current-activity': util.unique_id(it)} + self.set_properties(props) + return True + + +def _hash_private_key(self): + self.privkey_hash = None + + key_path = os.path.join(env.get_profile_path(), 'owner.key') + try: + f = open(key_path, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading private key: %s" % e) + return + + key = "" + for l in lines: + l = l.strip() + if l.startswith("-----BEGIN DSA PRIVATE KEY-----"): + continue + if l.startswith("-----END DSA PRIVATE KEY-----"): + continue + key += l + if not len(key): + logging.error("Error parsing public key.") + + # hash it + key_hash = util._sha_data(key) + self.privkey_hash = util.printable_hash(key_hash) + +def _extract_public_key(keyfile): + try: + f = open(keyfile, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading public key: %s" % e) + return None + + # Extract the public key + magic = "ssh-dss " + key = "" + for l in lines: + l = l.strip() + if not l.startswith(magic): + continue + key = l[len(magic):] + break + if not len(key): + logging.error("Error parsing public key.") + return None + return key + +def _extract_private_key(keyfile): + # Extract the private key + try: + f = open(keyfile, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading private key: %s" % e) + return None + + key = "" + for l in lines: + l = l.strip() + if l.startswith("-----BEGIN DSA PRIVATE KEY-----"): + continue + if l.startswith("-----END DSA PRIVATE KEY-----"): + continue + key += l + if not len(key): + logging.error("Error parsing private key.") + return None + return key + +def _get_new_keypair(num): + # Generate keypair + privkeyfile = os.path.join("/tmp", "test%d.key" % num) + pubkeyfile = os.path.join("/tmp", 'test%d.key.pub' % num) + + # force-remove key files if they exist to ssh-keygen doesn't + # start asking questions + try: + os.remove(pubkeyfile) + os.remove(privkeyfile) + except OSError: + pass + + cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % privkeyfile + import commands + print "Generating new keypair..." + (s, o) = commands.getstatusoutput(cmd) + print "Done." + pubkey = privkey = None + if s != 0: + logging.error("Could not generate key pair: %d (%s)" % (s, o)) + else: + pubkey = _extract_public_key(pubkeyfile) + privkey = _extract_private_key(privkeyfile) + + try: + os.remove(pubkeyfile) + os.remove(privkeyfile) + except OSError: + pass + return (pubkey, privkey) + +def _get_random_name(): + names = ["Liam", "Noel", "Guigsy", "Whitey", "Bonehead"] + return names[random.randint(0, len(names) - 1)] + +def _get_random_image(): import cairo, math, random, gtk def rand(): diff --git a/services/presence/presenceservice.py b/services/presence/presenceservice.py index ea50062f..45f51be1 100644 --- a/services/presence/presenceservice.py +++ b/services/presence/presenceservice.py @@ -25,7 +25,7 @@ from server_plugin import ServerPlugin from linklocal_plugin import LinkLocalPlugin from sugar import util -from buddy import Buddy, Owner +from buddy import Buddy, ShellOwner, TestOwner from activity import Activity _PRESENCE_SERVICE = "org.laptop.Sugar.Presence" @@ -40,7 +40,7 @@ class NotFoundError(dbus.DBusException): class PresenceService(dbus.service.Object): - def __init__(self): + def __init__(self, test=0): self._next_object_id = 0 self._buddies = {} # key -> Buddy @@ -52,7 +52,10 @@ class PresenceService(dbus.service.Object): # Create the Owner object objid = self._get_next_object_id() - self._owner = Owner(self._bus_name, objid) + if test > 0: + self._owner = TestOwner(self._bus_name, objid, test) + else: + self._owner = ShellOwner(self._bus_name, objid) self._buddies[self._owner.props.key] = self._owner self._registry = ManagerRegistry() @@ -326,9 +329,9 @@ class PresenceService(dbus.service.Object): activity.set_properties(props) -def main(): +def main(test=False): loop = gobject.MainLoop() - ps = PresenceService() + ps = PresenceService(test) try: loop.run() except KeyboardInterrupt: diff --git a/services/presence/server_plugin.py b/services/presence/server_plugin.py index c5f5f6d1..1d65a5e2 100644 --- a/services/presence/server_plugin.py +++ b/services/presence/server_plugin.py @@ -17,9 +17,7 @@ import gobject import dbus -from sugar import profile from sugar import util -from sugar import env import gtk from buddyiconcache import BuddyIconCache import logging @@ -131,21 +129,15 @@ class ServerPlugin(gobject.GObject): def _get_account_info(self): account_info = {} - pubkey = self._owner.props.key + account_info['server'] = self._owner.get_server() - 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)) + khash = util.printable_hash(util._sha_data(self._owner.props.key)) account_info['account'] = "%s@%s" % (khash, account_info['server']) - account_info['password'] = profile.get_private_key_hash() + account_info['password'] = self._owner.get_key_hash() + account_info['register'] = not self._owner.get_registered() + + print "ACCT: %s" % account_info return account_info def _find_existing_connection(self): @@ -208,7 +200,7 @@ class ServerPlugin(gobject.GObject): def _connected_cb(self): if self._account['register']: # we successfully register this account - profile.set_server_registered() + self._owner.props.registered = True # the group of contacts who may receive your presence publish = self._request_list_channel('publish') @@ -388,21 +380,27 @@ class ServerPlugin(gobject.GObject): self._conn[CONN_INTERFACE].Disconnect() def _contact_offline(self, handle): - self.emit("contact-offline", handle) + if not self._online_contacts.has_key(handle): + return + if self._online_contacts[handle]: + self.emit("contact-offline", handle) del self._online_contacts[handle] def _contact_online_activities_cb(self, handle, activities): if not activities or not len(activities): logging.debug("Handle %s - No activities" % handle) + self._contact_offline(handle) return self._buddy_activities_changed_cb(handle, activities) def _contact_online_activities_error_cb(self, handle, err): logging.debug("Handle %s - Error getting activities: %s" % (handle, err)) + self._contact_offline(handle) def _contact_online_aliases_cb(self, handle, props, aliases): if not aliases or not len(aliases): logging.debug("Handle %s - No aliases" % handle) + self._contact_offline(handle) return props['nick'] = aliases[0] @@ -416,12 +414,17 @@ class ServerPlugin(gobject.GObject): def _contact_online_aliases_error_cb(self, handle, err): logging.debug("Handle %s - Error getting nickname: %s" % (handle, err)) + self._contact_offline(handle) def _contact_online_properties_cb(self, handle, props): if not props.has_key('key'): logging.debug("Handle %s - invalid key." % handle) + self._contact_offline(handle) + return if not props.has_key('color'): logging.debug("Handle %s - invalid color." % handle) + self._contact_offline(handle) + return # Convert key from dbus byte array to python string props["key"] = psutils.bytes_to_string(props["key"]) @@ -432,8 +435,10 @@ class ServerPlugin(gobject.GObject): def _contact_online_properties_error_cb(self, handle, err): logging.debug("Handle %s - Error getting properties: %s" % (handle, err)) + self._contact_offline(handle) def _contact_online(self, handle): + self._online_contacts[handle] = None self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties(handle, reply_handler=lambda *args: self._contact_online_properties_cb(handle, *args), error_handler=lambda *args: self._contact_online_properties_error_cb(handle, *args)) @@ -449,7 +454,7 @@ class ServerPlugin(gobject.GObject): logging.debug("Handle %s (%s) was %s, status now '%s'." % (handle, jid, olstr, status)) if not online and status in ["available", "away", "brb", "busy", "dnd", "xa"]: self._contact_online(handle) - elif online and status in ["offline", "invisible"]: + elif status in ["offline", "invisible"]: self._contact_offline(handle) def _avatar_updated_cb(self, handle, new_avatar_token): @@ -459,6 +464,10 @@ class ServerPlugin(gobject.GObject): return jid = self._online_contacts[handle] + if not jid: + logging.debug("Handle %s not valid yet...") + return + icon = self._icon_cache.get_icon(jid, new_avatar_token) if not icon: # cache miss @@ -472,20 +481,24 @@ class ServerPlugin(gobject.GObject): for handle, alias in aliases: prop = {'nick': alias} #print "Buddy %s alias changed to %s" % (handle, alias) - self._buddy_properties_changed_cb(handle, prop) + if self._online_contacts.has_key(handle) and self._online_contacts[handle]: + self._buddy_properties_changed_cb(handle, prop) def _buddy_properties_changed_cb(self, handle, properties): 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) + if self._online_contacts.has_key(handle) and self._online_contacts[handle]: + self.emit("buddy-properties-changed", handle, properties) def _buddy_activities_changed_cb(self, handle, activities): if handle == self._conn[CONN_INTERFACE].GetSelfHandle(): # ignore network events for Owner activity changes since those # are handled locally return + if not self._online_contacts.has_key(handle) or not self._online_contacts[handle]: + return for act_id, act_handle in activities: self._activities[act_id] = act_handle @@ -497,6 +510,8 @@ class ServerPlugin(gobject.GObject): # ignore network events for Owner current activity changes since those # are handled locally return + if not self._online_contacts.has_key(handle) or not self._online_contacts[handle]: + return if not len(activity) or not util.validate_activity_id(activity): activity = None diff --git a/services/presence/sugar-presence-service b/services/presence/sugar-presence-service index 28591320..5a32a3e6 100755 --- a/services/presence/sugar-presence-service +++ b/services/presence/sugar-presence-service @@ -32,4 +32,13 @@ import presenceservice logging.info('Starting presence service') -presenceservice.main() +test=0 +if len(sys.argv) > 1: + try: + test = int(sys.argv[1]) + except ValueError: + logging.debug("Bad test user number.") + if test < 1 or test > 10: + logging.debug("Bad test user number.") + +presenceservice.main(test)