Rename the module to sugar3
The old gtk-2 based module will be present in the 0.94 branch in the sugar-toolkit. Signed-off-by: Simon Schampijer <simon@laptop.org> Acked-by: Sascha Silbe <silbe@activitycentral.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
sugardir = $(pythondir)/sugar3/presence
|
||||
sugar_PYTHON = \
|
||||
__init__.py \
|
||||
activity.py \
|
||||
buddy.py \
|
||||
connectionmanager.py \
|
||||
sugartubeconn.py \
|
||||
tubeconn.py \
|
||||
presenceservice.py
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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.
|
||||
|
||||
"""Client-code's interface to the PresenceService
|
||||
|
||||
Provides a simplified API for accessing the dbus service
|
||||
which coordinates native network presence and sharing
|
||||
information. This includes both "buddies" and "shared
|
||||
activities".
|
||||
"""
|
||||
@@ -0,0 +1,722 @@
|
||||
# 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
|
||||
|
||||
from sugar.presence.buddy import Buddy
|
||||
|
||||
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._joined_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):
|
||||
_logger.debug('%r: Activity properties changed to %r', self,
|
||||
properties)
|
||||
self._update_properties(properties)
|
||||
|
||||
def __got_properties_cb(self, properties):
|
||||
_logger.debug('__got_properties_cb %r', properties)
|
||||
self._get_properties_call = None
|
||||
self._update_properties(properties)
|
||||
|
||||
def __error_handler_cb(self, error):
|
||||
_logger.debug('__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:
|
||||
_logger.debug('%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):
|
||||
_logger.debug('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._joined_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):
|
||||
_logger.debug('%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):
|
||||
_logger.debug('__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):
|
||||
_logger.debug('__add_initial_buddies %r', contact_ids)
|
||||
for contact_id in contact_ids:
|
||||
self._buddies[contact_id] = self._get_buddy(contact_id)
|
||||
self._joined_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):
|
||||
_logger.debug('__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(removed, 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
|
||||
if contact_id not in self._joined_buddies:
|
||||
self._joined_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
|
||||
|
||||
_logger.debug('%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):
|
||||
_logger.debug('%r: Share finished %r', self, error)
|
||||
if error is None:
|
||||
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:
|
||||
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]
|
||||
|
||||
_logger.debug('%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"""
|
||||
_logger.debug('%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):
|
||||
_logger.debug('%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.channel_self_handle = join_command.channel_self_handle
|
||||
|
||||
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):
|
||||
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):
|
||||
_logger.debug('%r: Tubes channel %r is ready', self, channel)
|
||||
self.tubes_channel = channel
|
||||
self._tubes_ready()
|
||||
|
||||
def __text_channel_ready_cb(self, channel):
|
||||
_logger.debug('%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
|
||||
|
||||
_logger.debug('%r: finished setting up tubes', self)
|
||||
|
||||
self._add_self_to_channel()
|
||||
|
||||
def __text_channel_group_flags_changed_cb(self, added, removed):
|
||||
_logger.debug('__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):
|
||||
_logger.debug('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:
|
||||
_logger.debug('%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):
|
||||
_logger.debug('__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)
|
||||
@@ -0,0 +1,248 @@
|
||||
# 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 a buddy in the presence service
|
||||
|
||||
STABLE.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import gobject
|
||||
import dbus
|
||||
import gconf
|
||||
from telepathy.interfaces import CONNECTION, \
|
||||
CONNECTION_INTERFACE_ALIASING, \
|
||||
CONNECTION_INTERFACE_CONTACTS
|
||||
from telepathy.constants import HANDLE_TYPE_CONTACT
|
||||
|
||||
from sugar.presence.connectionmanager import get_connection_manager
|
||||
|
||||
ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
|
||||
CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
|
||||
|
||||
_logger = logging.getLogger('sugar.presence.buddy')
|
||||
|
||||
|
||||
class BaseBuddy(gobject.GObject):
|
||||
"""UI interface for a Buddy in the presence service
|
||||
|
||||
Each buddy interface tracks a set of activities and properties
|
||||
that can be queried to provide UI controls for manipulating
|
||||
the presence interface.
|
||||
|
||||
Properties Dictionary:
|
||||
'key': public key,
|
||||
'nick': nickname ,
|
||||
'color': color (XXX what format),
|
||||
'current-activity': (XXX dbus path?),
|
||||
'owner': (XXX dbus path?),
|
||||
"""
|
||||
|
||||
__gtype_name__ = 'PresenceBaseBuddy'
|
||||
|
||||
__gsignals__ = {
|
||||
'joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
||||
([gobject.TYPE_PYOBJECT])),
|
||||
'left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
||||
([gobject.TYPE_PYOBJECT])),
|
||||
'property-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
||||
([gobject.TYPE_PYOBJECT])),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
gobject.GObject.__init__(self)
|
||||
|
||||
self._key = None
|
||||
self._nick = None
|
||||
self._color = None
|
||||
self._current_activity = None
|
||||
self._owner = False
|
||||
self._ip4_address = None
|
||||
self._tags = None
|
||||
|
||||
def get_key(self):
|
||||
return self._key
|
||||
|
||||
def set_key(self, key):
|
||||
self._key = key
|
||||
|
||||
key = gobject.property(type=str, getter=get_key, setter=set_key)
|
||||
|
||||
def get_nick(self):
|
||||
return self._nick
|
||||
|
||||
def set_nick(self, nick):
|
||||
self._nick = nick
|
||||
|
||||
nick = gobject.property(type=str, getter=get_nick, setter=set_nick)
|
||||
|
||||
def get_color(self):
|
||||
return self._color
|
||||
|
||||
def set_color(self, color):
|
||||
self._color = color
|
||||
|
||||
color = gobject.property(type=str, getter=get_color, setter=set_color)
|
||||
|
||||
def get_current_activity(self):
|
||||
if self._current_activity is None:
|
||||
return None
|
||||
for activity in self._activities.values():
|
||||
if activity.props.id == self._current_activity:
|
||||
return activity
|
||||
return None
|
||||
|
||||
current_activity = gobject.property(type=object,
|
||||
getter=get_current_activity)
|
||||
|
||||
def get_owner(self):
|
||||
return self._owner
|
||||
|
||||
def set_owner(self, owner):
|
||||
self._owner = owner
|
||||
|
||||
owner = gobject.property(type=bool, getter=get_owner, setter=set_owner,
|
||||
default=False)
|
||||
|
||||
def get_ip4_address(self):
|
||||
return self._ip4_address
|
||||
|
||||
def set_ip4_address(self, ip4_address):
|
||||
self._ip4_address = ip4_address
|
||||
|
||||
ip4_address = gobject.property(type=str, getter=get_ip4_address,
|
||||
setter=set_ip4_address)
|
||||
|
||||
def get_tags(self):
|
||||
return self._tags
|
||||
|
||||
def set_tags(self, tags):
|
||||
self._tags = tags
|
||||
|
||||
tags = gobject.property(type=str, getter=get_tags, setter=set_tags)
|
||||
|
||||
def object_path(self):
|
||||
"""Retrieve our dbus object path"""
|
||||
return None
|
||||
|
||||
|
||||
class Buddy(BaseBuddy):
|
||||
__gtype_name__ = 'PresenceBuddy'
|
||||
|
||||
def __init__(self, account_path, contact_id):
|
||||
_logger.debug('Buddy.__init__')
|
||||
BaseBuddy.__init__(self)
|
||||
|
||||
self._account_path = account_path
|
||||
self.contact_id = contact_id
|
||||
self.contact_handle = None
|
||||
|
||||
connection_manager = get_connection_manager()
|
||||
connection = connection_manager.get_connection(account_path)
|
||||
|
||||
connection_name = connection.object_path.replace('/', '.')[1:]
|
||||
|
||||
bus = dbus.SessionBus()
|
||||
obj = bus.get_object(connection_name, connection.object_path)
|
||||
handles = obj.RequestHandles(HANDLE_TYPE_CONTACT, [self.contact_id],
|
||||
dbus_interface=CONNECTION)
|
||||
self.contact_handle = handles[0]
|
||||
|
||||
self._get_properties_call = bus.call_async(
|
||||
connection_name,
|
||||
connection.object_path,
|
||||
CONN_INTERFACE_BUDDY_INFO,
|
||||
'GetProperties',
|
||||
'u',
|
||||
(self.contact_handle,),
|
||||
reply_handler=self.__got_properties_cb,
|
||||
error_handler=self.__error_handler_cb,
|
||||
utf8_strings=True,
|
||||
byte_arrays=True)
|
||||
|
||||
self._get_attributes_call = bus.call_async(
|
||||
connection_name,
|
||||
connection.object_path,
|
||||
CONNECTION_INTERFACE_CONTACTS,
|
||||
'GetContactAttributes',
|
||||
'auasb',
|
||||
([self.contact_handle], [CONNECTION_INTERFACE_ALIASING],
|
||||
False),
|
||||
reply_handler=self.__got_attributes_cb,
|
||||
error_handler=self.__error_handler_cb)
|
||||
|
||||
def __got_properties_cb(self, properties):
|
||||
_logger.debug('__got_properties_cb %r', properties)
|
||||
self._get_properties_call = None
|
||||
self._update_properties(properties)
|
||||
|
||||
def __got_attributes_cb(self, attributes):
|
||||
_logger.debug('__got_attributes_cb %r', attributes)
|
||||
self._get_attributes_call = None
|
||||
self._update_attributes(attributes[self.contact_handle])
|
||||
|
||||
def __error_handler_cb(self, error):
|
||||
_logger.debug('__error_handler_cb %r', error)
|
||||
|
||||
def __properties_changed_cb(self, new_props):
|
||||
_logger.debug('%r: Buddy properties changed to %r', self, new_props)
|
||||
self._update_properties(new_props)
|
||||
|
||||
def _update_properties(self, properties):
|
||||
if 'key' in properties:
|
||||
self.props.key = properties['key']
|
||||
if 'color' in properties:
|
||||
self.props.color = properties['color']
|
||||
if 'current-activity' in properties:
|
||||
self.props.current_activity = properties['current-activity']
|
||||
if 'owner' in properties:
|
||||
self.props.owner = properties['owner']
|
||||
if 'ip4-address' in properties:
|
||||
self.props.ip4_address = properties['ip4-address']
|
||||
if 'tags' in properties:
|
||||
self.props.tags = properties['tags']
|
||||
|
||||
def _update_attributes(self, attributes):
|
||||
nick_key = CONNECTION_INTERFACE_ALIASING + '/alias'
|
||||
if nick_key in attributes:
|
||||
self.props.nick = attributes[nick_key]
|
||||
|
||||
def do_get_property(self, pspec):
|
||||
if pspec.name == 'nick' and self._get_attributes_call is not None:
|
||||
_logger.debug('%r: Blocking on GetContactAttributes() because '
|
||||
'someone wants property nick', self)
|
||||
self._get_attributes_call.block()
|
||||
elif pspec.name != 'nick' and self._get_properties_call is not None:
|
||||
_logger.debug('%r: Blocking on GetProperties() because someone '
|
||||
'wants property %s', self, pspec.name)
|
||||
self._get_properties_call.block()
|
||||
|
||||
return BaseBuddy.do_get_property(self, pspec)
|
||||
|
||||
|
||||
class Owner(BaseBuddy):
|
||||
|
||||
__gtype_name__ = 'PresenceOwner'
|
||||
|
||||
def __init__(self):
|
||||
BaseBuddy.__init__(self)
|
||||
|
||||
client = gconf.client_get_default()
|
||||
self.props.nick = client.get_string('/desktop/sugar/user/nick')
|
||||
self.props.color = client.get_string('/desktop/sugar/user/color')
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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 != '/':
|
||||
self._track_connection(account_path, connection_path)
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2008 One Laptop Per Child
|
||||
#
|
||||
# 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
|
||||
|
||||
"""Subclass of TubeConnection that converts handles to Sugar Buddies
|
||||
|
||||
STABLE.
|
||||
"""
|
||||
|
||||
from telepathy.constants import (
|
||||
CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES)
|
||||
|
||||
from sugar.presence.tubeconn import TubeConnection
|
||||
from sugar.presence import presenceservice
|
||||
|
||||
|
||||
class SugarTubeConnection(TubeConnection):
|
||||
"""Subclass of TubeConnection that converts handles to Sugar Buddies"""
|
||||
|
||||
def __new__(cls, conn, tubes_iface, tube_id, address=None,
|
||||
group_iface=None, mainloop=None):
|
||||
self = super(SugarTubeConnection, cls).__new__(
|
||||
cls, conn, tubes_iface, tube_id, address=address,
|
||||
group_iface=group_iface, mainloop=mainloop)
|
||||
self._conn = conn
|
||||
self._group_iface = group_iface
|
||||
return self
|
||||
|
||||
def get_buddy(self, cs_handle):
|
||||
"""Retrieve a Buddy object given a telepathy handle.
|
||||
|
||||
cs_handle: A channel-specific CONTACT type handle.
|
||||
returns: sugar.presence Buddy object or None
|
||||
"""
|
||||
pservice = presenceservice.get_instance()
|
||||
if self.self_handle == cs_handle:
|
||||
# It's me, just get my global handle
|
||||
handle = self._conn.GetSelfHandle()
|
||||
elif self._group_iface.GetGroupFlags() & \
|
||||
CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
|
||||
# The group (channel) has channel specific handles
|
||||
handle = self._group_iface.GetHandleOwners([cs_handle])[0]
|
||||
else:
|
||||
# The group does not have channel specific handles
|
||||
handle = cs_handle
|
||||
|
||||
# deal with failure to get the handle owner
|
||||
if handle == 0:
|
||||
return None
|
||||
return pservice.get_buddy_by_telepathy_handle(
|
||||
self._conn.service_name, self._conn.object_path, handle)
|
||||
@@ -0,0 +1,26 @@
|
||||
This is a test of presence.
|
||||
|
||||
To test this service we will start up a mock dbus library:
|
||||
|
||||
>>> from sugar.testing import mockdbus
|
||||
>>> import dbus
|
||||
>>> pres_service = mockdbus.MockService(
|
||||
... 'org.laptop.Presence', '/org/laptop/Presence', name='pres')
|
||||
>>> pres_service.install()
|
||||
>>> pres_interface = dbus.Interface(pres_service, 'org.laptop.Presence')
|
||||
|
||||
Then we import the library (second, to make sure it connects to our
|
||||
mocked system, though the lazy instantiation in get_instance() should
|
||||
handle it):
|
||||
|
||||
>>> from sugar.presence import PresenceService
|
||||
>>> ps = PresenceService.get_instance()
|
||||
>>> pres_interface.make_response('getServices', [])
|
||||
>>> ps.get_services()
|
||||
Called pres.org.laptop.Presence:getServices()
|
||||
[]
|
||||
>>> pres_interface.make_response('getBuddies', [])
|
||||
>>> ps.get_buddies()
|
||||
Called pres.org.laptop.Presence:getBuddies()
|
||||
[]
|
||||
|
||||
@@ -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, [])
|
||||
Reference in New Issue
Block a user