Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/collaboration
diff options
context:
space:
mode:
authorRaul Gutierrez Segales <raul.gutierrez.segales@collabora.co.uk>2011-01-20 15:58:18 (GMT)
committer Raul Gutierrez Segales <raul.gutierrez.segales@collabora.co.uk>2011-01-31 17:39:03 (GMT)
commit3bd9ef8bbf8e899e29cf5aee36242bb0b8405130 (patch)
tree68771fd34b46a5a2d9c06e5866b810dc886e38e0 /collaboration
parenta170f54f4cc1521d6554487c56db288a2d9c6041 (diff)
Collaboration support for non-Sugar apps
Diffstat (limited to 'collaboration')
-rw-r--r--collaboration/__init__.py0
-rw-r--r--collaboration/activity.py719
-rw-r--r--collaboration/buddy.py234
-rw-r--r--collaboration/connection_watcher.py122
-rw-r--r--collaboration/connectionmanager.py122
-rw-r--r--collaboration/dispatch/Makefile.am9
-rw-r--r--collaboration/dispatch/__init__.py10
-rw-r--r--collaboration/dispatch/dispatcher.py191
-rw-r--r--collaboration/dispatch/license.txt66
-rw-r--r--collaboration/dispatch/saferef.py254
-rwxr-xr-xcollaboration/neighborhood.py1038
-rw-r--r--collaboration/presenceservice.py266
-rw-r--r--collaboration/telepathyclient.py103
-rwxr-xr-xcollaboration/test.py14
-rw-r--r--collaboration/tubeconn.py114
-rw-r--r--collaboration/xocolor.py282
16 files changed, 3544 insertions, 0 deletions
diff --git a/collaboration/__init__.py b/collaboration/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/collaboration/__init__.py
diff --git a/collaboration/activity.py b/collaboration/activity.py
new file mode 100644
index 0000000..06e36df
--- /dev/null
+++ b/collaboration/activity.py
@@ -0,0 +1,719 @@
+# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""UI interface to an activity in the presence service
+
+STABLE.
+"""
+
+import logging
+from functools import partial
+
+import dbus
+from dbus import PROPERTIES_IFACE
+import gobject
+from telepathy.client import Channel
+from telepathy.interfaces import CHANNEL, \
+ CHANNEL_INTERFACE_GROUP, \
+ CHANNEL_TYPE_TUBES, \
+ CHANNEL_TYPE_TEXT, \
+ CONNECTION, \
+ PROPERTIES_INTERFACE
+from telepathy.constants import CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, \
+ HANDLE_TYPE_ROOM, \
+ HANDLE_TYPE_CONTACT, \
+ PROPERTY_FLAG_WRITE
+
+
+CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties'
+CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
+
+_logger = logging.getLogger('sugar.presence.activity')
+
+
+class Activity(gobject.GObject):
+ """UI interface for an Activity in the presence service
+
+ Activities in the presence service represent your and other user's
+ shared activities.
+
+ Properties:
+ id
+ color
+ name
+ type
+ joined
+ """
+ __gsignals__ = {
+ 'buddy-joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'buddy-left': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'new-channel': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])),
+ }
+
+ __gproperties__ = {
+ 'id': (str, None, None, None, gobject.PARAM_READABLE),
+ 'name': (str, None, None, None, gobject.PARAM_READWRITE),
+ 'tags': (str, None, None, None, gobject.PARAM_READWRITE),
+ 'color': (str, None, None, None, gobject.PARAM_READWRITE),
+ 'type': (str, None, None, None, gobject.PARAM_READABLE),
+ 'private': (bool, None, None, True, gobject.PARAM_READWRITE),
+ 'joined': (bool, None, None, False, gobject.PARAM_READABLE),
+ }
+
+ def __init__(self, account_path, connection, room_handle=None,
+ properties=None):
+ if room_handle is None and properties is None:
+ raise ValueError('Need to pass one of room_handle or properties')
+
+ if properties is None:
+ properties = {}
+
+ gobject.GObject.__init__(self)
+
+ self._account_path = account_path
+ self.telepathy_conn = connection
+ self.telepathy_text_chan = None
+ self.telepathy_tubes_chan = None
+
+ self.room_handle = room_handle
+ self._join_command = None
+ self._share_command = None
+ self._id = properties.get('id', None)
+ self._color = properties.get('color', None)
+ self._name = properties.get('name', None)
+ self._type = properties.get('type', None)
+ self._tags = properties.get('tags', None)
+ self._private = properties.get('private', True)
+ self._joined = properties.get('joined', False)
+ self._channel_self_handle = None
+ self._text_channel_group_flags = 0
+ self._buddies = {}
+
+ self._get_properties_call = None
+ if not self.room_handle is None:
+ self._start_tracking_properties()
+
+ def _start_tracking_properties(self):
+ bus = dbus.SessionBus()
+ self._get_properties_call = bus.call_async(
+ self.telepathy_conn.requested_bus_name,
+ self.telepathy_conn.object_path,
+ CONN_INTERFACE_ACTIVITY_PROPERTIES,
+ 'GetProperties',
+ 'u',
+ (self.room_handle,),
+ reply_handler=self.__got_properties_cb,
+ error_handler=self.__error_handler_cb,
+ utf8_strings=True)
+
+ # As only one Activity instance is needed per activity process,
+ # we can afford listening to ActivityPropertiesChanged like this.
+ self.telepathy_conn.connect_to_signal(
+ 'ActivityPropertiesChanged',
+ self.__activity_properties_changed_cb,
+ dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES)
+
+ def __activity_properties_changed_cb(self, room_handle, properties):
+ print('%r: Activity properties changed to %r', self,
+ properties)
+ self._update_properties(properties)
+
+ def __got_properties_cb(self, properties):
+ print('__got_properties_cb %r', properties)
+ self._get_properties_call = None
+ self._update_properties(properties)
+
+ def __error_handler_cb(self, error):
+ print('__error_handler_cb %r', error)
+
+ def _update_properties(self, new_props):
+ val = new_props.get('name', self._name)
+ if isinstance(val, str) and val != self._name:
+ self._name = val
+ self.notify('name')
+ val = new_props.get('tags', self._tags)
+ if isinstance(val, str) and val != self._tags:
+ self._tags = val
+ self.notify('tags')
+ val = new_props.get('color', self._color)
+ if isinstance(val, str) and val != self._color:
+ self._color = val
+ self.notify('color')
+ val = bool(new_props.get('private', self._private))
+ if val != self._private:
+ self._private = val
+ self.notify('private')
+ val = new_props.get('id', self._id)
+ if isinstance(val, str) and self._id is None:
+ self._id = val
+ self.notify('id')
+ val = new_props.get('type', self._type)
+ if isinstance(val, str) and self._type is None:
+ self._type = val
+ self.notify('type')
+
+ def object_path(self):
+ """Get our dbus object path"""
+ return self._object_path
+
+ def do_get_property(self, pspec):
+ """Retrieve a particular property from our property dictionary"""
+
+ if pspec.name == 'joined':
+ return self._joined
+
+ if self._get_properties_call is not None:
+ print('%r: Blocking on GetProperties() because someone '
+ 'wants property %s', self, pspec.name)
+ self._get_properties_call.block()
+
+ 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 == 'tags':
+ return self._tags
+ elif pspec.name == 'private':
+ return self._private
+
+ def do_set_property(self, pspec, val):
+ """Set a particular property in our property dictionary"""
+ # FIXME: need an asynchronous API to set these properties,
+ # particularly 'private'
+
+ if pspec.name == 'name':
+ self._name = val
+ elif pspec.name == 'color':
+ self._color = val
+ elif pspec.name == 'tags':
+ self._tags = val
+ elif pspec.name == 'private':
+ self._private = val
+ else:
+ raise ValueError('Unknown property %r', pspec.name)
+
+ self._publish_properties()
+
+ def set_private(self, val, reply_handler, error_handler):
+ print('set_private %r', val)
+ self._activity.SetProperties({'private': bool(val)},
+ reply_handler=reply_handler,
+ error_handler=error_handler)
+
+ def get_joined_buddies(self):
+ """Retrieve the set of Buddy objects attached to this activity
+
+ returns list of presence Buddy objects that we can successfully
+ create from the buddy object paths that PS has for this activity.
+ """
+ return self._buddies.values()
+
+ def get_buddy_by_handle(self, handle):
+ """Retrieve the Buddy object given a telepathy handle.
+
+ buddy object paths are cached in self._handle_to_buddy_path,
+ so we can get the buddy without calling PS.
+ """
+ object_path = self._handle_to_buddy_path.get(handle, None)
+ if object_path:
+ buddy = self._ps_new_object(object_path)
+ return buddy
+ return None
+
+ def invite(self, buddy, message, response_cb):
+ """Invite the given buddy to join this activity.
+
+ The callback will be called with one parameter: None on success,
+ or an exception on failure.
+ """
+ if not self._joined:
+ raise RuntimeError('Cannot invite a buddy to an activity that is'
+ 'not shared.')
+ self.telepathy_text_chan.AddMembers([buddy.contact_handle], message,
+ dbus_interface=CHANNEL_INTERFACE_GROUP,
+ reply_handler=partial(self.__invite_cb, response_cb),
+ error_handler=partial(self.__invite_cb, response_cb))
+
+ def __invite_cb(self, response_cb, error=None):
+ response_cb(error)
+
+ def set_up_tubes(self, reply_handler, error_handler):
+ raise NotImplementedError()
+
+ def __joined_cb(self, join_command, error):
+ print('%r: Join finished %r', self, error)
+ if error is not None:
+ self.emit('joined', error is None, str(error))
+ self.telepathy_text_chan = join_command.text_channel
+ self.telepathy_tubes_chan = join_command.tubes_channel
+ self._channel_self_handle = join_command.channel_self_handle
+ self._text_channel_group_flags = join_command.text_channel_group_flags
+ self._start_tracking_buddies()
+ self._start_tracking_channel()
+
+ def _start_tracking_buddies(self):
+ group = self.telepathy_text_chan[CHANNEL_INTERFACE_GROUP]
+
+ group.GetAllMembers(reply_handler=self.__get_all_members_cb,
+ error_handler=self.__error_handler_cb)
+
+ group.connect_to_signal('MembersChanged',
+ self.__text_channel_members_changed_cb)
+
+ def _start_tracking_channel(self):
+ channel = self.telepathy_text_chan[CHANNEL]
+ channel.connect_to_signal('Closed', self.__text_channel_closed_cb)
+
+ def __get_all_members_cb(self, members, local_pending, remote_pending):
+ print('__get_all_members_cb %r %r', members,
+ self._text_channel_group_flags)
+ if self._channel_self_handle in members:
+ members.remove(self._channel_self_handle)
+
+ if not members:
+ return
+
+ self._resolve_handles(members, reply_cb=self._add_initial_buddies)
+
+ def _resolve_handles(self, input_handles, reply_cb):
+ def get_handle_owners_cb(handles):
+ self.telepathy_conn.InspectHandles(HANDLE_TYPE_CONTACT, handles,
+ reply_handler=reply_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ if self._text_channel_group_flags & \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+
+ group = self.telepathy_text_chan[CHANNEL_INTERFACE_GROUP]
+ group.GetHandleOwners(input_handles,
+ reply_handler=get_handle_owners_cb,
+ error_handler=self.__error_handler_cb)
+ else:
+ get_handle_owners_cb(input_handles)
+
+ def _add_initial_buddies(self, contact_ids):
+ print('__add_initial_buddies %r', contact_ids)
+ #for contact_id in contact_ids:
+ # self._buddies[contact_id] = self._get_buddy(contact_id)
+ # Once we have the initial members, we can finish the join process
+ self._joined = True
+ self.emit('joined', True, None)
+
+ def __text_channel_members_changed_cb(self, message, added, removed,
+ local_pending, remote_pending,
+ actor, reason):
+ print('__text_channel_members_changed_cb %r',
+ [added, message, added, removed, local_pending,
+ remote_pending, actor, reason])
+ if self._channel_self_handle in added:
+ added.remove(self._channel_self_handle)
+ if added:
+ self._resolve_handles(added, reply_cb=self._add_buddies)
+
+ if self._channel_self_handle in removed:
+ removed.remove(self._channel_self_handle)
+ if removed:
+ self._resolve_handles(added, reply_cb=self._remove_buddies)
+
+ def _add_buddies(self, contact_ids):
+ for contact_id in contact_ids:
+ if contact_id not in self._buddies:
+ buddy = self._get_buddy(contact_id)
+ self.emit('buddy-joined', buddy)
+ self._buddies[contact_id] = buddy
+
+ def _remove_buddies(self, contact_ids):
+ for contact_id in contact_ids:
+ if contact_id in self._buddies:
+ buddy = self._get_buddy(contact_id)
+ self.emit('buddy-left', buddy)
+ del self._buddies[contact_id]
+
+ def _get_buddy(self, contact_id):
+ if contact_id in self._buddies:
+ return self._buddies[contact_id]
+ else:
+ return Buddy(self._account_path, contact_id)
+
+ def join(self):
+ """Join this activity.
+
+ Emits 'joined' and otherwise does nothing if we're already joined.
+ """
+ if self._join_command is not None:
+ return
+
+ if self._joined:
+ self.emit('joined', True, None)
+ return
+
+ print('%r: joining', self)
+
+ self._join_command = _JoinCommand(self.telepathy_conn,
+ self.room_handle)
+ self._join_command.connect('finished', self.__joined_cb)
+ self._join_command.run()
+
+ def share(self, share_activity_cb, share_activity_error_cb):
+ if not self.room_handle is None:
+ raise ValueError('Already have a room handle')
+
+ self._share_command = _ShareCommand(self.telepathy_conn, self._id)
+ self._share_command.connect('finished',
+ partial(self.__shared_cb,
+ share_activity_cb,
+ share_activity_error_cb))
+ self._share_command.run()
+
+ def __shared_cb(self, share_activity_cb, share_activity_error_cb,
+ share_command, error):
+ print('%r: Share finished %r', self, error)
+ if error is None:
+ print "There was no error!"
+ self._joined = True
+ self.room_handle = share_command.room_handle
+ self.telepathy_text_chan = share_command.text_channel
+ self.telepathy_tubes_chan = share_command.tubes_channel
+ self._channel_self_handle = share_command.channel_self_handle
+ self._text_channel_group_flags = \
+ share_command.text_channel_group_flags
+ self._publish_properties()
+ self._start_tracking_properties()
+ self._start_tracking_buddies()
+ self._start_tracking_channel()
+ share_activity_cb(self)
+ else:
+ print("error = %s" % error)
+ share_activity_error_cb(self, error)
+
+ def _publish_properties(self):
+ properties = {}
+
+ if self._color is not None:
+ properties['color'] = str(self._color)
+ if self._name is not None:
+ properties['name'] = str(self._name)
+ if self._type is not None:
+ properties['type'] = self._type
+ if self._tags is not None:
+ properties['tags'] = self._tags
+ properties['private'] = self._private
+
+ self.telepathy_conn.SetProperties(
+ self.room_handle,
+ properties,
+ dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES)
+
+ def __share_error_cb(self, share_activity_error_cb, error):
+ logging.debug('%r: Share failed because: %s', self, error)
+ share_activity_error_cb(self, error)
+
+ # GetChannels() wrapper
+
+ def get_channels(self):
+ """Retrieve communications channel descriptions for the activity
+
+ Returns a tuple containing:
+ - the D-Bus well-known service name of the connection
+ (FIXME: this is redundant; in Telepathy it can be derived
+ from that of the connection)
+ - the D-Bus object path of the connection
+ - a list of D-Bus object paths representing the channels
+ associated with this activity
+ """
+ bus_name = self.telepathy_conn.requested_bus_name
+ connection_path = self.telepathy_conn.object_path
+ channels = [self.telepathy_text_chan.object_path,
+ self.telepathy_tubes_chan.object_path]
+
+ print('%r: bus name is %s, connection is %s, channels are %r',
+ self, bus_name, connection_path, channels)
+ return bus_name, connection_path, channels
+
+ # Leaving
+ def __text_channel_closed_cb(self):
+ self._joined = False
+ self.emit('joined', False, 'left activity')
+
+ def leave(self):
+ """Leave this shared activity"""
+ print('%r: leaving', self)
+ self.telepathy_text_chan.Close()
+
+
+class _BaseCommand(gobject.GObject):
+ __gsignals__ = {
+ 'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self.text_channel = None
+ self.text_channel_group_flags = None
+ self.tubes_channel = None
+ self.room_handle = None
+ self.channel_self_handle = None
+
+ def run(self):
+ raise NotImplementedError()
+
+
+class _ShareCommand(_BaseCommand):
+ def __init__(self, connection, activity_id):
+ _BaseCommand.__init__(self)
+
+ self._connection = connection
+ self._activity_id = activity_id
+ self._finished = False
+ self._join_command = None
+
+ def run(self):
+ self._connection.RequestHandles(
+ HANDLE_TYPE_ROOM,
+ [self._activity_id],
+ reply_handler=self.__got_handles_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ def __got_handles_cb(self, handles):
+ logging.debug('__got_handles_cb %r', handles)
+ self.room_handle = handles[0]
+
+ self._join_command = _JoinCommand(self._connection, self.room_handle)
+ self._join_command.connect('finished', self.__joined_cb)
+ self._join_command.run()
+
+ def __joined_cb(self, join_command, error):
+ print('%r: Join finished %r', self, error)
+ if error is not None:
+ self._finished = True
+ self.emit('finished', error)
+ return
+
+ self.text_channel = join_command.text_channel
+ self.text_channel_group_flags = join_command.text_channel_group_flags
+ self.tubes_channel = join_command.tubes_channel
+
+ self._connection.AddActivity(
+ self._activity_id,
+ self.room_handle,
+ reply_handler=self.__added_activity_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONN_INTERFACE_BUDDY_INFO)
+
+ def __added_activity_cb(self):
+ self._finished = True
+ self.emit('finished', None)
+
+ def __error_handler_cb(self, error):
+ self._finished = True
+ self.emit('finished', error)
+
+
+class _JoinCommand(_BaseCommand):
+ def __init__(self, connection, room_handle):
+ _BaseCommand.__init__(self)
+
+ self._connection = connection
+ self._finished = False
+ self.room_handle = room_handle
+ self._global_self_handle = None
+
+ def run(self):
+ if self._finished:
+ raise RuntimeError('This command has already finished')
+ self._connection.Get(CONNECTION, 'SelfHandle',
+ reply_handler=self.__get_self_handle_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=PROPERTIES_IFACE)
+
+ def __get_self_handle_cb(self, handle):
+ self._global_self_handle = handle
+
+ self._connection.RequestChannel(CHANNEL_TYPE_TEXT,
+ HANDLE_TYPE_ROOM, self.room_handle, True,
+ reply_handler=self.__create_text_channel_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ self._connection.RequestChannel(CHANNEL_TYPE_TUBES,
+ HANDLE_TYPE_ROOM, self.room_handle, True,
+ reply_handler=self.__create_tubes_channel_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ def __create_text_channel_cb(self, channel_path):
+ Channel(self._connection.requested_bus_name, channel_path,
+ ready_handler=self.__text_channel_ready_cb)
+
+ def __create_tubes_channel_cb(self, channel_path):
+ print "Creating tubes channel with bus name %s" % self._connection.requested_bus_name
+ print "Creating tubes channel with channel path %s" % channel_path
+ Channel(self._connection.requested_bus_name, channel_path,
+ ready_handler=self.__tubes_channel_ready_cb)
+
+ def __error_handler_cb(self, error):
+ self._finished = True
+ self.emit('finished', error)
+
+ def __tubes_channel_ready_cb(self, channel):
+ print('%r: Tubes channel %r is ready', self, channel)
+ self.tubes_channel = channel
+ self._tubes_ready()
+
+ def __text_channel_ready_cb(self, channel):
+ print('%r: Text channel %r is ready', self, channel)
+ self.text_channel = channel
+ self._tubes_ready()
+
+ def _tubes_ready(self):
+ if self.text_channel is None or \
+ self.tubes_channel is None:
+ return
+
+ print('%r: finished setting up tubes', self)
+
+ self._add_self_to_channel()
+
+ def __text_channel_group_flags_changed_cb(self, added, removed):
+ print('__text_channel_group_flags_changed_cb %r %r', added,
+ removed)
+ self.text_channel_group_flags |= added
+ self.text_channel_group_flags &= ~removed
+
+ def _add_self_to_channel(self):
+ # FIXME: cope with non-Group channels here if we want to support
+ # non-OLPC-compatible IMs
+
+ group = self.text_channel[CHANNEL_INTERFACE_GROUP]
+
+ def got_all_members(members, local_pending, remote_pending):
+ print('got_all_members members %r local_pending %r '
+ 'remote_pending %r', members, local_pending,
+ remote_pending)
+
+ if self.text_channel_group_flags & \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ self_handle = self.channel_self_handle
+ else:
+ self_handle = self._global_self_handle
+
+ if self_handle in local_pending:
+ print('%r: We are in local pending - entering', self)
+ group.AddMembers([self_handle], '',
+ reply_handler=lambda: None,
+ error_handler=lambda e: self._join_failed_cb(e,
+ 'got_all_members AddMembers'))
+
+ if members:
+ self.__text_channel_members_changed_cb('', members, (),
+ (), (), 0, 0)
+
+ def got_group_flags(flags):
+ self.text_channel_group_flags = flags
+ # by the time we hook this, we need to know the group flags
+ group.connect_to_signal('MembersChanged',
+ self.__text_channel_members_changed_cb)
+
+ # bootstrap by getting the current state. This is where we find
+ # out whether anyone was lying to us in their PEP info
+ group.GetAllMembers(reply_handler=got_all_members,
+ error_handler=self.__error_handler_cb)
+
+ def got_self_handle(channel_self_handle):
+ self.channel_self_handle = channel_self_handle
+ group.connect_to_signal('GroupFlagsChanged',
+ self.__text_channel_group_flags_changed_cb)
+ group.GetGroupFlags(reply_handler=got_group_flags,
+ error_handler=self.__error_handler_cb)
+
+ group.GetSelfHandle(reply_handler=got_self_handle,
+ error_handler=self.__error_handler_cb)
+
+ def __text_channel_members_changed_cb(self, message, added, removed,
+ local_pending, remote_pending,
+ actor, reason):
+ print('__text_channel_members_changed_cb added %r removed %r '
+ 'local_pending %r remote_pending %r channel_self_handle '
+ '%r', added, removed, local_pending, remote_pending,
+ self.channel_self_handle)
+
+ if self.text_channel_group_flags & \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ self_handle = self.channel_self_handle
+ else:
+ self_handle = self._global_self_handle
+
+ if self_handle in added:
+ if PROPERTIES_INTERFACE not in self.text_channel:
+ self._finished = True
+ self.emit('finished', None)
+ else:
+ self.text_channel[PROPERTIES_INTERFACE].ListProperties(
+ reply_handler=self.__list_properties_cb,
+ error_handler=self.__error_handler_cb)
+
+ def __list_properties_cb(self, prop_specs):
+ # FIXME: invite-only ought to be set on private activities; but
+ # since only the owner can change invite-only, that would break
+ # activity scope changes.
+ props = {
+ # otherwise buddy resolution breaks
+ 'anonymous': False,
+ # anyone who knows about the channel can join
+ 'invite-only': False,
+ # so non-owners can invite others
+ 'invite-restricted': False,
+ # vanish when there are no members
+ 'persistent': False,
+ # don't appear in server room lists
+ 'private': True,
+ }
+ props_to_set = []
+ for ident, name, sig_, flags in prop_specs:
+ value = props.pop(name, None)
+ if value is not None:
+ if flags & PROPERTY_FLAG_WRITE:
+ props_to_set.append((ident, value))
+ # FIXME: else error, but only if we're creating the room?
+ # FIXME: if props is nonempty, then we want to set props that aren't
+ # supported here - raise an error?
+
+ if props_to_set:
+ self.text_channel[PROPERTIES_INTERFACE].SetProperties(
+ props_to_set, reply_handler=self.__set_properties_cb,
+ error_handler=self.__error_handler_cb)
+ else:
+ self._finished = True
+ self.emit('finished', None)
+
+ def __set_properties_cb(self):
+ self._finished = True
+ self.emit('finished', None)
diff --git a/collaboration/buddy.py b/collaboration/buddy.py
new file mode 100644
index 0000000..b117940
--- /dev/null
+++ b/collaboration/buddy.py
@@ -0,0 +1,234 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import gobject
+import gconf
+import dbus
+from telepathy.client import Connection
+from telepathy.interfaces import CONNECTION
+
+from xocolor import XoColor
+import connection_watcher
+
+
+CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
+
+_owner_instance = None
+
+
+class BaseBuddyModel(gobject.GObject):
+ __gtype_name__ = 'SugarBaseBuddyModel'
+
+ def __init__(self, **kwargs):
+ self._key = None
+ self._nick = None
+ self._color = None
+ self._tags = None
+ self._current_activity = None
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ def get_nick(self):
+ return self._nick
+
+ def set_nick(self, nick):
+ self._nick = nick
+
+ nick = gobject.property(type=object, getter=get_nick, setter=set_nick)
+
+ def get_key(self):
+ return self._key
+
+ def set_key(self, key):
+ self._key = key
+
+ key = gobject.property(type=object, getter=get_key, setter=set_key)
+
+ def get_color(self):
+ return self._color
+
+ def set_color(self, color):
+ self._color = color
+
+ color = gobject.property(type=object, getter=get_color, setter=set_color)
+
+ def get_tags(self):
+ return self._tags
+
+ tags = gobject.property(type=object, getter=get_tags)
+
+ def get_current_activity(self):
+ return self._current_activity
+
+ def set_current_activity(self, current_activity):
+ if self._current_activity != current_activity:
+ self._current_activity = current_activity
+ self.notify('current-activity')
+
+ current_activity = gobject.property(type=object,
+ getter=get_current_activity,
+ setter=set_current_activity)
+
+ def is_owner(self):
+ raise NotImplementedError
+
+
+class OwnerBuddyModel(BaseBuddyModel):
+ __gtype_name__ = 'SugarOwnerBuddyModel'
+
+ def __init__(self):
+ BaseBuddyModel.__init__(self)
+
+ #client = gconf.client_get_default()
+ #self.props.nick = client.get_string('/desktop/sugar/user/nick')
+ self.props.nick = "rgs"
+ #color = client.get_string('/desktop/sugar/user/color')
+ self.props.color = XoColor(None)
+
+ #self.props.key = get_profile().pubkey
+ self.props.key = "foobar"
+
+ self.connect('notify::nick', self.__property_changed_cb)
+ self.connect('notify::color', self.__property_changed_cb)
+ self.connect('notify::current-activity',
+ self.__current_activity_changed_cb)
+
+ bus = dbus.SessionBus()
+ bus.add_signal_receiver(
+ self.__name_owner_changed_cb,
+ signal_name='NameOwnerChanged',
+ dbus_interface='org.freedesktop.DBus')
+
+ bus_object = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH)
+ for service in bus_object.ListNames(
+ dbus_interface=dbus.BUS_DAEMON_IFACE):
+ if service.startswith(CONNECTION + '.'):
+ path = '/%s' % service.replace('.', '/')
+ Connection(service, path, bus,
+ ready_handler=self.__connection_ready_cb)
+
+ def __connection_ready_cb(self, connection):
+ self._sync_properties_on_connection(connection)
+
+ def __name_owner_changed_cb(self, name, old, new):
+ if name.startswith(CONNECTION + '.') and not old and new:
+ path = '/' + name.replace('.', '/')
+ Connection(name, path, ready_handler=self.__connection_ready_cb)
+
+ def __property_changed_cb(self, buddy, pspec):
+ self._sync_properties()
+
+ def __current_activity_changed_cb(self, buddy, pspec):
+ conn_watcher = connection_watcher.get_instance()
+ for connection in conn_watcher.get_connections():
+ if self.props.current_activity is not None:
+ activity_id = self.props.current_activity.activity_id
+ room_handle = self.props.current_activity.room_handle
+ else:
+ activity_id = ''
+ room_handle = 0
+
+ connection[CONNECTION_INTERFACE_BUDDY_INFO].SetCurrentActivity(
+ activity_id,
+ room_handle,
+ reply_handler=self.__set_current_activity_cb,
+ error_handler=self.__error_handler_cb)
+
+ def __set_current_activity_cb(self):
+ logging.debug('__set_current_activity_cb')
+
+ def _sync_properties(self):
+ conn_watcher = connection_watcher.get_instance()
+ for connection in conn_watcher.get_connections():
+ self._sync_properties_on_connection(connection)
+
+ def _sync_properties_on_connection(self, connection):
+ if CONNECTION_INTERFACE_BUDDY_INFO in connection:
+ properties = {}
+ if self.props.key is not None:
+ properties['key'] = dbus.ByteArray(self.props.key)
+ if self.props.color is not None:
+ properties['color'] = self.props.color.to_string()
+
+ logging.debug('calling SetProperties with %r', properties)
+ connection[CONNECTION_INTERFACE_BUDDY_INFO].SetProperties(
+ properties,
+ reply_handler=self.__set_properties_cb,
+ error_handler=self.__error_handler_cb)
+
+ def __set_properties_cb(self):
+ logging.debug('__set_properties_cb')
+
+ def __error_handler_cb(self, error):
+ raise RuntimeError(error)
+
+ def __connection_added_cb(self, conn_watcher, connection):
+ self._sync_properties_on_connection(connection)
+
+ def is_owner(self):
+ return True
+
+
+def get_owner_instance():
+ global _owner_instance
+ if _owner_instance is None:
+ _owner_instance = OwnerBuddyModel()
+ return _owner_instance
+
+
+class BuddyModel(BaseBuddyModel):
+ __gtype_name__ = 'SugarBuddyModel'
+
+ def __init__(self, **kwargs):
+
+ self._account = None
+ self._contact_id = None
+ self._handle = None
+
+ BaseBuddyModel.__init__(self, **kwargs)
+
+ def is_owner(self):
+ return False
+
+ def get_account(self):
+ return self._account
+
+ def set_account(self, account):
+ self._account = account
+
+ account = gobject.property(type=object, getter=get_account,
+ setter=set_account)
+
+ def get_contact_id(self):
+ return self._contact_id
+
+ def set_contact_id(self, contact_id):
+ self._contact_id = contact_id
+
+ contact_id = gobject.property(type=object, getter=get_contact_id,
+ setter=set_contact_id)
+
+ def get_handle(self):
+ return self._handle
+
+ def set_handle(self, handle):
+ self._handle = handle
+
+ handle = gobject.property(type=object, getter=get_handle,
+ setter=set_handle)
diff --git a/collaboration/connection_watcher.py b/collaboration/connection_watcher.py
new file mode 100644
index 0000000..96af1cf
--- /dev/null
+++ b/collaboration/connection_watcher.py
@@ -0,0 +1,122 @@
+# This should eventually land in telepathy-python, so has the same license:
+# Copyright (C) 2008 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# FIXME: this sould go upstream, in telepathy-python
+
+import logging
+
+import dbus
+import dbus.mainloop.glib
+import gobject
+
+from telepathy.client import Connection
+from telepathy.interfaces import CONN_INTERFACE
+from telepathy.constants import CONNECTION_STATUS_CONNECTED, \
+ CONNECTION_STATUS_DISCONNECTED
+
+
+_instance = None
+
+
+class ConnectionWatcher(gobject.GObject):
+ __gsignals__ = {
+ 'connection-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'connection-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ }
+
+ def __init__(self, bus=None):
+ gobject.GObject.__init__(self)
+
+ if bus is None:
+ self.bus = dbus.Bus()
+ else:
+ self.bus = bus
+
+ # D-Bus path -> Connection
+ self._connections = {}
+
+ self.bus.add_signal_receiver(self._status_changed_cb,
+ dbus_interface=CONN_INTERFACE, signal_name='StatusChanged',
+ path_keyword='path')
+
+ for conn in Connection.get_connections(bus):
+ conn.call_when_ready(self._conn_ready_cb)
+
+ def _status_changed_cb(self, *args, **kwargs):
+ path = kwargs['path']
+ if not path.startswith('/org/freedesktop/Telepathy/Connection/'):
+ return
+
+ status, reason_ = args
+ service_name = path.replace('/', '.')[1:]
+
+ if status == CONNECTION_STATUS_CONNECTED:
+ self._add_connection(service_name, path)
+ elif status == CONNECTION_STATUS_DISCONNECTED:
+ self._remove_connection(service_name, path)
+
+ def _conn_ready_cb(self, conn):
+ if conn.object_path in self._connections:
+ return
+
+ self._connections[conn.object_path] = conn
+ self.emit('connection-added', conn)
+
+ def _add_connection(self, service_name, path):
+ if path in self._connections:
+ return
+
+ try:
+ Connection(service_name, path, ready_handler=self._conn_ready_cb)
+ except dbus.exceptions.DBusException:
+ logging.debug('%s is propably already gone.', service_name)
+
+ def _remove_connection(self, service_name, path):
+ conn = self._connections.pop(path, None)
+ if conn is None:
+ return
+
+ self.emit('connection-removed', conn)
+
+ def get_connections(self):
+ return self._connections.values()
+
+
+def get_instance():
+ global _instance
+ if _instance is None:
+ _instance = ConnectionWatcher()
+ return _instance
+
+
+if __name__ == '__main__':
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+ def connection_added_cb(conn_watcher, conn):
+ print 'new connection', conn.service_name
+
+ def connection_removed_cb(conn_watcher, conn):
+ print 'removed connection', conn.service_name
+
+ watcher = ConnectionWatcher()
+ watcher.connect('connection-added', connection_added_cb)
+ watcher.connect('connection-removed', connection_removed_cb)
+
+ loop = gobject.MainLoop()
+ loop.run()
diff --git a/collaboration/connectionmanager.py b/collaboration/connectionmanager.py
new file mode 100644
index 0000000..5ae59dd
--- /dev/null
+++ b/collaboration/connectionmanager.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+UNSTABLE. It should really be internal to the sugar.presence package.
+"""
+
+from functools import partial
+
+import dbus
+from dbus import PROPERTIES_IFACE
+from telepathy.interfaces import ACCOUNT, \
+ ACCOUNT_MANAGER
+from telepathy.constants import CONNECTION_STATUS_CONNECTED
+
+ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
+ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager'
+
+
+class Connection(object):
+ def __init__(self, account_path, connection):
+ self.account_path = account_path
+ self.connection = connection
+ self.connected = False
+
+
+class ConnectionManager(object):
+ """Track available telepathy connections"""
+
+ def __init__(self):
+ self._connections_per_account = {}
+
+ bus = dbus.SessionBus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_paths = account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts',
+ dbus_interface=PROPERTIES_IFACE)
+ for account_path in account_paths:
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ obj.connect_to_signal('AccountPropertyChanged',
+ partial(self.__account_property_changed_cb, account_path))
+ connection_path = obj.Get(ACCOUNT, 'Connection')
+ if connection_path != '/':
+ try:
+ self._track_connection(account_path, connection_path)
+ except:
+ pass
+
+ def __account_property_changed_cb(self, account_path, properties):
+ if 'Connection' not in properties:
+ return
+ if properties['Connection'] == '/':
+ if account_path in self._connections_per_account:
+ del self._connections_per_account[account_path]
+ else:
+ self._track_connection(account_path, properties['Connection'])
+
+ def _track_connection(self, account_path, connection_path):
+ connection_name = connection_path.replace('/', '.')[1:]
+ bus = dbus.SessionBus()
+ connection = bus.get_object(connection_name, connection_path)
+ connection.connect_to_signal('StatusChanged',
+ partial(self.__status_changed_cb, account_path))
+ self._connections_per_account[account_path] = \
+ Connection(account_path, connection)
+
+ account = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ status = account.Get(ACCOUNT, 'ConnectionStatus')
+ if status == CONNECTION_STATUS_CONNECTED:
+ self._connections_per_account[account_path].connected = True
+ else:
+ self._connections_per_account[account_path].connected = False
+
+ def __status_changed_cb(self, account_path, status, reason):
+ if status == CONNECTION_STATUS_CONNECTED:
+ self._connections_per_account[account_path].connected = True
+ else:
+ self._connections_per_account[account_path].connected = False
+
+ def get_preferred_connection(self):
+ best_connection = None, None
+ for account_path, connection in self._connections_per_account.items():
+ if 'salut' in account_path and connection.connected:
+ best_connection = account_path, connection.connection
+ elif 'gabble' in account_path and connection.connected:
+ best_connection = account_path, connection.connection
+ break
+ return best_connection
+
+ def get_connection(self, account_path):
+ return self._connections_per_account[account_path].connection
+
+ def get_connections_per_account(self):
+ return self._connections_per_account
+
+ def get_account_for_connection(self, connection_path):
+ for account_path, connection in self._connections_per_account.items():
+ if connection.connection.object_path == connection_path:
+ return account_path
+ return None
+
+
+_connection_manager = None
+def get_connection_manager():
+ global _connection_manager
+ if not _connection_manager:
+ _connection_manager = ConnectionManager()
+ return _connection_manager
diff --git a/collaboration/dispatch/Makefile.am b/collaboration/dispatch/Makefile.am
new file mode 100644
index 0000000..eb44a32
--- /dev/null
+++ b/collaboration/dispatch/Makefile.am
@@ -0,0 +1,9 @@
+sugardir = $(pythondir)/sugar/dispatch
+sugar_PYTHON = \
+ __init__.py \
+ dispatcher.py \
+ saferef.py
+
+EXTRA_DIST = \
+ license.txt
+
diff --git a/collaboration/dispatch/__init__.py b/collaboration/dispatch/__init__.py
new file mode 100644
index 0000000..9f0a092
--- /dev/null
+++ b/collaboration/dispatch/__init__.py
@@ -0,0 +1,10 @@
+"""Multi-consumer multi-producer dispatching mechanism
+
+Originally based on pydispatch (BSD)
+http://pypi.python.org/pypi/PyDispatcher/2.0.1
+See license.txt for original license.
+
+Heavily modified for Django's purposes.
+"""
+
+from dispatcher import Signal
diff --git a/collaboration/dispatch/dispatcher.py b/collaboration/dispatch/dispatcher.py
new file mode 100644
index 0000000..45c32fe
--- /dev/null
+++ b/collaboration/dispatch/dispatcher.py
@@ -0,0 +1,191 @@
+import weakref
+import saferef
+
+WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref)
+
+
+def _make_id(target):
+ if hasattr(target, 'im_func'):
+ return (id(target.im_self), id(target.im_func))
+ return id(target)
+
+
+class Signal(object):
+ """Base class for all signals
+
+ Internal attributes:
+ receivers -- { receriverkey (id) : weakref(receiver) }
+ """
+
+ def __init__(self, providing_args=None):
+ """providing_args -- A list of the arguments this signal can pass along
+ in a send() call.
+ """
+ self.receivers = []
+ if providing_args is None:
+ providing_args = []
+ self.providing_args = set(providing_args)
+
+ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
+ """Connect receiver to sender for signal
+
+ receiver -- a function or an instance method which is to
+ receive signals. Receivers must be
+ hashable objects.
+
+ if weak is True, then receiver must be weak-referencable
+ (more precisely saferef.safeRef() must be able to create
+ a reference to the receiver).
+
+ Receivers must be able to accept keyword arguments.
+
+ If receivers have a dispatch_uid attribute, the receiver will
+ not be added if another receiver already exists with that
+ dispatch_uid.
+
+ sender -- the sender to which the receiver should respond
+ Must either be of type Signal, or None to receive events
+ from any sender.
+
+ weak -- whether to use weak references to the receiver
+ By default, the module will attempt to use weak
+ references to the receiver objects. If this parameter
+ is false, then strong references will be used.
+
+ dispatch_uid -- an identifier used to uniquely identify a particular
+ instance of a receiver. This will usually be a string, though it
+ may be anything hashable.
+
+ returns None
+ """
+ if dispatch_uid:
+ lookup_key = (dispatch_uid, _make_id(sender))
+ else:
+ lookup_key = (_make_id(receiver), _make_id(sender))
+
+ if weak:
+ receiver = saferef.safeRef(receiver,
+ onDelete=self._remove_receiver)
+
+ for r_key, _ in self.receivers:
+ if r_key == lookup_key:
+ break
+ else:
+ self.receivers.append((lookup_key, receiver))
+
+ def disconnect(self, receiver=None, sender=None, weak=True,
+ dispatch_uid=None):
+ """Disconnect receiver from sender for signal
+
+ receiver -- the registered receiver to disconnect. May be none if
+ dispatch_uid is specified.
+ sender -- the registered sender to disconnect
+ weak -- the weakref state to disconnect
+ dispatch_uid -- the unique identifier of the receiver to disconnect
+
+ disconnect reverses the process of connect.
+
+ If weak references are used, disconnect need not be called.
+ The receiver will be remove from dispatch automatically.
+
+ returns None
+ """
+
+ if dispatch_uid:
+ lookup_key = (dispatch_uid, _make_id(sender))
+ else:
+ lookup_key = (_make_id(receiver), _make_id(sender))
+
+ for idx, (r_key, _) in enumerate(self.receivers):
+ if r_key == lookup_key:
+ del self.receivers[idx]
+
+ def send(self, sender, **named):
+ """Send signal from sender to all connected receivers.
+
+ sender -- the sender of the signal
+ Either a specific object or None.
+
+ named -- named arguments which will be passed to receivers.
+
+ Returns a list of tuple pairs [(receiver, response), ... ].
+
+ If any receiver raises an error, the error propagates back
+ through send, terminating the dispatch loop, so it is quite
+ possible to not have all receivers called if a raises an
+ error.
+ """
+
+ responses = []
+ if not self.receivers:
+ return responses
+
+ for receiver in self._live_receivers(_make_id(sender)):
+ response = receiver(signal=self, sender=sender, **named)
+ responses.append((receiver, response))
+ return responses
+
+ def send_robust(self, sender, **named):
+ """Send signal from sender to all connected receivers catching errors
+
+ sender -- the sender of the signal
+ Can be any python object (normally one registered with
+ a connect if you actually want something to occur).
+
+ named -- named arguments which will be passed to receivers.
+ These arguments must be a subset of the argument names
+ defined in providing_args.
+
+ Return a list of tuple pairs [(receiver, response), ... ],
+ may raise DispatcherKeyError
+
+ if any receiver raises an error (specifically any subclass of
+ Exception), the error instance is returned as the result for that
+ receiver.
+ """
+
+ responses = []
+ if not self.receivers:
+ return responses
+
+ # Call each receiver with whatever arguments it can accept.
+ # Return a list of tuple pairs [(receiver, response), ... ].
+ for receiver in self._live_receivers(_make_id(sender)):
+ try:
+ response = receiver(signal=self, sender=sender, **named)
+ except Exception, err:
+ responses.append((receiver, err))
+ else:
+ responses.append((receiver, response))
+ return responses
+
+ def _live_receivers(self, senderkey):
+ """Filter sequence of receivers to get resolved, live receivers
+
+ This checks for weak references
+ and resolves them, then returning only live
+ receivers.
+ """
+ none_senderkey = _make_id(None)
+
+ for (receiverkey_, r_senderkey), receiver in self.receivers:
+ if r_senderkey == none_senderkey or r_senderkey == senderkey:
+ if isinstance(receiver, WEAKREF_TYPES):
+ # Dereference the weak reference.
+ receiver = receiver()
+ if receiver is not None:
+ yield receiver
+ else:
+ yield receiver
+
+ def _remove_receiver(self, receiver):
+ """Remove dead receivers from connections."""
+
+ to_remove = []
+ for key, connected_receiver in self.receivers:
+ if connected_receiver == receiver:
+ to_remove.append(key)
+ for key in to_remove:
+ for idx, (r_key, _) in enumerate(self.receivers):
+ if r_key == key:
+ del self.receivers[idx]
diff --git a/collaboration/dispatch/license.txt b/collaboration/dispatch/license.txt
new file mode 100644
index 0000000..0272c28
--- /dev/null
+++ b/collaboration/dispatch/license.txt
@@ -0,0 +1,66 @@
+sugar.dispatch was originally forked from django.dispatch
+
+Copyright (c) Django Software Foundation and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Django nor the names of its contributors may be used
+ to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+django.dispatch was originally forked from PyDispatcher.
+
+PyDispatcher License:
+
+ Copyright (c) 2001-2003, Patrick K. O'Brien and Contributors
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+ The name of Patrick K. O'Brien, or the name of any Contributor,
+ may not be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/collaboration/dispatch/saferef.py b/collaboration/dispatch/saferef.py
new file mode 100644
index 0000000..bb73b5d
--- /dev/null
+++ b/collaboration/dispatch/saferef.py
@@ -0,0 +1,254 @@
+"""
+"Safe weakrefs", originally from pyDispatcher.
+
+Provides a way to safely weakref any function, including bound methods (which
+aren't handled by the core weakref module).
+"""
+
+import weakref
+import traceback
+
+
+def safeRef(target, onDelete=None):
+ """Return a *safe* weak reference to a callable target
+
+ target -- the object to be weakly referenced, if it's a
+ bound method reference, will create a BoundMethodWeakref,
+ otherwise creates a simple weakref.
+ onDelete -- if provided, will have a hard reference stored
+ to the callable to be called after the safe reference
+ goes out of scope with the reference object, (either a
+ weakref or a BoundMethodWeakref) as argument.
+ """
+ if hasattr(target, 'im_self'):
+ if target.im_self is not None:
+ # Turn a bound method into a BoundMethodWeakref instance.
+ # Keep track of these instances for lookup by disconnect().
+ if not hasattr(target, 'im_func'):
+ raise TypeError("safeRef target %r has im_self, but no"
+ " im_func, don't know how to create reference" %
+ (target, ))
+ reference = get_bound_method_weakref(target=target,
+ onDelete=onDelete)
+ return reference
+ if callable(onDelete):
+ return weakref.ref(target, onDelete)
+ else:
+ return weakref.ref(target)
+
+
+class BoundMethodWeakref(object):
+ """'Safe' and reusable weak references to instance methods
+
+ BoundMethodWeakref objects provide a mechanism for
+ referencing a bound method without requiring that the
+ method object itself (which is normally a transient
+ object) is kept alive. Instead, the BoundMethodWeakref
+ object keeps weak references to both the object and the
+ function which together define the instance method.
+
+ Attributes:
+ key -- the identity key for the reference, calculated
+ by the class's calculateKey method applied to the
+ target instance method
+ deletionMethods -- sequence of callable objects taking
+ single argument, a reference to this object which
+ will be called when *either* the target object or
+ target function is garbage collected (i.e. when
+ this object becomes invalid). These are specified
+ as the onDelete parameters of safeRef calls.
+ weakSelf -- weak reference to the target object
+ weakFunc -- weak reference to the target function
+
+ Class Attributes:
+ _allInstances -- class attribute pointing to all live
+ BoundMethodWeakref objects indexed by the class's
+ calculateKey(target) method applied to the target
+ objects. This weak value dictionary is used to
+ short-circuit creation so that multiple references
+ to the same (object, function) pair produce the
+ same BoundMethodWeakref instance.
+
+ """
+
+ _allInstances = weakref.WeakValueDictionary()
+
+ def __new__(cls, target, onDelete=None, *arguments, **named):
+ """Create new instance or return current instance
+
+ Basically this method of construction allows us to
+ short-circuit creation of references to already-
+ referenced instance methods. The key corresponding
+ to the target is calculated, and if there is already
+ an existing reference, that is returned, with its
+ deletionMethods attribute updated. Otherwise the
+ new instance is created and registered in the table
+ of already-referenced methods.
+ """
+ key = cls.calculateKey(target)
+ current = cls._allInstances.get(key)
+ if current is not None:
+ current.deletionMethods.append(onDelete)
+ return current
+ else:
+ base = super(BoundMethodWeakref, cls).__new__(cls)
+ cls._allInstances[key] = base
+ base.__init__(target, onDelete, *arguments, **named)
+ return base
+
+ def __init__(self, target, onDelete=None):
+ """Return a weak-reference-like instance for a bound method
+
+ target -- the instance-method target for the weak
+ reference, must have im_self and im_func attributes
+ and be reconstructable via:
+ target.im_func.__get__( target.im_self )
+ which is true of built-in instance methods.
+ onDelete -- optional callback which will be called
+ when this weak reference ceases to be valid
+ (i.e. either the object or the function is garbage
+ collected). Should take a single argument,
+ which will be passed a pointer to this object.
+ """
+ def remove(weak, self=self):
+ """Set self.isDead to true when method or instance is destroyed"""
+ methods = self.deletionMethods[:]
+ del self.deletionMethods[:]
+ try:
+ # pylint: disable=W0212
+ del self.__class__._allInstances[self.key]
+ except KeyError:
+ pass
+ for function in methods:
+ try:
+ if callable(function):
+ function(self)
+ except Exception, e:
+ try:
+ traceback.print_exc()
+ except AttributeError:
+ print ('Exception during saferef %s cleanup function'
+ ' %s: %s' % (self, function, e))
+ self.deletionMethods = [onDelete]
+ self.key = self.calculateKey(target)
+ self.weakSelf = weakref.ref(target.im_self, remove)
+ self.weakFunc = weakref.ref(target.im_func, remove)
+ self.selfName = str(target.im_self)
+ self.funcName = str(target.im_func.__name__)
+
+ def calculateKey(cls, target):
+ """Calculate the reference key for this reference
+
+ Currently this is a two-tuple of the id()'s of the
+ target object and the target function respectively.
+ """
+ return (id(target.im_self), id(target.im_func))
+ calculateKey = classmethod(calculateKey)
+
+ def __str__(self):
+ """Give a friendly representation of the object"""
+ return '%s( %s.%s )' % (self.__class__.__name__, self.selfName,
+ self.funcName)
+
+ __repr__ = __str__
+
+ def __nonzero__(self):
+ """Whether we are still a valid reference"""
+ return self() is not None
+
+ def __cmp__(self, other):
+ """Compare with another reference"""
+ if not isinstance(other, self.__class__):
+ return cmp(self.__class__, type(other))
+ return cmp(self.key, other.key)
+
+ def __call__(self):
+ """Return a strong reference to the bound method
+
+ If the target cannot be retrieved, then will
+ return None, otherwise returns a bound instance
+ method for our object and function.
+
+ Note:
+ You may call this method any number of times,
+ as it does not invalidate the reference.
+ """
+ target = self.weakSelf()
+ if target is not None:
+ function = self.weakFunc()
+ if function is not None:
+ return function.__get__(target)
+ return None
+
+
+class BoundNonDescriptorMethodWeakref(BoundMethodWeakref):
+ """A specialized BoundMethodWeakref, for platforms where instance methods
+ are not descriptors.
+
+ It assumes that the function name and the target attribute name are the
+ same, instead of assuming that the function is a descriptor. This approach
+ is equally fast, but not 100% reliable because functions can be stored on
+ an attribute named differenty than the function's name such as in:
+
+ class A: pass
+ def foo(self): return 'foo'
+ A.bar = foo
+
+ But this shouldn't be a common use case. So, on platforms where methods
+ aren't descriptors (such as Jython) this implementation has the advantage
+ of working in the most cases.
+ """
+ def __init__(self, target, onDelete=None):
+ """Return a weak-reference-like instance for a bound method
+
+ target -- the instance-method target for the weak
+ reference, must have im_self and im_func attributes
+ and be reconstructable via:
+ target.im_func.__get__( target.im_self )
+ which is true of built-in instance methods.
+ onDelete -- optional callback which will be called
+ when this weak reference ceases to be valid
+ (i.e. either the object or the function is garbage
+ collected). Should take a single argument,
+ which will be passed a pointer to this object.
+ """
+ assert getattr(target.im_self, target.__name__) == target, \
+ ("method %s isn't available as the attribute %s of %s" %
+ (target, target.__name__, target.im_self))
+ super(BoundNonDescriptorMethodWeakref, self).__init__(target, onDelete)
+
+ def __call__(self):
+ """Return a strong reference to the bound method
+
+ If the target cannot be retrieved, then will
+ return None, otherwise returns a bound instance
+ method for our object and function.
+
+ Note:
+ You may call this method any number of times,
+ as it does not invalidate the reference.
+ """
+ target = self.weakSelf()
+ if target is not None:
+ function = self.weakFunc()
+ if function is not None:
+ # Using curry() would be another option, but it erases the
+ # "signature" of the function. That is, after a function is
+ # curried, the inspect module can't be used to determine how
+ # many arguments the function expects, nor what keyword
+ # arguments it supports, and pydispatcher needs this
+ # information.
+ return getattr(target, function.__name__)
+ return None
+
+
+def get_bound_method_weakref(target, onDelete):
+ """Instantiates the appropiate BoundMethodWeakRef, depending on the details
+ of the underlying class method implementation"""
+ if hasattr(target, '__get__'):
+ # target method is a descriptor, so the default implementation works:
+ return BoundMethodWeakref(target=target, onDelete=onDelete)
+ else:
+ # no luck, use the alternative implementation:
+ return BoundNonDescriptorMethodWeakref(target=target,
+ onDelete=onDelete)
diff --git a/collaboration/neighborhood.py b/collaboration/neighborhood.py
new file mode 100755
index 0000000..ad7ce21
--- /dev/null
+++ b/collaboration/neighborhood.py
@@ -0,0 +1,1038 @@
+#!/usr/bin/python
+#
+# neighborhood.py
+#
+#
+
+from functools import partial
+from hashlib import sha1
+
+import traceback
+import gobject
+import gconf
+import dbus
+from dbus import PROPERTIES_IFACE
+from telepathy.interfaces import ACCOUNT, \
+ ACCOUNT_MANAGER, \
+ CHANNEL, \
+ CHANNEL_INTERFACE_GROUP, \
+ CHANNEL_TYPE_CONTACT_LIST, \
+ CHANNEL_TYPE_FILE_TRANSFER, \
+ CLIENT, \
+ CONNECTION, \
+ CONNECTION_INTERFACE_ALIASING, \
+ CONNECTION_INTERFACE_CONTACTS, \
+ CONNECTION_INTERFACE_CONTACT_CAPABILITIES, \
+ CONNECTION_INTERFACE_REQUESTS, \
+ CONNECTION_INTERFACE_SIMPLE_PRESENCE
+from telepathy.constants import HANDLE_TYPE_CONTACT, \
+ HANDLE_TYPE_LIST, \
+ CONNECTION_PRESENCE_TYPE_OFFLINE, \
+ CONNECTION_STATUS_CONNECTED, \
+ CONNECTION_STATUS_DISCONNECTED
+from telepathy.client import Connection, Channel
+
+from buddy import get_owner_instance
+from buddy import BuddyModel
+
+from xocolor import XoColor
+
+import activity
+
+from connectionmanager import get_connection_manager
+
+import signal, os, sys
+from activity import Activity
+
+ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
+ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager'
+CHANNEL_DISPATCHER_SERVICE = 'org.freedesktop.Telepathy.ChannelDispatcher'
+CHANNEL_DISPATCHER_PATH = '/org/freedesktop/Telepathy/ChannelDispatcher'
+SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar'
+SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar'
+
+CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
+CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \
+ 'org.laptop.Telepathy.ActivityProperties'
+
+_QUERY_DBUS_TIMEOUT = 200
+"""
+Time in seconds to wait when querying contact properties. Some jabber servers
+will be very slow in returning these queries, so just be patient.
+"""
+
+
+class ActivityModel(gobject.GObject):
+ __gsignals__ = {
+ 'current-buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'current-buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+
+ def __init__(self, activity_id, room_handle):
+ gobject.GObject.__init__(self)
+
+ self.activity_id = activity_id
+ self.room_handle = room_handle
+ self._bundle = None
+ self._color = None
+ self._private = True
+ self._name = None
+ self._current_buddies = []
+ self._buddies = []
+
+ def get_color(self):
+ return self._color
+
+ def set_color(self, color):
+ self._color = color
+
+ color = gobject.property(type=object, getter=get_color, setter=set_color)
+
+ def get_bundle(self):
+ return self._bundle
+
+ def set_bundle(self, bundle):
+ self._bundle = bundle
+
+ bundle = gobject.property(type=object, getter=get_bundle,
+ setter=set_bundle)
+
+ def get_name(self):
+ return self._name
+
+ def set_name(self, name):
+ self._name = name
+
+ name = gobject.property(type=object, getter=get_name, setter=set_name)
+
+ def is_private(self):
+ return self._private
+
+ def set_private(self, private):
+ self._private = private
+
+ private = gobject.property(type=object, getter=is_private,
+ setter=set_private)
+
+ def get_buddies(self):
+ return self._buddies
+
+ def add_buddy(self, buddy):
+ self._buddies.append(buddy)
+ self.notify('buddies')
+ self.emit('buddy-added', buddy)
+
+ def remove_buddy(self, buddy):
+ self._buddies.remove(buddy)
+ self.notify('buddies')
+ self.emit('buddy-removed', buddy)
+
+ buddies = gobject.property(type=object, getter=get_buddies)
+
+ def get_current_buddies(self):
+ return self._current_buddies
+
+ def add_current_buddy(self, buddy):
+ self._current_buddies.append(buddy)
+ self.notify('current-buddies')
+ self.emit('current-buddy-added', buddy)
+
+ def remove_current_buddy(self, buddy):
+ self._current_buddies.remove(buddy)
+ self.notify('current-buddies')
+ self.emit('current-buddy-removed', buddy)
+
+ current_buddies = gobject.property(type=object, getter=get_current_buddies)
+
+
+class _Account(gobject.GObject):
+ __gsignals__ = {
+ 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object, object])),
+ 'activity-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object, object])),
+ 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object, object, object])),
+ 'buddy-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object, object])),
+ 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'buddy-joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object, object])),
+ 'buddy-left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object, object])),
+ 'current-activity-updated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([object, object])),
+ 'connected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'disconnected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ }
+
+ def __init__(self, account_path):
+ gobject.GObject.__init__(self)
+
+ self.object_path = account_path
+
+ self._connection = None
+ self._buddy_handles = {}
+ self._activity_handles = {}
+ self._self_handle = None
+
+ self._buddies_per_activity = {}
+ self._activities_per_buddy = {}
+
+ self._start_listening()
+
+ def _start_listening(self):
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path)
+ obj.Get(ACCOUNT, 'Connection',
+ reply_handler=self.__got_connection_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Account.GetConnection'))
+ obj.connect_to_signal(
+ 'AccountPropertyChanged', self.__account_property_changed_cb)
+
+ def __error_handler_cb(self, function_name, error):
+ raise RuntimeError('Error when calling %s: %s' % (function_name,
+ error))
+
+ def __got_connection_cb(self, connection_path):
+ #print('_Account.__got_connection_cb %r', connection_path)
+
+ if connection_path == '/':
+ self._check_registration_error()
+ return
+
+ self._prepare_connection(connection_path)
+
+ def _check_registration_error(self):
+ """
+ See if a previous connection attempt failed and we need to unset
+ the register flag.
+ """
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path)
+ obj.Get(ACCOUNT, 'ConnectionError',
+ reply_handler=self.__got_connection_error_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Account.GetConnectionError'))
+
+ def __got_connection_error_cb(self, error):
+ #print('_Account.__got_connection_error_cb %r', error)
+ if error == 'org.freedesktop.Telepathy.Error.RegistrationExists':
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path)
+ obj.UpdateParameters({'register': False}, [],
+ dbus_interface=ACCOUNT)
+
+ def __account_property_changed_cb(self, properties):
+ #print('_Account.__account_property_changed_cb %r %r %r',
+ #self.object_path, properties.get('Connection', None),
+ # self._connection)
+ if 'Connection' not in properties:
+ return
+ if properties['Connection'] == '/':
+ self._check_registration_error()
+ self._connection = None
+ elif self._connection is None:
+ self._prepare_connection(properties['Connection'])
+
+ def _prepare_connection(self, connection_path):
+ connection_name = connection_path.replace('/', '.')[1:]
+ print("Preparing %s" % connection_name)
+ self._connection = Connection(connection_name, connection_path,
+ ready_handler=self.__connection_ready_cb)
+
+ def __connection_ready_cb(self, connection):
+ print('_Account.__connection_ready_cb %r', connection.object_path)
+ connection.connect_to_signal('StatusChanged',
+ self.__status_changed_cb)
+
+ connection[PROPERTIES_IFACE].Get(CONNECTION,
+ 'Status',
+ reply_handler=self.__get_status_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Connection.GetStatus'))
+
+ def __get_status_cb(self, status):
+ #print('_Account.__get_status_cb %r %r',
+ #self._connection.object_path, status)
+ self._update_status(status)
+
+ def __status_changed_cb(self, status, reason):
+ #print('_Account.__status_changed_cb %r %r', status, reason)
+ self._update_status(status)
+
+ def _update_status(self, status):
+ if status == CONNECTION_STATUS_CONNECTED:
+ self._connection[PROPERTIES_IFACE].Get(CONNECTION,
+ 'SelfHandle',
+ reply_handler=self.__get_self_handle_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Connection.GetSelfHandle'))
+ self.emit('connected')
+ else:
+ for contact_handle, contact_id in self._buddy_handles.items():
+ if contact_id is not None:
+ self.emit('buddy-removed', contact_id)
+
+ for room_handle, activity_id in self._activity_handles.items():
+ self.emit('activity-removed', activity_id)
+
+ self._buddy_handles = {}
+ self._activity_handles = {}
+ self._buddies_per_activity = {}
+ self._activities_per_buddy = {}
+
+ self.emit('disconnected')
+
+ if status == CONNECTION_STATUS_DISCONNECTED:
+ self._connection = None
+
+ def __get_self_handle_cb(self, self_handle):
+ self._self_handle = self_handle
+
+ if CONNECTION_INTERFACE_CONTACT_CAPABILITIES in self._connection:
+ interface = CONNECTION_INTERFACE_CONTACT_CAPABILITIES
+ connection = self._connection[interface]
+ client_name = CLIENT + '.Sugar.FileTransfer'
+ file_transfer_channel_class = {
+ CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER,
+ CHANNEL + '.TargetHandleType': HANDLE_TYPE_CONTACT}
+ capabilities = []
+ connection.UpdateCapabilities(
+ [(client_name, [file_transfer_channel_class], capabilities)],
+ reply_handler=self.__update_capabilities_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Connection.UpdateCapabilities'))
+
+ connection = self._connection[CONNECTION_INTERFACE_ALIASING]
+ connection.connect_to_signal('AliasesChanged',
+ self.__aliases_changed_cb)
+
+ connection = self._connection[CONNECTION_INTERFACE_SIMPLE_PRESENCE]
+ connection.connect_to_signal('PresencesChanged',
+ self.__presences_changed_cb)
+
+ if CONNECTION_INTERFACE_BUDDY_INFO in self._connection:
+ connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO]
+ connection.connect_to_signal('PropertiesChanged',
+ self.__buddy_info_updated_cb,
+ byte_arrays=True)
+
+ connection.connect_to_signal('ActivitiesChanged',
+ self.__buddy_activities_changed_cb)
+
+ connection.connect_to_signal('CurrentActivityChanged',
+ self.__current_activity_changed_cb)
+ else:
+ print('Connection %s does not support OLPC buddy '
+ 'properties', self._connection.object_path)
+ pass
+
+ if CONNECTION_INTERFACE_ACTIVITY_PROPERTIES in self._connection:
+ connection = self._connection[
+ CONNECTION_INTERFACE_ACTIVITY_PROPERTIES]
+ connection.connect_to_signal(
+ 'ActivityPropertiesChanged',
+ self.__activity_properties_changed_cb)
+ else:
+ print('Connection %s does not support OLPC activity '
+ 'properties', self._connection.object_path)
+ pass
+
+ properties = {
+ CHANNEL + '.ChannelType': CHANNEL_TYPE_CONTACT_LIST,
+ CHANNEL + '.TargetHandleType': HANDLE_TYPE_LIST,
+ CHANNEL + '.TargetID': 'subscribe',
+ }
+ properties = dbus.Dictionary(properties, signature='sv')
+ connection = self._connection[CONNECTION_INTERFACE_REQUESTS]
+ is_ours, channel_path, properties = \
+ connection.EnsureChannel(properties)
+
+ channel = Channel(self._connection.service_name, channel_path)
+ channel[CHANNEL_INTERFACE_GROUP].connect_to_signal(
+ 'MembersChanged', self.__members_changed_cb)
+
+ channel[PROPERTIES_IFACE].Get(CHANNEL_INTERFACE_GROUP,
+ 'Members',
+ reply_handler=self.__get_members_ready_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Connection.GetMembers'))
+
+ def __update_capabilities_cb(self):
+ pass
+
+ def __aliases_changed_cb(self, aliases):
+ #print('_Account.__aliases_changed_cb')
+ for handle, alias in aliases:
+ if handle in self._buddy_handles:
+ #print('Got handle %r with nick %r, going to update',
+ # handle, alias)
+ properties = {CONNECTION_INTERFACE_ALIASING + '/alias': alias}
+ self.emit('buddy-updated', self._buddy_handles[handle],
+ properties)
+
+ def __presences_changed_cb(self, presences):
+ #print('_Account.__presences_changed_cb %r', presences)
+ for handle, presence in presences.iteritems():
+ if handle in self._buddy_handles:
+ presence_type, status_, message_ = presence
+ if presence_type == CONNECTION_PRESENCE_TYPE_OFFLINE:
+ contact_id = self._buddy_handles[handle]
+ del self._buddy_handles[handle]
+ self.emit('buddy-removed', contact_id)
+
+ def __buddy_info_updated_cb(self, handle, properties):
+ #print('_Account.__buddy_info_updated_cb %r', handle)
+ self.emit('buddy-updated', self._buddy_handles[handle], properties)
+
+ def __current_activity_changed_cb(self, contact_handle, activity_id,
+ room_handle):
+ #print('_Account.__current_activity_changed_cb %r %r %r',
+ # contact_handle, activity_id, room_handle)
+ if contact_handle in self._buddy_handles:
+ contact_id = self._buddy_handles[contact_handle]
+ if not activity_id and room_handle:
+ activity_id = self._activity_handles.get(room_handle, '')
+ self.emit('current-activity-updated', contact_id, activity_id)
+
+ def __get_current_activity_cb(self, contact_handle, activity_id,
+ room_handle):
+ #print('_Account.__get_current_activity_cb %r %r %r',
+ # contact_handle, activity_id, room_handle)
+ contact_id = self._buddy_handles[contact_handle]
+ self.emit('current-activity-updated', contact_id, activity_id)
+
+ def __buddy_activities_changed_cb(self, buddy_handle, activities):
+ self._update_buddy_activities(buddy_handle, activities)
+
+ def _update_buddy_activities(self, buddy_handle, activities):
+ #print('_Account._update_buddy_activities')
+ if not buddy_handle in self._buddy_handles:
+ self._buddy_handles[buddy_handle] = None
+
+ if not buddy_handle in self._activities_per_buddy:
+ self._activities_per_buddy[buddy_handle] = set()
+
+ for activity_id, room_handle in activities:
+ if room_handle not in self._activity_handles:
+ self._activity_handles[room_handle] = activity_id
+ self.emit('activity-added', room_handle, activity_id)
+
+ connection = self._connection[
+ CONNECTION_INTERFACE_ACTIVITY_PROPERTIES]
+ connection.GetProperties(room_handle,
+ reply_handler=partial(self.__get_properties_cb,
+ room_handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'ActivityProperties.GetProperties'))
+
+ # Sometimes we'll get CurrentActivityChanged before we get to
+ # know about the activity so we miss the event. In that case,
+ # request again the current activity for this buddy.
+ connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO]
+ connection.GetCurrentActivity(
+ buddy_handle,
+ reply_handler=partial(self.__get_current_activity_cb,
+ buddy_handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.GetCurrentActivity'))
+
+ if not activity_id in self._buddies_per_activity:
+ self._buddies_per_activity[activity_id] = set()
+ self._buddies_per_activity[activity_id].add(buddy_handle)
+ if activity_id not in self._activities_per_buddy[buddy_handle]:
+ self._activities_per_buddy[buddy_handle].add(activity_id)
+ if self._buddy_handles[buddy_handle] is not None:
+ self.emit('buddy-joined-activity',
+ self._buddy_handles[buddy_handle],
+ activity_id)
+
+ current_activity_ids = \
+ [activity_id for activity_id, room_handle in activities]
+ for activity_id in self._activities_per_buddy[buddy_handle].copy():
+ if not activity_id in current_activity_ids:
+ self._remove_buddy_from_activity(buddy_handle, activity_id)
+
+ def __get_properties_cb(self, room_handle, properties):
+ #print('_Account.__get_properties_cb %r %r', room_handle,
+ # properties)
+ if properties:
+ self._update_activity(room_handle, properties)
+
+ def _remove_buddy_from_activity(self, buddy_handle, activity_id):
+ if buddy_handle in self._buddies_per_activity[activity_id]:
+ self._buddies_per_activity[activity_id].remove(buddy_handle)
+
+ if activity_id in self._activities_per_buddy[buddy_handle]:
+ self._activities_per_buddy[buddy_handle].remove(activity_id)
+
+ if self._buddy_handles[buddy_handle] is not None:
+ self.emit('buddy-left-activity',
+ self._buddy_handles[buddy_handle],
+ activity_id)
+
+ if not self._buddies_per_activity[activity_id]:
+ del self._buddies_per_activity[activity_id]
+
+ for room_handle in self._activity_handles.copy():
+ if self._activity_handles[room_handle] == activity_id:
+ del self._activity_handles[room_handle]
+ break
+
+ self.emit('activity-removed', activity_id)
+
+ def __activity_properties_changed_cb(self, room_handle, properties):
+ #print('_Account.__activity_properties_changed_cb %r %r',
+ # room_handle, properties)
+ self._update_activity(room_handle, properties)
+
+ def _update_activity(self, room_handle, properties):
+ if room_handle in self._activity_handles:
+ self.emit('activity-updated', self._activity_handles[room_handle],
+ properties)
+ else:
+ #print('_Account.__activity_properties_changed_cb unknown '
+ # 'activity')
+ # We don't get ActivitiesChanged for the owner of the connection,
+ # so we query for its activities in order to find out.
+ if CONNECTION_INTERFACE_BUDDY_INFO in self._connection:
+ handle = self._self_handle
+ connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO]
+ connection.GetActivities(
+ handle,
+ reply_handler=partial(self.__got_activities_cb, handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.Getactivities'))
+
+ def __members_changed_cb(self, message, added, removed, local_pending,
+ remote_pending, actor, reason):
+ self._add_buddy_handles(added)
+
+ def __get_members_ready_cb(self, handles):
+ #print('_Account.__get_members_ready_cb %r', handles)
+ if not handles:
+ return
+
+ self._add_buddy_handles(handles)
+
+ def _add_buddy_handles(self, handles):
+ #print('_Account._add_buddy_handles %r', handles)
+ interfaces = [CONNECTION, CONNECTION_INTERFACE_ALIASING]
+ self._connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes(
+ handles, interfaces, False,
+ reply_handler=self.__get_contact_attributes_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Contacts.GetContactAttributes'))
+
+ def __got_buddy_info_cb(self, handle, nick, properties):
+ #print('_Account.__got_buddy_info_cb %r', handle)
+ self.emit('buddy-updated', self._buddy_handles[handle], properties)
+
+ def __get_contact_attributes_cb(self, attributes):
+ #print('_Account.__get_contact_attributes_cb %r',
+ # attributes.keys())
+
+ for handle in attributes.keys():
+ nick = attributes[handle][CONNECTION_INTERFACE_ALIASING + '/alias']
+
+ if handle in self._buddy_handles and \
+ not self._buddy_handles[handle] is None:
+ #print('Got handle %r with nick %r, going to update',
+ # handle, nick)
+ self.emit('buddy-updated', self._buddy_handles[handle],
+ attributes[handle])
+ else:
+ #print('Got handle %r with nick %r, going to add',
+ # handle, nick)
+
+ contact_id = attributes[handle][CONNECTION + '/contact-id']
+ self._buddy_handles[handle] = contact_id
+
+ if CONNECTION_INTERFACE_BUDDY_INFO in self._connection:
+ connection = \
+ self._connection[CONNECTION_INTERFACE_BUDDY_INFO]
+
+ connection.GetProperties(
+ handle,
+ reply_handler=partial(self.__got_buddy_info_cb, handle,
+ nick),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.GetProperties'),
+ byte_arrays=True,
+ timeout=_QUERY_DBUS_TIMEOUT)
+
+ connection.GetActivities(
+ handle,
+ reply_handler=partial(self.__got_activities_cb,
+ handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.GetActivities'),
+ timeout=_QUERY_DBUS_TIMEOUT)
+
+ connection.GetCurrentActivity(
+ handle,
+ reply_handler=partial(self.__get_current_activity_cb,
+ handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.GetCurrentActivity'),
+ timeout=_QUERY_DBUS_TIMEOUT)
+
+ self.emit('buddy-added', contact_id, nick, handle)
+
+ def __got_activities_cb(self, buddy_handle, activities):
+ #print('_Account.__got_activities_cb %r %r', buddy_handle,
+ # activities)
+ self._update_buddy_activities(buddy_handle, activities)
+
+ def enable(self):
+ #print('_Account.enable %s', self.object_path)
+ self._set_enabled(True)
+
+ def disable(self):
+ #print('_Account.disable %s', self.object_path)
+ self._set_enabled(False)
+ self._connection = None
+
+ def _set_enabled(self, value):
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path)
+ obj.Set(ACCOUNT, 'Enabled', value,
+ reply_handler=self.__set_enabled_cb,
+ error_handler=partial(self.__error_handler_cb,
+ 'Account.SetEnabled'),
+ dbus_interface='org.freedesktop.DBus.Properties')
+
+ def __set_enabled_cb(self):
+ #print('_Account.__set_enabled_cb success')
+ pass
+
+
+
+class Neighborhood(gobject.GObject):
+ __gsignals__ = {
+ 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+
+ def __init__(self, params = {}):
+ gobject.GObject.__init__(self)
+
+ self._buddies = {None: get_owner_instance()}
+ self._activities = {}
+ self._server_account = None
+ self._nicks = {}
+
+ #
+ # Jabber params
+ #
+ self._nickname = params["nickname"]
+ self._account_id = params["account_id"]
+ self._server = params["server"]
+ self._port = params["port"]
+ self._password = params["password"]
+ self._register = params["register"]
+
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts',
+ dbus_interface=PROPERTIES_IFACE,
+ reply_handler=self.__got_accounts_cb,
+ error_handler=self.__error_handler_cb)
+
+ def show_buddies(self):
+ print "\n\nBuddy list\n\n"
+ for k in self._nicks.keys():
+ try:
+ print "%s = %s" % (k, self._nicks[k])
+ except:
+ pass
+
+ print "\n\nActivities list\n\n"
+ for k in self._activities.keys():
+ try:
+ print "%s" % k
+ except:
+ pass
+
+ def __got_accounts_cb(self, account_paths):
+ self._server_account = self._ensure_server_account(account_paths)
+ self._connect_to_account(self._server_account)
+
+ def __error_handler_cb(self, error):
+ raise RuntimeError(error)
+
+
+ def _connect_to_account(self, account):
+ account.connect('buddy-added', self.__buddy_added_cb)
+ account.connect('buddy-updated', self.__buddy_updated_cb)
+ account.connect('buddy-removed', self.__buddy_removed_cb)
+ account.connect('buddy-joined-activity',
+ self.__buddy_joined_activity_cb)
+ account.connect('buddy-left-activity', self.__buddy_left_activity_cb)
+ account.connect('activity-added', self.__activity_added_cb)
+ account.connect('activity-updated', self.__activity_updated_cb)
+ account.connect('activity-removed', self.__activity_removed_cb)
+ account.connect('current-activity-updated',
+ self.__current_activity_updated_cb)
+ account.connect('connected', self.__account_connected_cb)
+ account.connect('disconnected', self.__account_disconnected_cb)
+
+ def __account_connected_cb(self, account):
+ #print('__account_connected_cb %s', account.object_path)
+ if account == self._server_account:
+ #self._link_local_account.disable()
+ pass
+
+ def __account_disconnected_cb(self, account):
+ #print('__account_disconnected_cb %s', account.object_path)
+ if account == self._server_account:
+ self._link_local_account.enable()
+
+ def _ensure_link_local_account(self, account_paths):
+ for account_path in account_paths:
+ if 'salut' in account_path:
+ #print('Already have a Salut account')
+ account = _Account(account_path)
+ account.enable()
+ return account
+
+ #print('Still dont have a Salut account, creating one')
+
+ client = gconf.client_get_default()
+ nick = client.get_string('/desktop/sugar/user/nick')
+
+ params = {
+ 'nickname': nick,
+ 'first-name': '',
+ 'last-name': '',
+ 'jid': self._get_jabber_account_id(),
+ 'published-name': nick,
+ }
+
+ properties = {
+ ACCOUNT + '.Enabled': True,
+ ACCOUNT + '.Nickname': nick,
+ ACCOUNT + '.ConnectAutomatically': True,
+ }
+
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_path = account_manager.CreateAccount('salut', 'local-xmpp',
+ 'salut', params,
+ properties)
+ return _Account(account_path)
+
+ def _ensure_server_account(self, account_paths):
+ bus = dbus.Bus()
+
+ for account_path in account_paths:
+ if 'gabble' in account_path:
+ obj_acct_mgr = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ properties = obj_acct_mgr.Get(ACCOUNT, 'Parameters')
+ if properties.has_key("server") and properties["server"] == self._server:
+ print("Enabiling account_path = %s, server = %s", account_path, self._server)
+ account = _Account(account_path)
+ account.enable()
+ return account
+
+ params = {
+ 'account': self._get_jabber_account_id(),
+ 'password': self._password,
+ 'server': self._server,
+ 'resource': 'sugar',
+ 'require-encryption': True,
+ 'ignore-ssl-errors': True,
+ 'register': self._register,
+ 'old-ssl': True,
+ 'port': dbus.UInt32(self._port),
+ }
+
+ properties = {
+ ACCOUNT + '.Enabled': True,
+ ACCOUNT + '.Nickname': self._nickname,
+ ACCOUNT + '.ConnectAutomatically': True,
+ }
+
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_path = account_manager.CreateAccount('gabble', 'jabber',
+ 'jabber', params,
+ properties)
+ return _Account(account_path)
+
+ def _get_jabber_account_id(self):
+ return self._account_id
+
+ def __jabber_server_changed_cb(self, client, timestamp, entry, *extra):
+ #print('__jabber_server_changed_cb')
+
+ bus = dbus.Bus()
+ account = bus.get_object(ACCOUNT_MANAGER_SERVICE,
+ self._server_account.object_path)
+
+ server = client.get_string(
+ '/desktop/sugar/collaboration/jabber_server')
+ account_id = self._get_jabber_account_id()
+ needs_reconnect = account.UpdateParameters({'server': server,
+ 'account': account_id,
+ 'register': True},
+ dbus.Array([], 's'),
+ dbus_interface=ACCOUNT)
+ if needs_reconnect:
+ account.Reconnect()
+
+ self._update_jid()
+
+ def __nick_changed_cb(self, client, timestamp, entry, *extra):
+ nick = client.get_string('/desktop/sugar/user/nick')
+ for account in self._server_account, self._link_local_account:
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account.object_path)
+ obj.Set(ACCOUNT, 'Nickname', nick, dbus_interface=PROPERTIES_IFACE)
+
+ self._update_jid()
+
+ def _update_jid(self):
+ bus = dbus.Bus()
+ account = bus.get_object(ACCOUNT_MANAGER_SERVICE,
+ self._link_local_account.object_path)
+
+ account_id = self._get_jabber_account_id()
+ needs_reconnect = account.UpdateParameters({'jid': account_id},
+ dbus.Array([], 's'),
+ dbus_interface=ACCOUNT)
+ if needs_reconnect:
+ account.Reconnect()
+
+ def __buddy_added_cb(self, account, contact_id, nick, handle):
+
+ self._nicks[contact_id] = nick
+ if contact_id in self._buddies:
+ #print('__buddy_added_cb buddy already tracked')
+ return
+
+ buddy = BuddyModel(
+ nick=nick,
+ account=account.object_path,
+ contact_id=contact_id,
+ handle=handle)
+ self._buddies[contact_id] = buddy
+
+ def __buddy_updated_cb(self, account, contact_id, properties):
+ #print('__buddy_updated_cb %r', contact_id)
+ if contact_id is None:
+ # Don't know the contact-id yet, will get the full state later
+ return
+
+ if contact_id not in self._buddies:
+ #print('__buddy_updated_cb Unknown buddy with contact_id'
+ # ' %r', contact_id)
+ return
+
+ buddy = self._buddies[contact_id]
+
+ is_new = buddy.props.key is None and 'key' in properties
+
+ if 'color' in properties:
+ buddy.props.color = XoColor(properties['color'])
+
+ if 'key' in properties:
+ buddy.props.key = properties['key']
+
+ if 'nick' in properties:
+ buddy.props.nick = properties['nick']
+
+ if is_new:
+ self.emit('buddy-added', buddy)
+
+ def __buddy_removed_cb(self, account, contact_id):
+ #print('Neighborhood.__buddy_removed_cb %r', contact_id)
+ try:
+ self._nicks.pop(contact_id)
+ except:
+ pass
+ if contact_id not in self._buddies:
+ #print('Neighborhood.__buddy_removed_cb Unknown buddy with '
+ # 'contact_id %r', contact_id)
+ return
+
+ buddy = self._buddies[contact_id]
+ del self._buddies[contact_id]
+
+ if buddy.props.key is not None:
+ self.emit('buddy-removed', buddy)
+
+ def __activity_added_cb(self, account, room_handle, activity_id):
+ #print('__activity_added_cb %r %r', room_handle, activity_id)
+ if activity_id in self._activities:
+ #print('__activity_added_cb activity already tracked')
+ return
+
+ activity = ActivityModel(activity_id, room_handle)
+ self._activities[activity_id] = activity
+
+ def __activity_updated_cb(self, account, activity_id, properties):
+ print('__activity_updated_cb %r %r', activity_id, properties)
+ if activity_id not in self._activities:
+ print('__activity_updated_cb Unknown activity with activity_id %r', activity_id)
+ return
+
+ ###
+ # FIXME: this should be configurable, we only care about the activity thats using this lib
+ # i.e.: Turtle Art
+
+ if properties['type']:
+ print properties['type']
+
+ # we should somehow emulate this and say we only have TurtleArtActivity
+ #registry = bundleregistry.get_registry()
+ #bundle = registry.get_bundle(properties['type'])
+ #bundle = None
+ #if not bundle:
+ # print('Ignoring shared activity we don''t have')
+ # return
+
+ activity = self._activities[activity_id]
+
+ is_new = activity.props.bundle is None
+
+ if 'color' in properties:
+ activity.props.color = XoColor(properties['color'])
+ activity.props.bundle = None # FIXME: we have no access to the bundleregistry
+ if 'name' in properties:
+ activity.props.name = properties['name']
+ if 'private' in properties:
+ activity.props.private = properties['private']
+
+ if is_new:
+ print "The activity is new"
+ self.emit('activity-added', activity)
+ else:
+ print "The activity is *NOT* new"
+
+ def __activity_removed_cb(self, account, activity_id):
+ if activity_id not in self._activities:
+ print('Unknown activity with id %s. Already removed?', activity_id)
+ return
+ activity = self._activities[activity_id]
+ del self._activities[activity_id]
+
+ self.emit('activity-removed', activity)
+
+ def __current_activity_updated_cb(self, account, contact_id, activity_id):
+ #print('__current_activity_updated_cb %r %r', contact_id,
+ # activity_id)
+ if contact_id not in self._buddies:
+ #print('__current_activity_updated_cb Unknown buddy with '
+ # 'contact_id %r', contact_id)
+ return
+ if activity_id and activity_id not in self._activities:
+ #print('__current_activity_updated_cb Unknown activity with'
+ # ' id %s', activity_id)
+ activity_id = ''
+
+ buddy = self._buddies[contact_id]
+ if buddy.props.current_activity is not None:
+ if buddy.props.current_activity.activity_id == activity_id:
+ return
+ buddy.props.current_activity.remove_current_buddy(buddy)
+
+ if activity_id:
+ activity = self._activities[activity_id]
+ buddy.props.current_activity = activity
+ activity.add_current_buddy(buddy)
+ else:
+ buddy.props.current_activity = None
+
+ def __buddy_joined_activity_cb(self, account, contact_id, activity_id):
+ if contact_id not in self._buddies:
+ #print('__buddy_joined_activity_cb Unknown buddy with '
+ # 'contact_id %r', contact_id)
+ return
+
+ if activity_id not in self._activities:
+ #print('__buddy_joined_activity_cb Unknown activity with '
+ # 'activity_id %r', activity_id)
+ return
+
+ self._activities[activity_id].add_buddy(self._buddies[contact_id])
+
+ def __buddy_left_activity_cb(self, account, contact_id, activity_id):
+ if contact_id not in self._buddies:
+ #print('__buddy_left_activity_cb Unknown buddy with '
+ # 'contact_id %r', contact_id)
+ return
+
+ if activity_id not in self._activities:
+ #print('__buddy_left_activity_cb Unknown activity with '
+ # 'activity_id %r', activity_id)
+ return
+
+ self._activities[activity_id].remove_buddy(self._buddies[contact_id])
+
+ def get_buddies(self):
+ return self._buddies.values()
+
+ def get_buddy_by_key(self, key):
+ for buddy in self._buddies.values():
+ if buddy.key == key:
+ return buddy
+ return None
+
+ def get_buddy_by_handle(self, contact_handle):
+ for buddy in self._buddies.values():
+ if not buddy.is_owner() and buddy.handle == contact_handle:
+ return buddy
+ return None
+
+ def get_activity(self, activity_id):
+ return self._activities.get(activity_id, None)
+
+ def get_activity_by_room(self, room_handle):
+ for activity in self._activities.values():
+ if activity.room_handle == room_handle:
+ return activity
+ return None
+
+ def get_activities(self):
+ return self._activities.values()
+
+_neighborhood = None
+def get_neighborhood(params = {}):
+ global _neighborhood
+ if _neighborhood is None:
+ _neighborhood = Neighborhood(params)
+ return _neighborhood
+
+if __name__ == "__main__":
+ params = {}
+ params["nickname"] = "test"
+ params["account_id"] = "test"
+ params["server"] = "localhost"
+ params["port"] = 5223
+ params["password"] = "test"
+ params["register"] = True
+
+ loop = gobject.MainLoop()
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ n = get_neighborhood(params)
+ loop.run()
diff --git a/collaboration/presenceservice.py b/collaboration/presenceservice.py
new file mode 100644
index 0000000..b6c581e
--- /dev/null
+++ b/collaboration/presenceservice.py
@@ -0,0 +1,266 @@
+# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+STABLE.
+"""
+
+import logging
+
+import gobject
+import dbus
+import dbus.exceptions
+import dbus.glib
+from dbus import PROPERTIES_IFACE
+
+from sugar.presence.buddy import Buddy, Owner
+from sugar.presence.activity import Activity
+from sugar.presence.connectionmanager import get_connection_manager
+
+from telepathy.interfaces import ACCOUNT, \
+ ACCOUNT_MANAGER, \
+ CONNECTION
+from telepathy.constants import HANDLE_TYPE_CONTACT
+
+
+_logger = logging.getLogger('sugar.presence.presenceservice')
+
+ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
+ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager'
+
+CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties'
+
+
+class PresenceService(gobject.GObject):
+ """Provides simplified access to the Telepathy framework to activities"""
+ __gsignals__ = {
+ 'activity-shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
+ gobject.TYPE_PYOBJECT])),
+ }
+
+ def __init__(self):
+ """Initialise the service and attempt to connect to events
+ """
+ gobject.GObject.__init__(self)
+
+ self._activity_cache = None
+ self._buddy_cache = {}
+
+ def get_activity(self, activity_id, warn_if_none=True):
+ """Retrieve single Activity object for the given unique id
+
+ activity_id -- unique ID for the activity
+
+ returns single Activity object or None if the activity
+ is not found using GetActivityById on the service
+ """
+ if self._activity_cache is not None:
+ if self._activity_cache.props.id != activity_id:
+ raise RuntimeError('Activities can only access their own'
+ ' shared instance')
+ return self._activity_cache
+ else:
+ connection_manager = get_connection_manager()
+ connections_per_account = \
+ connection_manager.get_connections_per_account()
+ for account_path, connection in connections_per_account.items():
+ if not connection.connected:
+ continue
+ logging.debug('Calling GetActivity on %s', account_path)
+ try:
+ room_handle = connection.connection.GetActivity(
+ activity_id,
+ dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES)
+ except dbus.exceptions.DBusException, e:
+ name = 'org.freedesktop.Telepathy.Error.NotAvailable'
+ if e.get_dbus_name() == name:
+ logging.debug("There's no shared activity with the id "
+ "%s", activity_id)
+ else:
+ raise
+ else:
+ activity = Activity(account_path, connection.connection,
+ room_handle=room_handle)
+ self._activity_cache = activity
+ return activity
+
+ return None
+
+ def get_activity_by_handle(self, connection_path, room_handle):
+ if self._activity_cache is not None:
+ if self._activity_cache.room_handle != room_handle:
+ raise RuntimeError('Activities can only access their own'
+ ' shared instance')
+ return self._activity_cache
+ else:
+ connection_manager = get_connection_manager()
+ account_path = \
+ connection_manager.get_account_for_connection(connection_path)
+
+ connection_name = connection_path.replace('/', '.')[1:]
+ bus = dbus.SessionBus()
+ connection = bus.get_object(connection_name, connection_path)
+ activity = Activity(account_path, connection,
+ room_handle=room_handle)
+ self._activity_cache = activity
+ return activity
+
+ def get_buddy(self, account_path, contact_id):
+ if (account_path, contact_id) in self._buddy_cache:
+ return self._buddy_cache[(account_path, contact_id)]
+
+ buddy = Buddy(account_path, contact_id)
+ self._buddy_cache[(account_path, contact_id)] = buddy
+ return buddy
+
+ # DEPRECATED
+ def get_buddy_by_telepathy_handle(self, tp_conn_name, tp_conn_path,
+ handle):
+ """Retrieve single Buddy object for the given public key
+
+ :Parameters:
+ `tp_conn_name` : str
+ The well-known bus name of a Telepathy connection
+ `tp_conn_path` : dbus.ObjectPath
+ The object path of the Telepathy connection
+ `handle` : int or long
+ The handle of a Telepathy contact on that connection,
+ of type HANDLE_TYPE_CONTACT. This may not be a
+ channel-specific handle.
+ :Returns: the Buddy object, or None if the buddy is not found
+ """
+
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_paths = account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts',
+ dbus_interface=PROPERTIES_IFACE)
+ for account_path in account_paths:
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ connection_path = obj.Get(ACCOUNT, 'Connection')
+ if connection_path == tp_conn_path:
+ connection_name = connection_path.replace('/', '.')[1:]
+ connection = bus.get_object(connection_name, connection_path)
+ contact_ids = connection.InspectHandles(HANDLE_TYPE_CONTACT,
+ [handle],
+ dbus_interface=CONNECTION)
+ return self.get_buddy(account_path, contact_ids[0])
+
+ raise ValueError('Unknown buddy in connection %s with handle %d',
+ tp_conn_path, handle)
+
+ def get_owner(self):
+ """Retrieves the laptop Buddy object."""
+ return Owner()
+
+ def __share_activity_cb(self, activity):
+ """Finish sharing the activity
+ """
+ self.emit('activity-shared', True, activity, None)
+
+ def __share_activity_error_cb(self, activity, error):
+ """Notify with GObject event of unsuccessful sharing of activity
+ """
+ self.emit('activity-shared', False, activity, error)
+
+ def share_activity(self, activity, properties=None, private=True):
+ if properties is None:
+ properties = {}
+
+ if 'id' not in properties:
+ properties['id'] = activity.get_id()
+
+ if 'type' not in properties:
+ properties['type'] = activity.get_bundle_id()
+
+ if 'name' not in properties:
+ properties['name'] = activity.metadata.get('title', None)
+
+ if 'color' not in properties:
+ properties['color'] = activity.metadata.get('icon-color', None)
+
+ properties['private'] = private
+
+ if self._activity_cache is not None:
+ raise ValueError('Activity %s is already tracked',
+ activity.get_id())
+
+ connection_manager = get_connection_manager()
+ account_path, connection = \
+ connection_manager.get_preferred_connection()
+
+ if connection is None:
+ self.emit('activity-shared', False, None,
+ 'No active connection available')
+ return
+
+ shared_activity = Activity(account_path, connection,
+ properties=properties)
+ self._activity_cache = shared_activity
+
+ if shared_activity.props.joined:
+ raise RuntimeError('Activity %s is already shared.' %
+ activity.props.id)
+
+ shared_activity.share(self.__share_activity_cb,
+ self.__share_activity_error_cb)
+
+ def get_preferred_connection(self):
+ """Gets the preferred telepathy connection object that an activity
+ should use when talking directly to telepathy
+
+ returns the bus name and the object path of the Telepathy connection
+ """
+ manager = get_connection_manager()
+ account_path, connection = manager.get_preferred_connection()
+ if connection is None:
+ return None
+ else:
+ return connection.requested_bus_name, connection.object_path
+
+ # DEPRECATED
+ def get(self, object_path):
+ raise NotImplementedError()
+
+ # DEPRECATED
+ def get_activities(self):
+ raise NotImplementedError()
+
+ # DEPRECATED
+ def get_activities_async(self, reply_handler=None, error_handler=None):
+ raise NotImplementedError()
+
+ # DEPRECATED
+ def get_buddies(self):
+ raise NotImplementedError()
+
+ # DEPRECATED
+ def get_buddies_async(self, reply_handler=None, error_handler=None):
+ raise NotImplementedError()
+
+
+_ps = None
+
+
+def get_instance(allow_offline_iface=False):
+ """Retrieve this process' view of the PresenceService"""
+ global _ps
+ if not _ps:
+ _ps = PresenceService()
+ return _ps
diff --git a/collaboration/telepathyclient.py b/collaboration/telepathyclient.py
new file mode 100644
index 0000000..5491530
--- /dev/null
+++ b/collaboration/telepathyclient.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import dbus
+from dbus import PROPERTIES_IFACE
+from telepathy.interfaces import CLIENT, \
+ CLIENT_APPROVER, \
+ CLIENT_HANDLER, \
+ CLIENT_INTERFACE_REQUESTS
+from telepathy.server import DBusProperties
+
+import dispatch
+
+
+SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar'
+SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar'
+
+_instance = None
+
+
+class TelepathyClient(dbus.service.Object, DBusProperties):
+ def __init__(self):
+ self._interfaces = set([CLIENT, CLIENT_HANDLER,
+ CLIENT_INTERFACE_REQUESTS, PROPERTIES_IFACE,
+ CLIENT_APPROVER])
+
+ bus = dbus.Bus()
+ bus_name = dbus.service.BusName(SUGAR_CLIENT_SERVICE, bus=bus)
+
+ dbus.service.Object.__init__(self, bus_name, SUGAR_CLIENT_PATH)
+ DBusProperties.__init__(self)
+
+ self._implement_property_get(CLIENT, {
+ 'Interfaces': lambda: list(self._interfaces),
+ })
+ self._implement_property_get(CLIENT_HANDLER, {
+ 'HandlerChannelFilter': self.__get_filters_cb,
+ })
+ self._implement_property_get(CLIENT_APPROVER, {
+ 'ApproverChannelFilter': self.__get_filters_cb,
+ })
+
+ self.got_channel = dispatch.Signal()
+ self.got_dispatch_operation = dispatch.Signal()
+
+ def __get_filters_cb(self):
+ logging.debug('__get_filters_cb')
+ filter_dict = dbus.Dictionary({}, signature='sv')
+ return dbus.Array([filter_dict], signature='a{sv}')
+
+ @dbus.service.method(dbus_interface=CLIENT_HANDLER,
+ in_signature='ooa(oa{sv})aota{sv}', out_signature='')
+ def HandleChannels(self, account, connection, channels, requests_satisfied,
+ user_action_time, handler_info):
+ logging.debug('HandleChannels\n%r\n%r\n%r\n%r\n%r\n%r\n', account,
+ connection, channels, requests_satisfied,
+ user_action_time, handler_info)
+ for channel in channels:
+ self.got_channel.send(self, account=account,
+ connection=connection, channel=channel)
+
+ @dbus.service.method(dbus_interface=CLIENT_INTERFACE_REQUESTS,
+ in_signature='oa{sv}', out_signature='')
+ def AddRequest(self, request, properties):
+ logging.debug('AddRequest\n%r\n%r', request, properties)
+
+ @dbus.service.method(dbus_interface=CLIENT_APPROVER,
+ in_signature='a(oa{sv})oa{sv}', out_signature='',
+ async_callbacks=('success_cb', 'error_cb_'))
+ def AddDispatchOperation(self, channels, dispatch_operation_path,
+ properties, success_cb, error_cb_):
+ success_cb()
+ try:
+ logging.debug('AddDispatchOperation\n%r\n%r\n%r', channels,
+ dispatch_operation_path, properties)
+
+ self.got_dispatch_operation.send(self, channels=channels,
+ dispatch_operation_path=dispatch_operation_path,
+ properties=properties)
+ except Exception, e:
+ logging.exception(e)
+
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = TelepathyClient()
+ return _instance
diff --git a/collaboration/test.py b/collaboration/test.py
new file mode 100755
index 0000000..db7be0d
--- /dev/null
+++ b/collaboration/test.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+import gobject
+import dbus
+import dbus.mainloop
+import dbus.mainloop.glib
+from connectionmanager import get_connection_manager
+
+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+
+conn_manager = get_connection_manager()
+account_path, connection = conn_manager.get_preferred_connection()
+print account_path
diff --git a/collaboration/tubeconn.py b/collaboration/tubeconn.py
new file mode 100644
index 0000000..1014a46
--- /dev/null
+++ b/collaboration/tubeconn.py
@@ -0,0 +1,114 @@
+# This should eventually land in telepathy-python, so has the same license:
+
+# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""
+STABLE.
+"""
+
+__all__ = ('TubeConnection', )
+__docformat__ = 'reStructuredText'
+
+
+import logging
+
+from dbus.connection import Connection
+
+
+logger = logging.getLogger('telepathy.tubeconn')
+
+
+class TubeConnection(Connection):
+
+ def __new__(cls, conn, tubes_iface, tube_id, address=None,
+ group_iface=None, mainloop=None):
+ # pylint: disable=W0212
+ # Confused by __new__
+ if address is None:
+ address = tubes_iface.GetDBusTubeAddress(tube_id)
+ self = super(TubeConnection, cls).__new__(cls, address,
+ mainloop=mainloop)
+
+ self._tubes_iface = tubes_iface
+ self.tube_id = tube_id
+ self.participants = {}
+ self.bus_name_to_handle = {}
+ self._mapping_watches = []
+
+ if group_iface is None:
+ method = conn.GetSelfHandle
+ else:
+ method = group_iface.GetSelfHandle
+ method(reply_handler=self._on_get_self_handle_reply,
+ error_handler=self._on_get_self_handle_error)
+
+ return self
+
+ def _on_get_self_handle_reply(self, handle):
+ # pylint: disable=W0201
+ # Confused by __new__
+ self.self_handle = handle
+ match = self._tubes_iface.connect_to_signal('DBusNamesChanged',
+ self._on_dbus_names_changed)
+ self._tubes_iface.GetDBusNames(self.tube_id,
+ reply_handler=self._on_get_dbus_names_reply,
+ error_handler=self._on_get_dbus_names_error)
+ self._dbus_names_changed_match = match
+
+ def _on_get_self_handle_error(self, e):
+ logging.basicConfig()
+ logger.error('GetSelfHandle failed: %s', e)
+
+ def close(self):
+ self._dbus_names_changed_match.remove()
+ self._on_dbus_names_changed(self.tube_id, (), self.participants.keys())
+ super(TubeConnection, self).close()
+
+ def _on_get_dbus_names_reply(self, names):
+ self._on_dbus_names_changed(self.tube_id, names, ())
+
+ def _on_get_dbus_names_error(self, e):
+ logging.basicConfig()
+ logger.error('GetDBusNames failed: %s', e)
+
+ def _on_dbus_names_changed(self, tube_id, added, removed):
+ if tube_id == self.tube_id:
+ for handle, bus_name in added:
+ if handle == self.self_handle:
+ # I've just joined - set my unique name
+ self.set_unique_name(bus_name)
+ self.participants[handle] = bus_name
+ self.bus_name_to_handle[bus_name] = handle
+
+ # call the callback while the removed people are still in
+ # participants, so their bus names are available
+ for callback in self._mapping_watches:
+ callback(added, removed)
+
+ for handle in removed:
+ bus_name = self.participants.pop(handle, None)
+ self.bus_name_to_handle.pop(bus_name, None)
+
+ def watch_participants(self, callback):
+ self._mapping_watches.append(callback)
+ if self.participants:
+ # GetDBusNames already returned: fake a participant add event
+ # immediately
+ added = []
+ for k, v in self.participants.iteritems():
+ added.append((k, v))
+ callback(added, [])
diff --git a/collaboration/xocolor.py b/collaboration/xocolor.py
new file mode 100644
index 0000000..395e345
--- /dev/null
+++ b/collaboration/xocolor.py
@@ -0,0 +1,282 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+STABLE.
+"""
+
+import random
+import logging
+
+import gconf
+
+colors = [
+['#B20008', '#FF2B34'], \
+['#FF2B34', '#B20008'], \
+['#E6000A', '#FF2B34'], \
+['#FF2B34', '#E6000A'], \
+['#FFADCE', '#FF2B34'], \
+['#9A5200', '#FF2B34'], \
+['#FF2B34', '#9A5200'], \
+['#FF8F00', '#FF2B34'], \
+['#FF2B34', '#FF8F00'], \
+['#FFC169', '#FF2B34'], \
+['#807500', '#FF2B34'], \
+['#FF2B34', '#807500'], \
+['#BE9E00', '#FF2B34'], \
+['#FF2B34', '#BE9E00'], \
+['#F8E800', '#FF2B34'], \
+['#008009', '#FF2B34'], \
+['#FF2B34', '#008009'], \
+['#00B20D', '#FF2B34'], \
+['#FF2B34', '#00B20D'], \
+['#8BFF7A', '#FF2B34'], \
+['#00588C', '#FF2B34'], \
+['#FF2B34', '#00588C'], \
+['#005FE4', '#FF2B34'], \
+['#FF2B34', '#005FE4'], \
+['#BCCDFF', '#FF2B34'], \
+['#5E008C', '#FF2B34'], \
+['#FF2B34', '#5E008C'], \
+['#7F00BF', '#FF2B34'], \
+['#FF2B34', '#7F00BF'], \
+['#D1A3FF', '#FF2B34'], \
+['#9A5200', '#FF8F00'], \
+['#FF8F00', '#9A5200'], \
+['#C97E00', '#FF8F00'], \
+['#FF8F00', '#C97E00'], \
+['#FFC169', '#FF8F00'], \
+['#807500', '#FF8F00'], \
+['#FF8F00', '#807500'], \
+['#BE9E00', '#FF8F00'], \
+['#FF8F00', '#BE9E00'], \
+['#F8E800', '#FF8F00'], \
+['#008009', '#FF8F00'], \
+['#FF8F00', '#008009'], \
+['#00B20D', '#FF8F00'], \
+['#FF8F00', '#00B20D'], \
+['#8BFF7A', '#FF8F00'], \
+['#00588C', '#FF8F00'], \
+['#FF8F00', '#00588C'], \
+['#005FE4', '#FF8F00'], \
+['#FF8F00', '#005FE4'], \
+['#BCCDFF', '#FF8F00'], \
+['#5E008C', '#FF8F00'], \
+['#FF8F00', '#5E008C'], \
+['#A700FF', '#FF8F00'], \
+['#FF8F00', '#A700FF'], \
+['#D1A3FF', '#FF8F00'], \
+['#B20008', '#FF8F00'], \
+['#FF8F00', '#B20008'], \
+['#FF2B34', '#FF8F00'], \
+['#FF8F00', '#FF2B34'], \
+['#FFADCE', '#FF8F00'], \
+['#807500', '#F8E800'], \
+['#F8E800', '#807500'], \
+['#BE9E00', '#F8E800'], \
+['#F8E800', '#BE9E00'], \
+['#FFFA00', '#EDDE00'], \
+['#008009', '#F8E800'], \
+['#F8E800', '#008009'], \
+['#00EA11', '#F8E800'], \
+['#F8E800', '#00EA11'], \
+['#8BFF7A', '#F8E800'], \
+['#00588C', '#F8E800'], \
+['#F8E800', '#00588C'], \
+['#00A0FF', '#F8E800'], \
+['#F8E800', '#00A0FF'], \
+['#BCCEFF', '#F8E800'], \
+['#5E008C', '#F8E800'], \
+['#F8E800', '#5E008C'], \
+['#AC32FF', '#F8E800'], \
+['#F8E800', '#AC32FF'], \
+['#D1A3FF', '#F8E800'], \
+['#B20008', '#F8E800'], \
+['#F8E800', '#B20008'], \
+['#FF2B34', '#F8E800'], \
+['#F8E800', '#FF2B34'], \
+['#FFADCE', '#F8E800'], \
+['#9A5200', '#F8E800'], \
+['#F8E800', '#9A5200'], \
+['#FF8F00', '#F8E800'], \
+['#F8E800', '#FF8F00'], \
+['#FFC169', '#F8E800'], \
+['#008009', '#00EA11'], \
+['#00EA11', '#008009'], \
+['#00B20D', '#00EA11'], \
+['#00EA11', '#00B20D'], \
+['#8BFF7A', '#00EA11'], \
+['#00588C', '#00EA11'], \
+['#00EA11', '#00588C'], \
+['#005FE4', '#00EA11'], \
+['#00EA11', '#005FE4'], \
+['#BCCDFF', '#00EA11'], \
+['#5E008C', '#00EA11'], \
+['#00EA11', '#5E008C'], \
+['#7F00BF', '#00EA11'], \
+['#00EA11', '#7F00BF'], \
+['#D1A3FF', '#00EA11'], \
+['#B20008', '#00EA11'], \
+['#00EA11', '#B20008'], \
+['#FF2B34', '#00EA11'], \
+['#00EA11', '#FF2B34'], \
+['#FFADCE', '#00EA11'], \
+['#9A5200', '#00EA11'], \
+['#00EA11', '#9A5200'], \
+['#FF8F00', '#00EA11'], \
+['#00EA11', '#FF8F00'], \
+['#FFC169', '#00EA11'], \
+['#807500', '#00EA11'], \
+['#00EA11', '#807500'], \
+['#BE9E00', '#00EA11'], \
+['#00EA11', '#BE9E00'], \
+['#F8E800', '#00EA11'], \
+['#00588C', '#00A0FF'], \
+['#00A0FF', '#00588C'], \
+['#005FE4', '#00A0FF'], \
+['#00A0FF', '#005FE4'], \
+['#BCCDFF', '#00A0FF'], \
+['#5E008C', '#00A0FF'], \
+['#00A0FF', '#5E008C'], \
+['#9900E6', '#00A0FF'], \
+['#00A0FF', '#9900E6'], \
+['#D1A3FF', '#00A0FF'], \
+['#B20008', '#00A0FF'], \
+['#00A0FF', '#B20008'], \
+['#FF2B34', '#00A0FF'], \
+['#00A0FF', '#FF2B34'], \
+['#FFADCE', '#00A0FF'], \
+['#9A5200', '#00A0FF'], \
+['#00A0FF', '#9A5200'], \
+['#FF8F00', '#00A0FF'], \
+['#00A0FF', '#FF8F00'], \
+['#FFC169', '#00A0FF'], \
+['#807500', '#00A0FF'], \
+['#00A0FF', '#807500'], \
+['#BE9E00', '#00A0FF'], \
+['#00A0FF', '#BE9E00'], \
+['#F8E800', '#00A0FF'], \
+['#008009', '#00A0FF'], \
+['#00A0FF', '#008009'], \
+['#00B20D', '#00A0FF'], \
+['#00A0FF', '#00B20D'], \
+['#8BFF7A', '#00A0FF'], \
+['#5E008C', '#AC32FF'], \
+['#AC32FF', '#5E008C'], \
+['#7F00BF', '#AC32FF'], \
+['#AC32FF', '#7F00BF'], \
+['#D1A3FF', '#AC32FF'], \
+['#B20008', '#AC32FF'], \
+['#AC32FF', '#B20008'], \
+['#FF2B34', '#AC32FF'], \
+['#AC32FF', '#FF2B34'], \
+['#FFADCE', '#AC32FF'], \
+['#9A5200', '#AC32FF'], \
+['#AC32FF', '#9A5200'], \
+['#FF8F00', '#AC32FF'], \
+['#AC32FF', '#FF8F00'], \
+['#FFC169', '#AC32FF'], \
+['#807500', '#AC32FF'], \
+['#AC32FF', '#807500'], \
+['#BE9E00', '#AC32FF'], \
+['#AC32FF', '#BE9E00'], \
+['#F8E800', '#AC32FF'], \
+['#008009', '#AC32FF'], \
+['#AC32FF', '#008009'], \
+['#00B20D', '#AC32FF'], \
+['#AC32FF', '#00B20D'], \
+['#8BFF7A', '#AC32FF'], \
+['#00588C', '#AC32FF'], \
+['#AC32FF', '#00588C'], \
+['#005FE4', '#AC32FF'], \
+['#AC32FF', '#005FE4'], \
+['#BCCDFF', '#AC32FF'], \
+]
+
+
+def _parse_string(color_string):
+ if not isinstance(color_string, (str, unicode)):
+ logging.error('Invalid color string: %r', color_string)
+ return None
+
+ if color_string == 'white':
+ return ['#ffffff', '#414141']
+ elif color_string == 'insensitive':
+ return ['#ffffff', '#e2e2e2']
+
+ splitted = color_string.split(',')
+ if len(splitted) == 2:
+ return [splitted[0], splitted[1]]
+ else:
+ return None
+
+
+def is_valid(color_string):
+ return (_parse_string(color_string) != None)
+
+
+class XoColor:
+
+ def __init__(self, color_string=None):
+ if color_string == None:
+ randomize = True
+ elif not is_valid(color_string):
+ logging.debug('Color string is not valid: %s, '
+ 'fallback to default', color_string)
+ client = gconf.client_get_default()
+ color_string = client.get_string('/desktop/sugar/user/color')
+ randomize = False
+ else:
+ randomize = False
+
+ if randomize:
+ n = int(random.random() * (len(colors) - 1))
+ [self.stroke, self.fill] = colors[n]
+ else:
+ [self.stroke, self.fill] = _parse_string(color_string)
+
+ def __cmp__(self, other):
+ if isinstance(other, XoColor):
+ if self.stroke == other.stroke and self.fill == other.fill:
+ return 0
+ return -1
+
+ def get_stroke_color(self):
+ return self.stroke
+
+ def get_fill_color(self):
+ return self.fill
+
+ def to_string(self):
+ return '%s,%s' % (self.stroke, self.fill)
+
+
+if __name__ == '__main__':
+ import sys
+ import re
+
+ f = open(sys.argv[1], 'r')
+
+ print 'colors = ['
+
+ for line in f.readlines():
+ match = re.match(r'fill: ([A-Z0-9]*) stroke: ([A-Z0-9]*)', line)
+ print "['#%s', '#%s'], \\" % (match.group(2), match.group(1))
+
+ print ']'
+
+ f.close()