9721436536
Delay making the BusName object until after the PS object has been exported, to guarantee race-free activation.
633 lines
22 KiB
Python
633 lines
22 KiB
Python
"""An "actor" on the network, whether remote or local"""
|
|
# Copyright (C) 2007, Red Hat, Inc.
|
|
# Copyright (C) 2007, Collabora Ltd.
|
|
#
|
|
# 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 os
|
|
import gobject
|
|
import dbus
|
|
import dbus.service
|
|
from dbus.gobject_service import ExportedGObject
|
|
import psutils
|
|
|
|
from sugar import env, profile
|
|
import logging
|
|
|
|
_BUDDY_PATH = "/org/laptop/Sugar/Presence/Buddies/"
|
|
_BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy"
|
|
_OWNER_INTERFACE = "org.laptop.Sugar.Presence.Buddy.Owner"
|
|
|
|
_PROP_NICK = "nick"
|
|
_PROP_KEY = "key"
|
|
_PROP_ICON = "icon"
|
|
_PROP_CURACT = "current-activity"
|
|
_PROP_COLOR = "color"
|
|
_PROP_OWNER = "owner"
|
|
_PROP_VALID = "valid"
|
|
|
|
# Will go away soon
|
|
_PROP_IP4_ADDRESS = "ip4-address"
|
|
|
|
_logger = logging.getLogger('s-p-s.buddy')
|
|
|
|
|
|
class Buddy(ExportedGObject):
|
|
"""Person on the network (tracks properties and shared activites)
|
|
|
|
The Buddy is a collection of metadata describing a particular
|
|
actor/person on the network. The Buddy object tracks a set of
|
|
activities which the actor has shared with the presence service.
|
|
|
|
Buddies have a "valid" property which is used to flag Buddies
|
|
which are no longer reachable. That is, a Buddy may represent
|
|
a no-longer reachable target on the network.
|
|
|
|
The Buddy emits GObject events that the PresenceService uses
|
|
to track changes in its status.
|
|
|
|
Attributes:
|
|
|
|
_activities -- dictionary mapping activity ID to
|
|
activity.Activity objects
|
|
handles -- dictionary mapping Telepathy client plugin to
|
|
contact handle (an integer representing the JID or unique ID);
|
|
channel-specific handles do not appear here
|
|
"""
|
|
|
|
__gsignals__ = {
|
|
'validity-changed':
|
|
# The buddy's validity changed.
|
|
# Validity starts off False, and becomes True when the buddy
|
|
# has a color, a nick and a key.
|
|
# * the new validity: bool
|
|
(gobject.SIGNAL_RUN_FIRST, None, [bool]),
|
|
'property-changed':
|
|
# One of the buddy's properties has changed.
|
|
# * those properties that have changed:
|
|
# dict { str => object }
|
|
(gobject.SIGNAL_RUN_FIRST, None, [object]),
|
|
'icon-changed':
|
|
# The buddy's icon changed.
|
|
# * the bytes of the icon: str
|
|
(gobject.SIGNAL_RUN_FIRST, None, [object]),
|
|
'disappeared':
|
|
# The buddy is offline (has no Telepathy handles and is not the
|
|
# Owner)
|
|
(gobject.SIGNAL_RUN_FIRST, None, []),
|
|
}
|
|
|
|
__gproperties__ = {
|
|
_PROP_KEY : (str, None, None, None,
|
|
gobject.PARAM_READWRITE |
|
|
gobject.PARAM_CONSTRUCT_ONLY),
|
|
_PROP_ICON : (object, None, None, gobject.PARAM_READWRITE),
|
|
_PROP_NICK : (str, None, None, None, gobject.PARAM_READWRITE),
|
|
_PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE),
|
|
_PROP_CURACT : (str, None, None, None, gobject.PARAM_READWRITE),
|
|
_PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE),
|
|
_PROP_OWNER : (bool, None, None, False, gobject.PARAM_READABLE),
|
|
_PROP_IP4_ADDRESS : (str, None, None, None, gobject.PARAM_READWRITE)
|
|
}
|
|
|
|
def __init__(self, bus, object_id, **kwargs):
|
|
"""Initialize the Buddy object
|
|
|
|
bus -- connection to the D-Bus session bus
|
|
object_id -- the activity's unique identifier
|
|
kwargs -- used to initialize the object's properties
|
|
|
|
constructs a DBUS "object path" from the _BUDDY_PATH
|
|
and object_id
|
|
"""
|
|
if not object_id or not isinstance(object_id, int):
|
|
raise ValueError("object id must be a valid number")
|
|
|
|
self._object_path = _BUDDY_PATH + str(object_id)
|
|
|
|
self._activities = {} # Activity ID -> Activity
|
|
self._activity_sigids = {}
|
|
self.handles = {} # tp client -> handle
|
|
|
|
self._valid = False
|
|
self._owner = False
|
|
self._key = None
|
|
self._icon = ''
|
|
self._current_activity = None
|
|
self._nick = None
|
|
self._color = None
|
|
self._ip4_address = None
|
|
|
|
if not kwargs.get(_PROP_KEY):
|
|
raise ValueError("key required")
|
|
|
|
_ALLOWED_INIT_PROPS = [_PROP_NICK, _PROP_KEY, _PROP_ICON,
|
|
_PROP_CURACT, _PROP_COLOR, _PROP_IP4_ADDRESS]
|
|
for (key, value) in kwargs.items():
|
|
if key not in _ALLOWED_INIT_PROPS:
|
|
_logger.debug("Invalid init property '%s'; ignoring..." % key)
|
|
del kwargs[key]
|
|
|
|
# Set icon after superclass init, because it sends DBus and GObject
|
|
# signals when set
|
|
icon_data = None
|
|
if kwargs.has_key(_PROP_ICON):
|
|
icon_data = kwargs[_PROP_ICON]
|
|
del kwargs[_PROP_ICON]
|
|
|
|
ExportedGObject.__init__(self, bus, self._object_path,
|
|
gobject_properties=kwargs)
|
|
|
|
if icon_data:
|
|
self.props.icon = icon_data
|
|
|
|
def do_get_property(self, pspec):
|
|
"""Retrieve current value for the given property specifier
|
|
|
|
pspec -- property specifier with a "name" attribute
|
|
"""
|
|
if pspec.name == _PROP_KEY:
|
|
return self._key
|
|
elif pspec.name == _PROP_ICON:
|
|
return self._icon
|
|
elif pspec.name == _PROP_NICK:
|
|
return self._nick
|
|
elif pspec.name == _PROP_COLOR:
|
|
return self._color
|
|
elif pspec.name == _PROP_CURACT:
|
|
if not self._current_activity:
|
|
return None
|
|
if not self._activities.has_key(self._current_activity):
|
|
return None
|
|
return self._current_activity
|
|
elif pspec.name == _PROP_VALID:
|
|
return self._valid
|
|
elif pspec.name == _PROP_OWNER:
|
|
return self._owner
|
|
elif pspec.name == _PROP_IP4_ADDRESS:
|
|
return self._ip4_address
|
|
|
|
def do_set_property(self, pspec, value):
|
|
"""Set given property
|
|
|
|
pspec -- property specifier with a "name" attribute
|
|
value -- value to set
|
|
|
|
emits 'icon-changed' signal on icon setting
|
|
calls _update_validity on all calls
|
|
"""
|
|
if pspec.name == _PROP_ICON:
|
|
if str(value) != self._icon:
|
|
self._icon = str(value)
|
|
self.IconChanged(self._icon)
|
|
self.emit('icon-changed', self._icon)
|
|
elif pspec.name == _PROP_NICK:
|
|
self._nick = value
|
|
elif pspec.name == _PROP_COLOR:
|
|
self._color = value
|
|
elif pspec.name == _PROP_CURACT:
|
|
self._current_activity = value
|
|
elif pspec.name == _PROP_KEY:
|
|
if self._key:
|
|
raise RuntimeError("Key already set.")
|
|
self._key = value
|
|
elif pspec.name == _PROP_IP4_ADDRESS:
|
|
self._ip4_address = value
|
|
|
|
self._update_validity()
|
|
|
|
# dbus signals
|
|
@dbus.service.signal(_BUDDY_INTERFACE,
|
|
signature="ay")
|
|
def IconChanged(self, icon_data):
|
|
"""Generates DBUS signal with icon_data"""
|
|
|
|
@dbus.service.signal(_BUDDY_INTERFACE,
|
|
signature="o")
|
|
def JoinedActivity(self, activity_path):
|
|
"""Generates DBUS signal when buddy joins activity
|
|
|
|
activity_path -- DBUS path to the activity object
|
|
"""
|
|
|
|
@dbus.service.signal(_BUDDY_INTERFACE,
|
|
signature="o")
|
|
def LeftActivity(self, activity_path):
|
|
"""Generates DBUS signal when buddy leaves activity
|
|
|
|
activity_path -- DBUS path to the activity object
|
|
"""
|
|
|
|
@dbus.service.signal(_BUDDY_INTERFACE,
|
|
signature="a{sv}")
|
|
def PropertyChanged(self, updated):
|
|
"""Generates DBUS signal when buddy's property changes
|
|
|
|
updated -- updated property-set (dictionary) with the
|
|
Buddy's property (changed) values. Note: not the
|
|
full set of properties, just the changes.
|
|
"""
|
|
|
|
def add_telepathy_handle(self, tp_client, handle):
|
|
"""Add a Telepathy handle."""
|
|
conn = tp_client.get_connection()
|
|
self.TelepathyHandleAdded(conn.service_name, conn.object_path, handle)
|
|
self.handles[tp_client] = handle
|
|
|
|
@dbus.service.signal(_BUDDY_INTERFACE, signature='sou')
|
|
def TelepathyHandleAdded(self, tp_conn_name, tp_conn_path, handle):
|
|
"""Another Telepathy handle has become associated with the buddy.
|
|
|
|
This must only be emitted for non-channel-specific handles.
|
|
|
|
tp_conn_name -- The bus name at which the Telepathy connection may be
|
|
found
|
|
tp_conn_path -- The object path at which the Telepathy connection may
|
|
be found
|
|
handle -- The handle of type CONTACT, which is not channel-specific,
|
|
newly associated with the buddy
|
|
"""
|
|
|
|
def remove_telepathy_handle(self, tp_client, handle):
|
|
"""Remove a Telepathy handle."""
|
|
conn = tp_client.get_connection()
|
|
my_handle = self.handles.get(tp_client, 0)
|
|
if my_handle == handle:
|
|
del self.handles[tp_client]
|
|
self.TelepathyHandleRemoved(conn.service_name, conn.object_path,
|
|
handle)
|
|
# the Owner can't disappear - that would be silly
|
|
if not self.handles and not self._owner:
|
|
self.emit('disappeared')
|
|
else:
|
|
_logger.debug('Telepathy handle %u supposedly removed, but '
|
|
'my handle on that connection is %u - ignoring',
|
|
handle, my_handle)
|
|
|
|
@dbus.service.signal(_BUDDY_INTERFACE, signature='sou')
|
|
def TelepathyHandleRemoved(self, tp_conn_name, tp_conn_path, handle):
|
|
"""A Telepathy handle has ceased to be associated with the buddy,
|
|
probably because that contact went offline.
|
|
|
|
The parameters are the same as for TelepathyHandleAdded.
|
|
"""
|
|
|
|
# dbus methods
|
|
@dbus.service.method(_BUDDY_INTERFACE,
|
|
in_signature="", out_signature="ay")
|
|
def GetIcon(self):
|
|
"""Retrieve Buddy's icon data
|
|
|
|
returns empty string or dbus.ByteArray
|
|
"""
|
|
if not self.props.icon:
|
|
return ""
|
|
return dbus.ByteArray(self.props.icon)
|
|
|
|
@dbus.service.method(_BUDDY_INTERFACE,
|
|
in_signature="", out_signature="ao")
|
|
def GetJoinedActivities(self):
|
|
"""Retrieve set of Buddy's joined activities (paths)
|
|
|
|
returns list of dbus service paths for the Buddy's joined
|
|
activities
|
|
"""
|
|
acts = []
|
|
for act in self.get_joined_activities():
|
|
if act.props.valid:
|
|
acts.append(act.object_path())
|
|
return acts
|
|
|
|
@dbus.service.method(_BUDDY_INTERFACE,
|
|
in_signature="", out_signature="a{sv}")
|
|
def GetProperties(self):
|
|
"""Retrieve set of Buddy's properties
|
|
|
|
returns dictionary of
|
|
nick : str(nickname)
|
|
owner : bool( whether this Buddy is an owner??? )
|
|
XXX what is the owner flag for?
|
|
key : str(public-key)
|
|
color: Buddy's icon colour
|
|
XXX what type?
|
|
current-activity: Buddy's current activity_id, or
|
|
"" if no current activity
|
|
"""
|
|
props = {}
|
|
props[_PROP_NICK] = self.props.nick
|
|
props[_PROP_OWNER] = self.props.owner
|
|
props[_PROP_KEY] = self.props.key
|
|
props[_PROP_COLOR] = self.props.color
|
|
|
|
if self.props.ip4_address:
|
|
props[_PROP_IP4_ADDRESS] = self.props.ip4_address
|
|
else:
|
|
props[_PROP_IP4_ADDRESS] = ""
|
|
|
|
if self.props.current_activity:
|
|
props[_PROP_CURACT] = self.props.current_activity
|
|
else:
|
|
props[_PROP_CURACT] = ""
|
|
return props
|
|
|
|
@dbus.service.method(_BUDDY_INTERFACE,
|
|
in_signature='', out_signature='a(sou)')
|
|
def GetTelepathyHandles(self):
|
|
"""Return a list of non-channel-specific Telepathy contact handles
|
|
associated with this Buddy.
|
|
|
|
:Returns:
|
|
An array of triples (connection well-known bus name, connection
|
|
object path, handle).
|
|
"""
|
|
ret = []
|
|
for plugin in self.handles:
|
|
conn = plugin.get_connection()
|
|
ret.append((str(conn.service_name), conn.object_path,
|
|
self.handles[plugin]))
|
|
|
|
# methods
|
|
def object_path(self):
|
|
"""Retrieve our dbus.ObjectPath object"""
|
|
return dbus.ObjectPath(self._object_path)
|
|
|
|
def _activity_validity_changed_cb(self, activity, valid):
|
|
"""Join or leave the activity when its validity changes"""
|
|
if valid:
|
|
self.JoinedActivity(activity.object_path())
|
|
else:
|
|
self.LeftActivity(activity.object_path())
|
|
|
|
def add_activity(self, activity):
|
|
"""Add an activity to the Buddy's set of activities
|
|
|
|
activity -- activity.Activity instance
|
|
|
|
calls JoinedActivity
|
|
"""
|
|
actid = activity.props.id
|
|
if self._activities.has_key(actid):
|
|
return
|
|
self._activities[actid] = activity
|
|
# join/leave activity when it's validity changes
|
|
sigid = activity.connect("validity-changed",
|
|
self._activity_validity_changed_cb)
|
|
self._activity_sigids[actid] = sigid
|
|
if activity.props.valid:
|
|
self.JoinedActivity(activity.object_path())
|
|
|
|
def remove_activity(self, activity):
|
|
"""Remove the activity from the Buddy's set of activities
|
|
|
|
activity -- activity.Activity instance
|
|
|
|
calls LeftActivity
|
|
"""
|
|
actid = activity.props.id
|
|
if not self._activities.has_key(actid):
|
|
return
|
|
activity.disconnect(self._activity_sigids[actid])
|
|
del self._activity_sigids[actid]
|
|
del self._activities[actid]
|
|
if activity.props.valid:
|
|
self.LeftActivity(activity.object_path())
|
|
|
|
def get_joined_activities(self):
|
|
"""Retrieves list of still-valid activity objects"""
|
|
acts = []
|
|
for act in self._activities.values():
|
|
acts.append(act)
|
|
return acts
|
|
|
|
def set_properties(self, properties):
|
|
"""Set the given set of properties on the object
|
|
|
|
properties -- set of property values to set
|
|
|
|
if no change, no events generated
|
|
if change, generates property-changed and
|
|
calls _update_validity
|
|
"""
|
|
changed = False
|
|
changed_props = {}
|
|
if _PROP_NICK in properties.keys():
|
|
nick = properties[_PROP_NICK]
|
|
if nick != self._nick:
|
|
self._nick = nick
|
|
changed_props[_PROP_NICK] = nick
|
|
changed = True
|
|
if _PROP_COLOR in properties.keys():
|
|
color = properties[_PROP_COLOR]
|
|
if color != self._color:
|
|
self._color = color
|
|
changed_props[_PROP_COLOR] = color
|
|
changed = True
|
|
if _PROP_CURACT in properties.keys():
|
|
curact = properties[_PROP_CURACT]
|
|
if curact != self._current_activity:
|
|
self._current_activity = curact
|
|
changed_props[_PROP_CURACT] = curact
|
|
changed = True
|
|
if _PROP_IP4_ADDRESS in properties.keys():
|
|
ip4addr = properties[_PROP_IP4_ADDRESS]
|
|
if ip4addr != self._ip4_address:
|
|
self._ip4_address = ip4addr
|
|
changed_props[_PROP_IP4_ADDRESS] = ip4addr
|
|
changed = True
|
|
|
|
if not changed or not len(changed_props.keys()):
|
|
return
|
|
|
|
# Try emitting PropertyChanged before updating validity
|
|
# to avoid leaking a PropertyChanged signal before the buddy is
|
|
# actually valid the first time after creation
|
|
if self._valid:
|
|
dbus_changed = {}
|
|
for key, value in changed_props.items():
|
|
if value:
|
|
dbus_changed[key] = value
|
|
else:
|
|
dbus_changed[key] = ""
|
|
self.PropertyChanged(dbus_changed)
|
|
|
|
self.emit('property-changed', changed_props)
|
|
|
|
self._update_validity()
|
|
|
|
def _update_validity(self):
|
|
"""Check whether we are now valid
|
|
|
|
validity is True if color, nick and key are non-null
|
|
|
|
emits validity-changed if we have changed validity
|
|
"""
|
|
try:
|
|
old_valid = self._valid
|
|
if self._color and self._nick and self._key:
|
|
self._valid = True
|
|
else:
|
|
self._valid = False
|
|
|
|
if old_valid != self._valid:
|
|
self.emit("validity-changed", self._valid)
|
|
except AttributeError:
|
|
self._valid = False
|
|
|
|
|
|
class GenericOwner(Buddy):
|
|
"""Common functionality for Local User-like objects
|
|
|
|
The TestOwner wants to produce something *like* a
|
|
ShellOwner, but with randomised changes and the like.
|
|
This class provides the common features for a real
|
|
local owner and a testing one.
|
|
"""
|
|
__gtype_name__ = "GenericOwner"
|
|
|
|
def __init__(self, ps, bus, object_id, **kwargs):
|
|
"""Initialize the GenericOwner instance
|
|
|
|
ps -- presenceservice.PresenceService object
|
|
bus -- a connection to the D-Bus session bus
|
|
object_id -- the activity's unique identifier
|
|
kwargs -- used to initialize the object's properties
|
|
|
|
calls Buddy.__init__
|
|
"""
|
|
self._ps = ps
|
|
self._server = kwargs.pop("server", "olpc.collabora.co.uk")
|
|
self._key_hash = kwargs.pop("key_hash", None)
|
|
self._registered = kwargs.pop("registered", False)
|
|
|
|
self._ip4_addr_monitor = psutils.IP4AddressMonitor.get_instance()
|
|
self._ip4_addr_monitor.connect("address-changed",
|
|
self._ip4_address_changed_cb)
|
|
if self._ip4_addr_monitor.props.address:
|
|
kwargs["ip4-address"] = self._ip4_addr_monitor.props.address
|
|
|
|
Buddy.__init__(self, bus, object_id, **kwargs)
|
|
self._owner = True
|
|
|
|
self._bus = dbus.SessionBus()
|
|
|
|
def _ip4_address_changed_cb(self, monitor, address):
|
|
"""Handle IPv4 address change, set property to generate event"""
|
|
props = {_PROP_IP4_ADDRESS: address}
|
|
self.set_properties(props)
|
|
|
|
def get_registered(self):
|
|
"""Retrieve whether owner has registered with presence server"""
|
|
return self._registered
|
|
|
|
def get_server(self):
|
|
"""Retrieve XMPP server hostname (used by the server plugin)"""
|
|
return self._server
|
|
|
|
def get_key_hash(self):
|
|
"""Retrieve the user's private-key hash (used by the server plugin
|
|
as a password)
|
|
"""
|
|
return self._key_hash
|
|
|
|
def set_registered(self, registered):
|
|
"""Customisation point: handle the registration of the owner"""
|
|
raise RuntimeError("Subclasses must implement")
|
|
|
|
|
|
class ShellOwner(GenericOwner):
|
|
"""Representation of the local-machine owner using Sugar's Shell
|
|
|
|
The ShellOwner uses the Sugar Shell's dbus services to
|
|
register for updates about the user's profile description.
|
|
"""
|
|
__gtype_name__ = "ShellOwner"
|
|
|
|
_SHELL_SERVICE = "org.laptop.Shell"
|
|
_SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner"
|
|
_SHELL_PATH = "/org/laptop/Shell"
|
|
|
|
def __init__(self, ps, bus, object_id, test=False):
|
|
"""Initialize the ShellOwner instance
|
|
|
|
ps -- presenceservice.PresenceService object
|
|
bus -- a connection to the D-Bus session bus
|
|
object_id -- the activity's unique identifier
|
|
test -- ignored
|
|
|
|
Retrieves initial property values from the profile
|
|
module. Loads the buddy icon from file as well.
|
|
XXX note: no error handling on that
|
|
|
|
calls GenericOwner.__init__
|
|
"""
|
|
server = profile.get_server()
|
|
key_hash = profile.get_private_key_hash()
|
|
registered = profile.get_server_registered()
|
|
key = profile.get_pubkey()
|
|
nick = profile.get_nick_name()
|
|
color = profile.get_color().to_string()
|
|
|
|
icon_file = os.path.join(env.get_profile_path(), "buddy-icon.jpg")
|
|
f = open(icon_file, "r")
|
|
icon = f.read()
|
|
f.close()
|
|
|
|
GenericOwner.__init__(self, ps, bus, object_id, key=key,
|
|
nick=nick, color=color, icon=icon, server=server,
|
|
key_hash=key_hash, registered=registered)
|
|
|
|
# Ask to get notifications on Owner object property changes in the
|
|
# shell. If it's not currently running, no problem - we'll get the
|
|
# signals when it does run
|
|
for (signal, cb) in (('IconChanged', self._icon_changed_cb),
|
|
('ColorChanged', self._color_changed_cb),
|
|
('NickChanged', self._nick_changed_cb)):
|
|
self._bus.add_signal_receiver(cb, signal_name=signal,
|
|
dbus_interface=self._SHELL_OWNER_INTERFACE,
|
|
bus_name=self._SHELL_SERVICE,
|
|
path=self._SHELL_PATH)
|
|
|
|
def set_registered(self, value):
|
|
"""Handle notification that we have been registered"""
|
|
if value:
|
|
profile.set_server_registered()
|
|
|
|
def _icon_changed_cb(self, icon):
|
|
"""Handle icon change, set property to generate event"""
|
|
self.props.icon = icon
|
|
|
|
def _color_changed_cb(self, color):
|
|
"""Handle color change, set property to generate event"""
|
|
props = {_PROP_COLOR: color}
|
|
self.set_properties(props)
|
|
|
|
def _nick_changed_cb(self, nick):
|
|
"""Handle nickname change, set property to generate event"""
|
|
props = {_PROP_NICK: nick}
|
|
self.set_properties(props)
|
|
|
|
def _cur_activity_changed_cb(self, activity_id):
|
|
"""Handle current-activity change, set property to generate event
|
|
|
|
Filters out local activities (those not in self.activites)
|
|
because the network users can't join those activities, so
|
|
the activity_id shared will be None in those cases...
|
|
"""
|
|
if not self._activities.has_key(activity_id):
|
|
# This activity is local-only
|
|
activity_id = None
|
|
props = {_PROP_CURACT: activity_id}
|
|
self.set_properties(props)
|