Handle invitations using Mission Control 5.

src/sugar/activity/activity.py: If the activity is being invoked to
handle an invite, create a Client.Handler instance and share the
activity when HandleChannels is invoked.

src/sugar/activity/activityfactory.py,
src/sugar/activity/activityhandle.py,
src/sugar/activity/main.py: Add a -i switch that indicates to the
activity that it should handle the channel from an invitation.

src/sugar/presence/activity.py: Expose Activity.room_handle.

src/sugar/presence/presenceservice.py: Add get_activity_by_handle().

src/sugar/presence/util.py: Add get_account_for_connection().
This commit is contained in:
Tomeu Vizoso 2010-07-15 10:50:05 +02:00
parent af6e3aa5ef
commit 363f828205
7 changed files with 170 additions and 46 deletions

View File

@ -52,15 +52,25 @@ import logging
import os import os
import time import time
from hashlib import sha1 from hashlib import sha1
import gconf from functools import partial
import gconf
import gtk import gtk
import gobject import gobject
import dbus import dbus
import dbus.service import dbus.service
from dbus import PROPERTIES_IFACE
import cjson import cjson
from telepathy.server import DBusProperties
from telepathy.interfaces import CONNECTION, \
CHANNEL, \
CHANNEL_TYPE_TEXT, \
CLIENT, \
CLIENT_HANDLER
from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT
from sugar import util from sugar import util
from sugar import dispatch
from sugar.presence import presenceservice from sugar.presence import presenceservice
from sugar.activity.activityservice import ActivityService from sugar.activity.activityservice import ActivityService
from sugar.activity.namingalert import NamingAlert from sugar.activity.namingalert import NamingAlert
@ -88,6 +98,7 @@ J_DBUS_SERVICE = 'org.laptop.Journal'
J_DBUS_PATH = '/org/laptop/Journal' J_DBUS_PATH = '/org/laptop/Journal'
J_DBUS_INTERFACE = 'org.laptop.Journal' J_DBUS_INTERFACE = 'org.laptop.Journal'
CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties'
class _ActivitySession(gobject.GObject): class _ActivitySession(gobject.GObject):
@ -300,10 +311,50 @@ class Activity(Window, gtk.Container):
if self._jobject.metadata.has_key('share-scope'): if self._jobject.metadata.has_key('share-scope'):
share_scope = self._jobject.metadata['share-scope'] share_scope = self._jobject.metadata['share-scope']
self.shared_activity = None
self._join_id = None
if handle.handle_invite:
wait_loop = gobject.MainLoop()
self._client_handler = _ClientHandler(
self.get_bundle_id(),
partial(self.__got_channel_cb, wait_loop))
# The current API requires that self.shared_activity is set before
# exiting from __init__, so we wait until we have got the shared
# activity.
wait_loop.run()
else:
pservice = presenceservice.get_instance()
mesh_instance = pservice.get_activity(self._activity_id,
warn_if_none=False)
self._set_up_sharing(mesh_instance, share_scope)
if handle.object_id is None and create_jobject:
logging.debug('Creating a jobject.')
self._jobject = datastore.create()
title = _('%s Activity') % get_bundle_name()
self._jobject.metadata['title'] = title
self.set_title(self._jobject.metadata['title'])
self._jobject.metadata['title_set_by_user'] = '0'
self._jobject.metadata['activity'] = self.get_bundle_id()
self._jobject.metadata['activity_id'] = self.get_id()
self._jobject.metadata['keep'] = '0'
self._jobject.metadata['preview'] = ''
self._jobject.metadata['share-scope'] = SCOPE_PRIVATE
if self.shared_activity is not None:
icon_color = self.shared_activity.props.color
else:
client = gconf.client_get_default()
icon_color = client.get_string('/desktop/sugar/user/color')
self._jobject.metadata['icon-color'] = icon_color
self._jobject.file_path = ''
# Cannot call datastore.write async for creates:
# https://dev.laptop.org/ticket/3071
datastore.write(self._jobject)
def _set_up_sharing(self, mesh_instance, share_scope):
# handle activity share/join # handle activity share/join
pservice = presenceservice.get_instance()
mesh_instance = pservice.get_activity(self._activity_id,
warn_if_none=False)
logging.debug("*** Act %s, mesh instance %r, scope %s", logging.debug("*** Act %s, mesh instance %r, scope %s",
self._activity_id, mesh_instance, share_scope) self._activity_id, mesh_instance, share_scope)
if mesh_instance is not None: if mesh_instance is not None:
@ -331,29 +382,19 @@ class Activity(Window, gtk.Container):
else: else:
logging.debug('Unknown share scope %r', share_scope) logging.debug('Unknown share scope %r', share_scope)
if handle.object_id is None and create_jobject: def __got_channel_cb(self, wait_loop, connection_path, channel_path):
logging.debug('Creating a jobject.') logging.debug('Activity.__got_channel_cb')
self._jobject = datastore.create() connection_name = connection_path.replace('/', '.')[1:]
title = _('%s Activity') % get_bundle_name()
self._jobject.metadata['title'] = title
self.set_title(self._jobject.metadata['title'])
self._jobject.metadata['title_set_by_user'] = '0'
self._jobject.metadata['activity'] = self.get_bundle_id()
self._jobject.metadata['activity_id'] = self.get_id()
self._jobject.metadata['keep'] = '0'
self._jobject.metadata['preview'] = ''
self._jobject.metadata['share-scope'] = SCOPE_PRIVATE
if self.shared_activity is not None:
icon_color = self.shared_activity.props.color
else:
client = gconf.client_get_default()
icon_color = client.get_string('/desktop/sugar/user/color')
self._jobject.metadata['icon-color'] = icon_color
self._jobject.file_path = '' bus = dbus.SessionBus()
# Cannot call datastore.write async for creates: channel = bus.get_object(connection_name, channel_path)
# https://dev.laptop.org/ticket/3071 room_handle = channel.Get(CHANNEL, 'TargetHandle')
datastore.write(self._jobject)
pservice = presenceservice.get_instance()
mesh_instance = pservice.get_activity_by_handle(connection_path,
room_handle)
self._set_up_sharing(mesh_instance, SCOPE_PRIVATE)
wait_loop.quit()
def get_active(self): def get_active(self):
return self._active return self._active
@ -646,6 +687,7 @@ class Activity(Window, gtk.Container):
def __joined_cb(self, activity, success, err): def __joined_cb(self, activity, success, err):
"""Callback when join has finished""" """Callback when join has finished"""
logging.debug('Activity.__joined_cb %r', success)
self.shared_activity.disconnect(self._join_id) self.shared_activity.disconnect(self._join_id)
self._join_id = None self._join_id = None
if not success: if not success:
@ -854,6 +896,50 @@ class Activity(Window, gtk.Container):
# DEPRECATED # DEPRECATED
_shared_activity = property(lambda self: self.shared_activity, None) _shared_activity = property(lambda self: self.shared_activity, None)
SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar'
class _ClientHandler(dbus.service.Object, DBusProperties):
def __init__(self, bundle_id, got_channel_cb):
self._interfaces = set([CLIENT, CLIENT_HANDLER, PROPERTIES_IFACE])
self._got_channel_cb = got_channel_cb
bus = dbus.Bus()
name = CLIENT + '.' + bundle_id
bus_name = dbus.service.BusName(name, bus=bus)
path = '/' + name.replace('.', '/')
dbus.service.Object.__init__(self, bus_name, 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,
})
def __get_filters_cb(self):
logging.debug('__get_filters_cb')
filters = {
CHANNEL + '.ChannelType' : CHANNEL_TYPE_TEXT,
CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT,
}
filter_dict = dbus.Dictionary(filters, signature='sv')
logging.debug('__get_filters_cb %r', dbus.Array([filter_dict], signature='a{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\t%r\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r',
account, connection, channels, requests_satisfied,
user_action_time, handler_info)
try:
for channel in channels:
self._got_channel_cb(connection, channel[0])
except Exception, e:
logging.exception(e)
_session = None _session = None

View File

@ -121,7 +121,8 @@ def get_environment(activity):
return environ return environ
def get_command(activity, activity_id=None, object_id=None, uri=None): def get_command(activity, activity_id=None, object_id=None, uri=None,
activity_invite=False):
if not activity_id: if not activity_id:
activity_id = create_activity_id() activity_id = create_activity_id()
@ -133,6 +134,8 @@ def get_command(activity, activity_id=None, object_id=None, uri=None):
command.extend(['-o', object_id]) command.extend(['-o', object_id])
if uri is not None: if uri is not None:
command.extend(['-u', uri]) command.extend(['-u', uri])
if activity_invite:
command.append('-i')
# if the command is in $BUNDLE_ROOT/bin, execute the absolute path so there # if the command is in $BUNDLE_ROOT/bin, execute the absolute path so there
# is no need to mangle with the shell's PATH # is no need to mangle with the shell's PATH
@ -236,8 +239,8 @@ class ActivityCreationHandler(gobject.GObject):
environ = get_environment(self._bundle) environ = get_environment(self._bundle)
(log_path, log_file) = open_log_file(self._bundle) (log_path, log_file) = open_log_file(self._bundle)
command = get_command(self._bundle, self._handle.activity_id, command = get_command(self._bundle, self._handle.activity_id,
self._handle.object_id, self._handle.object_id, self._handle.uri,
self._handle.uri) self._handle.handle_invite)
dev_null = file('/dev/null', 'w') dev_null = file('/dev/null', 'w')
environment_dir = None environment_dir = None

View File

@ -23,7 +23,8 @@ STABLE.
class ActivityHandle(object): class ActivityHandle(object):
"""Data structure storing simple activity metadata""" """Data structure storing simple activity metadata"""
def __init__(self, activity_id=None, object_id=None, uri=None): def __init__(self, activity_id=None, object_id=None, uri=None,
handle_invite=False):
"""Initialise the handle from activity_id """Initialise the handle from activity_id
activity_id -- unique id for the activity to be activity_id -- unique id for the activity to be
@ -45,14 +46,18 @@ class ActivityHandle(object):
activity, rather than a journal object activity, rather than a journal object
(downloads stored on the file system for (downloads stored on the file system for
example or web pages) example or web pages)
handle_invite -- the activity is being launched for handling an invite
from the network
""" """
self.activity_id = activity_id self.activity_id = activity_id
self.object_id = object_id self.object_id = object_id
self.uri = uri self.uri = uri
self.handle_invite = handle_invite
def get_dict(self): def get_dict(self):
"""Retrieve our settings as a dictionary""" """Retrieve our settings as a dictionary"""
result = {'activity_id': self.activity_id} result = {'activity_id': self.activity_id,
'handle_invite': self.handle_invite}
if self.object_id: if self.object_id:
result['object_id'] = self.object_id result['object_id'] = self.object_id
if self.uri: if self.uri:
@ -65,5 +70,6 @@ def create_from_dict(handle_dict):
"""Create a handle from a dictionary of parameters""" """Create a handle from a dictionary of parameters"""
result = ActivityHandle(handle_dict['activity_id'], result = ActivityHandle(handle_dict['activity_id'],
object_id = handle_dict.get('object_id'), object_id = handle_dict.get('object_id'),
uri = handle_dict.get('uri')) uri = handle_dict.get('uri'),
handle_invite = handle_dict.get('handle_invite'))
return result return result

View File

@ -75,6 +75,10 @@ def main():
parser.add_option('-s', '--single-process', dest='single_process', parser.add_option('-s', '--single-process', dest='single_process',
action='store_true', action='store_true',
help='start all the instances in the same process') help='start all the instances in the same process')
parser.add_option('-i', '--handle-invite', dest='handle_invite',
action='store_true',
help='the activity is being launched for handling an '
'invite from the network')
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
logger.start() logger.start()
@ -121,7 +125,8 @@ def main():
activity_constructor = getattr(module, class_name) activity_constructor = getattr(module, class_name)
activity_handle = activityhandle.ActivityHandle( activity_handle = activityhandle.ActivityHandle(
activity_id=options.activity_id, activity_id=options.activity_id,
object_id=options.object_id, uri=options.uri) object_id=options.object_id, uri=options.uri,
handle_invite=options.handle_invite)
if options.single_process is True: if options.single_process is True:
sessionbus = dbus.SessionBus() sessionbus = dbus.SessionBus()

View File

@ -94,7 +94,7 @@ class Activity(gobject.GObject):
self.telepathy_text_chan = None self.telepathy_text_chan = None
self.telepathy_tubes_chan = None self.telepathy_tubes_chan = None
self._room_handle = room_handle self.room_handle = room_handle
self._join_command = None self._join_command = None
self._id = properties.get('id', None) self._id = properties.get('id', None)
self._color = properties.get('color', None) self._color = properties.get('color', None)
@ -111,7 +111,7 @@ class Activity(gobject.GObject):
self._handle_to_buddy = {} self._handle_to_buddy = {}
self._get_properties_call = None self._get_properties_call = None
if not self._room_handle is None: if not self.room_handle is None:
self._start_tracking_properties() self._start_tracking_properties()
def _start_tracking_properties(self): def _start_tracking_properties(self):
@ -122,7 +122,7 @@ class Activity(gobject.GObject):
CONN_INTERFACE_ACTIVITY_PROPERTIES, CONN_INTERFACE_ACTIVITY_PROPERTIES,
'GetProperties', 'GetProperties',
'u', 'u',
(self._room_handle,), (self.room_handle,),
reply_handler=self.__got_properties_cb, reply_handler=self.__got_properties_cb,
error_handler=self.__error_handler_cb, error_handler=self.__error_handler_cb,
utf8_strings=True) utf8_strings=True)
@ -364,12 +364,12 @@ class Activity(gobject.GObject):
_logger.debug('%r: joining', self) _logger.debug('%r: joining', self)
self._join_command = _JoinCommand(self.telepathy_conn, self._join_command = _JoinCommand(self.telepathy_conn,
self._room_handle) self.room_handle)
self._join_command.connect('finished', self.__joined_cb) self._join_command.connect('finished', self.__joined_cb)
self._join_command.run() self._join_command.run()
def share(self, share_activity_cb, share_activity_error_cb): def share(self, share_activity_cb, share_activity_error_cb):
if not self._room_handle is None: if not self.room_handle is None:
raise ValueError('Already have a room handle') raise ValueError('Already have a room handle')
self._share_command = _ShareCommand(self.telepathy_conn, self._id) self._share_command = _ShareCommand(self.telepathy_conn, self._id)
@ -384,7 +384,7 @@ class Activity(gobject.GObject):
_logger.debug('%r: Share finished %r', self, error) _logger.debug('%r: Share finished %r', self, error)
if error is None: if error is None:
self._joined = True self._joined = True
self._room_handle = share_command.room_handle self.room_handle = share_command.room_handle
self.telepathy_text_chan = share_command.text_channel self.telepathy_text_chan = share_command.text_channel
self.telepathy_tubes_chan = share_command.tubes_channel self.telepathy_tubes_chan = share_command.tubes_channel
self._publish_properties() self._publish_properties()
@ -410,7 +410,7 @@ class Activity(gobject.GObject):
logging.debug('_publish_properties calling SetProperties %r', properties) logging.debug('_publish_properties calling SetProperties %r', properties)
self.telepathy_conn.SetProperties( self.telepathy_conn.SetProperties(
self._room_handle, self.room_handle,
properties, properties,
dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES)
@ -528,7 +528,7 @@ class _JoinCommand(_BaseCommand):
_BaseCommand.__init__(self) _BaseCommand.__init__(self)
self._connection = connection self._connection = connection
self._room_handle = room_handle self.room_handle = room_handle
self._finished = False self._finished = False
self._text_channel_group_flags = None self._text_channel_group_flags = None
self.text_channel = None self.text_channel = None
@ -539,13 +539,13 @@ class _JoinCommand(_BaseCommand):
raise RuntimeError('This command has already finished') raise RuntimeError('This command has already finished')
self._connection.RequestChannel(CHANNEL_TYPE_TEXT, self._connection.RequestChannel(CHANNEL_TYPE_TEXT,
HANDLE_TYPE_ROOM, self._room_handle, True, HANDLE_TYPE_ROOM, self.room_handle, True,
reply_handler=self.__create_text_channel_cb, reply_handler=self.__create_text_channel_cb,
error_handler=self.__error_handler_cb, error_handler=self.__error_handler_cb,
dbus_interface=CONNECTION) dbus_interface=CONNECTION)
self._connection.RequestChannel(CHANNEL_TYPE_TUBES, self._connection.RequestChannel(CHANNEL_TYPE_TUBES,
HANDLE_TYPE_ROOM, self._room_handle, True, HANDLE_TYPE_ROOM, self.room_handle, True,
reply_handler=self.__create_tubes_channel_cb, reply_handler=self.__create_tubes_channel_cb,
error_handler=self.__error_handler_cb, error_handler=self.__error_handler_cb,
dbus_interface=CONNECTION) dbus_interface=CONNECTION)
@ -636,7 +636,7 @@ class _JoinCommand(_BaseCommand):
def __text_channel_members_changed_cb(self, message, added, removed, def __text_channel_members_changed_cb(self, message, added, removed,
local_pending, remote_pending, local_pending, remote_pending,
actor, reason): actor, reason):
_logger.debug('__text_channel_members_changed_cb added %r removed %r local_pending %r remote_pending %r', added, removed, local_pending, remote_pending) _logger.debug('__text_channel_members_changed_cb added %r removed %r local_pending %r remote_pending %r self_handle %r', added, removed, local_pending, remote_pending, self._self_handle)
if self._self_handle in added: if self._self_handle in added:
logging.info('KILL_PS Set the channel properties') logging.info('KILL_PS Set the channel properties')
self._finished = True self._finished = True
@ -645,9 +645,9 @@ class _JoinCommand(_BaseCommand):
return return
#_logger.debug('Activity %r text channel %u currently has %r', #_logger.debug('Activity %r text channel %u currently has %r',
# self, self._room_handle, self._handle_to_buddy) # self, self.room_handle, self._handle_to_buddy)
_logger.debug('Text channel %u members changed: + %r, - %r, LP %r, ' _logger.debug('Text channel %u members changed: + %r, - %r, LP %r, '
'RP %r, message %r, actor %r, reason %r', self._room_handle, 'RP %r, message %r, actor %r, reason %r', self.room_handle,
added, removed, local_pending, remote_pending, added, removed, local_pending, remote_pending,
message, actor, reason) message, actor, reason)
# Note: D-Bus calls this with list arguments, but after GetMembers() # Note: D-Bus calls this with list arguments, but after GetMembers()

View File

@ -280,6 +280,24 @@ class PresenceService(gobject.GObject):
return None 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_buddies(self): def get_buddies(self):
"""Retrieve set of all buddies from service """Retrieve set of all buddies from service

View File

@ -50,6 +50,12 @@ class ConnectionManager(object):
def get_connections_per_account(self): def get_connections_per_account(self):
return self._connections_per_account 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.object_path == connection_path:
return account_path
return None
_connection_manager = None _connection_manager = None
def get_connection_manager(): def get_connection_manager():