Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon McVittie <simon.mcvittie@collabora.co.uk>2007-08-30 10:06:50 (GMT)
committer Simon McVittie <simon.mcvittie@collabora.co.uk>2007-08-30 10:06:50 (GMT)
commit319ef24bc7f41987bcd464cf34dc0746fcb9894d (patch)
tree8ad935670387b8810feab2dd3676b7af5fd80989
parentcc6191863f97a3995e7c616b539631c205350e89 (diff)
Support mutable activity properties (name, tags, color, private).
If we're in an activity's local pending set, join the channel correctly. Allow sending invitations using Activity.Invite().
-rw-r--r--src/activity.py319
-rw-r--r--src/buddy.py10
-rw-r--r--src/presenceservice.py21
3 files changed, 312 insertions, 38 deletions
diff --git a/src/activity.py b/src/activity.py
index 71f2b2d..4223166 100644
--- a/src/activity.py
+++ b/src/activity.py
@@ -41,9 +41,11 @@ _PROP_ID = "id"
_PROP_NAME = "name"
_PROP_COLOR = "color"
_PROP_TYPE = "type"
+_PROP_TAGS = "tags"
_PROP_VALID = "valid"
_PROP_LOCAL = "local"
_PROP_JOINED = "joined"
+_PROP_PRIVATE = "private"
_PROP_CUSTOM_PROPS = "custom-props"
_logger = logging.getLogger('s-p-s.activity')
@@ -76,8 +78,10 @@ class Activity(ExportedGObject):
gobject.PARAM_READWRITE |
gobject.PARAM_CONSTRUCT_ONLY),
_PROP_NAME : (str, None, None, None, gobject.PARAM_READWRITE),
+ _PROP_TAGS : (object, None, None, gobject.PARAM_READWRITE),
_PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE),
_PROP_TYPE : (str, None, None, None, gobject.PARAM_READWRITE),
+ _PROP_PRIVATE : (bool, None, None, True, gobject.PARAM_READWRITE),
_PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE),
_PROP_LOCAL : (bool, None, None, False,
gobject.PARAM_READWRITE |
@@ -110,6 +114,8 @@ class Activity(ExportedGObject):
The globally unique activity ID (required)
`name` : str
Human-readable title for the activity
+ `tags` : unicode
+ Tags for this activity
`color` : str
Activity color in #RRGGBB,#RRGGBB (stroke,fill) format
`type` : str
@@ -118,6 +124,8 @@ class Activity(ExportedGObject):
If True, this activity was initiated locally and is not
(yet) advertised on the network
(FIXME: is this description right?)
+ `private` : bool
+ If True, this activity is not advertised to everyone
`custom-props` : dict
Activity-specific properties
"""
@@ -156,6 +164,8 @@ class Activity(ExportedGObject):
self._id = None
self._actname = None
self._color = None
+ self._private = True
+ self._tags = u''
self._local = False
self._type = None
self._custom_props = {}
@@ -216,10 +226,14 @@ class Activity(ExportedGObject):
return self._id
elif pspec.name == _PROP_NAME:
return self._actname
+ elif pspec.name == _PROP_TAGS:
+ return self._tags
elif pspec.name == _PROP_COLOR:
return self._color
elif pspec.name == _PROP_TYPE:
return self._type
+ elif pspec.name == _PROP_PRIVATE:
+ return self._private
elif pspec.name == _PROP_VALID:
return self._valid
elif pspec.name == _PROP_JOINED:
@@ -245,6 +259,10 @@ class Activity(ExportedGObject):
self._actname = value
elif pspec.name == _PROP_COLOR:
self._color = value
+ elif pspec.name == _PROP_PRIVATE:
+ self._private = value
+ elif pspec.name == _PROP_TAGS:
+ self._tags = value
elif pspec.name == _PROP_TYPE:
if self._type:
raise RuntimeError("activity type is already set")
@@ -316,6 +334,16 @@ class Activity(ExportedGObject):
_logger.debug('BuddyLeft: %s', buddy_path)
@dbus.service.signal(_ACTIVITY_INTERFACE,
+ signature="a{sv}")
+ def PropertiesChanged(self, properties):
+ """Emits D-Bus signal when properties of this activity change.
+
+ The properties dict is the same as for GetProperties, but omits
+ properties that have not actually changed.
+ """
+ _logger.debug('Emitting PropertiesChanged: %r', properties)
+
+ @dbus.service.signal(_ACTIVITY_INTERFACE,
signature="o")
def NewChannel(self, channel_path):
"""Generates DBUS signal when a new channel is created for this
@@ -331,13 +359,44 @@ class Activity(ExportedGObject):
# dbus methods
@dbus.service.method(_ACTIVITY_INTERFACE,
+ in_signature="", out_signature="a{sv}")
+ def GetProperties(self):
+ """D-Bus method to get this activity's properties.
+
+ The keys of the dict are defined by Presence Service. Currently
+ the possible keys are:
+
+ `private` : bool
+ If False, the activity is advertised to everyone
+ `name` : unicode
+ The name of the activity - '' if not known yet
+ `tags` : unicode
+ The activity's tags (initially '')
+ `color` : string of the form #112233,#456789
+ The activity's icon color - '' if not known yet
+ `type` : string in the same format as a D-Bus well-known name
+ The activity type (cannot change) - '' if not known yet
+ `id` : string
+ The activity ID (cannot change) - '' if not known yet
+ """
+ ret = {_PROP_PRIVATE: self._private,
+ _PROP_NAME: self._name or u'',
+ _PROP_TAGS: self._tags,
+ _PROP_COLOR: self._color or '',
+ _PROP_TYPE: self._type or '',
+ _PROP_ID: self._id or '',
+ }
+ _logger.debug('%r', ret)
+ return ret
+
+ @dbus.service.method(_ACTIVITY_INTERFACE,
in_signature="", out_signature="s")
def GetId(self):
"""DBUS method to get this activity's (randomly generated) unique ID
:Returns: Activity ID as a string
"""
- return self._id
+ return self._id or ''
@dbus.service.method(_ACTIVITY_INTERFACE,
in_signature="", out_signature="s")
@@ -346,7 +405,7 @@ class Activity(ExportedGObject):
:Returns: Activity colour as a string in the format #RRGGBB,#RRGGBB
"""
- return self._color
+ return self._color or ''
@dbus.service.method(_ACTIVITY_INTERFACE,
in_signature="", out_signature="s")
@@ -356,7 +415,67 @@ class Activity(ExportedGObject):
:Returns: Activity type as a string, in the same form as a D-Bus
well-known name
"""
- return self._type
+ return self._type or ''
+
+ @dbus.service.method(_ACTIVITY_INTERFACE,
+ in_signature='os', out_signature='',
+ async_callbacks=('async_cb', 'async_err_cb'))
+ def Invite(self, buddy_path, message, async_cb, async_err_cb):
+ """Invite a buddy to join this activity if they are not already in it.
+
+ :Parameters:
+ `buddy` : dbus.ObjectPath
+ The buddy to be invited
+ `message` : dbus.String
+ A message to send to the buddy
+ :Raises NotJoinedError: if we're not in the activity ourselves
+ :Raises NotFoundError: if there is no such buddy
+ :Raises WrongConnectionError: if the buddy is not visible on that
+ Telepathy connection
+ :Raises telepathy.errors.PermissionDenied: if we can't invite the buddy
+ """
+ if not self._joined:
+ _logger.debug('Not inviting %s into %s: I am not a member',
+ buddy_path, self._id)
+ async_err_cb(NotJoinedError("Can't invite buddies into an "
+ "activity you haven't yourself "
+ "joined"))
+ return
+
+ assert self._tp is not None
+ assert self._text_channel is not None
+
+ buddy = self._ps.get_buddy_by_path(buddy_path)
+ if buddy is None:
+ _logger.debug('Not inviting nonexistent buddy %s', buddy_path)
+ async_err_cb(NotFoundError('Buddy not found: %s' % buddy_path))
+ return
+
+ if buddy in self._buddies:
+ # nothing to do
+ _logger.debug('Not inviting %s into %s: already a member',
+ buddy_path, self._id)
+ async_cb()
+ return
+
+ # actually invite them
+ buddy_ident = buddy.get_identifier_by_plugin(self._tp)
+ if buddy_ident is None:
+ conn_path = self._tp.get_connection().object_path
+ _logger.debug('Activity %s is on connection %s but buddy %s is '
+ 'not', self._id, conn_path, buddy_path)
+ async_err_cb(WrongConnectionError('Buddy %s cannot be '
+ 'invited to activity %s: the buddy is not on the '
+ 'Telepathy connection %s'
+ % (buddy_path, self._id, conn_path)))
+ else:
+ _logger.debug('Inviting buddy %s to activity %s via handle #%d '
+ '<%s>', buddy_path, self._id, buddy_ident[0],
+ buddy_ident[1])
+ self._text_channel.AddMembers([buddy_ident[0]], message,
+ dbus_interface=CHANNEL_INTERFACE_GROUP,
+ reply_handler=async_cb,
+ error_handler=async_err_cb)
@dbus.service.method(_ACTIVITY_INTERFACE,
in_signature="", out_signature="",
@@ -419,13 +538,76 @@ class Activity(ExportedGObject):
return self.get_channels()
@dbus.service.method(_ACTIVITY_INTERFACE,
+ in_signature='a{sv}', out_signature='')
+ def SetProperties(self, new_props):
+ """D-Bus method to update the activity's properties.
+
+ The parameter has the same keys as for GetProperties(); missing
+ keys are treated as unchanged.
+ """
+ if not self.joined:
+ raise NotJoinedError('Not in activity %s' % self._id)
+
+ changed = False
+
+ val = new_props.pop(_PROP_TYPE, None)
+ if val is not None:
+ if self._type != val:
+ raise ValueError('"type" property may not change')
+
+ val = new_props.pop(_PROP_ID, None)
+ if val is not None:
+ if self._id != val:
+ raise ValueError('"id" property may not change')
+
+ val = new_props.pop(_PROP_PRIVATE, None)
+ if val is not None:
+ if not isinstance(val, (bool, dbus.Boolean)):
+ raise ValueError('"private" property must be boolean')
+ if self._private != val:
+ self._private = val
+ changed = True
+
+ val = new_props.pop(_PROP_NAME, None)
+ if val is not None:
+ if not isinstance(val, unicode):
+ raise ValueError('"name" property must be unicode string')
+ if self._actname != val:
+ self._actname = val
+ changed = True
+
+ val = new_props.pop(_PROP_TAGS, None)
+ if val is not None:
+ if not isinstance(val, unicode):
+ raise ValueError('"tags" property must be unicode string')
+ if self._tags != val:
+ self._tags = val
+ changed = True
+
+ val = new_props.pop(_PROP_COLOR, None)
+ if val is not None:
+ if not isinstance(val, unicode):
+ raise ValueError('"color" property must be string')
+ val = val.decode('ascii')
+ if self._color != val:
+ self._color = val
+ changed = True
+
+ if changed:
+ # FIXME: pass SetProperties errors back to caller too
+ self.send_properties()
+
+ if new_props:
+ raise ValueError('Unknown properties: %s' % new_props.keys())
+
+ @dbus.service.method(_ACTIVITY_INTERFACE,
in_signature="", out_signature="s")
def GetName(self):
"""DBUS method to get this activity's name
returns Activity name
"""
- return self._actname
+ return self._actname or u''
# methods
def object_path(self):
@@ -615,11 +797,14 @@ class Activity(ExportedGObject):
def _join_activity_channel_props_listed_cb(self, channel,
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 = {
'anonymous': False, # otherwise buddy resolution breaks
- 'invite-only': self._private,
+ 'invite-only': False, # anyone who knows about the channel can join
'persistent': False, # vanish when there are no members
- 'private': self._private,
+ 'private': True, # don't appear in server room lists
}
props_to_set = []
for ident, name, sig, flags in prop_specs:
@@ -638,8 +823,7 @@ class Activity(ExportedGObject):
else:
self._joined_cb(channel)
- def _join_activity_create_channel_cb(self, chan_path):
- channel = Channel(self._tp.get_connection().service_name, chan_path)
+ def _join_activity_enter_channel_cb(self, channel):
if PROPERTIES_INTERFACE not in channel:
self._join_activity_channel_props_listed_cb(channel, ())
else:
@@ -649,6 +833,39 @@ class Activity(ExportedGObject):
channel, prop_specs),
error_handler=self._join_failed_cb)
+ def _join_activity_local_pending_listed_cb(self, channel, local_pending):
+ _logger.debug('%s has local pending set: %s', self._id, local_pending)
+ self_ident = self._ps.owner.get_identifier_by_plugin(self._tp)
+ assert self_ident is not None
+
+ # FIXME: do this asynchronously too
+ room_self_handle = channel[CHANNEL_INTERFACE_GROUP].GetSelfHandle()
+
+ if self_ident[0] in local_pending:
+ _logger.debug('I am local pending - entering room')
+ channel[CHANNEL_INTERFACE_GROUP].AddMembers([self_ident[0]], '',
+ reply_handler=lambda:
+ self._join_activity_enter_channel_cb(channel),
+ error_handler=self._join_failed_cb)
+ elif room_self_handle in local_pending:
+ _logger.debug('I am local pending with channel-specific handle - '
+ 'entering room')
+ channel[CHANNEL_INTERFACE_GROUP].AddMembers([room_self_handle], '',
+ reply_handler=lambda:
+ self._join_activity_enter_channel_cb(channel),
+ error_handler=self._join_failed_cb)
+ else:
+ _logger.debug('I am already in the room')
+ self._join_activity_enter_channel_cb(channel)
+
+ def _join_activity_create_channel_cb(self, chan_path):
+ channel = Channel(self._tp.get_connection().service_name, chan_path)
+ channel[CHANNEL_INTERFACE_GROUP].GetLocalPendingMembers(
+ reply_handler=lambda local_pending:
+ self._join_activity_local_pending_listed_cb(
+ channel, local_pending),
+ error_handler=self._join_failed_cb)
+
def _join_activity_got_handles_cb(self, handles):
assert len(handles) == 1
@@ -826,10 +1043,11 @@ class Activity(ExportedGObject):
"""
props = {}
- props['name'] = self._actname
- props['color'] = self._color
- props['type'] = self._type
+ props['name'] = self._actname or u''
+ props['color'] = self._color or ''
+ props['type'] = self._type or ''
props['private'] = self._private
+ props['tags'] = self._tags
conn = self._tp.get_connection()
@@ -850,8 +1068,9 @@ class Activity(ExportedGObject):
error_handler=properties_set)
def set_properties(self, properties):
- """Sets name, colour and/or type properties for this activity all
- at once.
+ """Sets properties for this activity from a Telepathy
+ ActivityPropertiesChanged signal or the return from the Telepathy
+ GetProperties method.
properties - Dictionary object containing properties keyed by
property names
@@ -861,37 +1080,67 @@ class Activity(ExportedGObject):
be called, resulting in a "validity-changed" signal being generated.
Called by the PresenceService on the local machine.
"""
- changed = False
+ changed_properties = {}
+
+ validity_maybe_changed = False
# split reserved properties from activity-custom properties
(rprops, cprops) = self._split_properties(properties)
- if _PROP_NAME in rprops.keys():
- name = rprops[_PROP_NAME]
- if name != self._actname:
- self._actname = name
- changed = True
- if _PROP_COLOR in rprops.keys():
- color = rprops[_PROP_COLOR]
- if color != self._color:
- self._color = color
- changed = True
+ val = rprops.get(_PROP_NAME, self._actname)
+ if isinstance(val, unicode) and val != self._actname:
+ self._actname = val
+ changed_properties[_PROP_NAME] = val
+ validity_maybe_changed = True
- if _PROP_TYPE in rprops.keys():
- type_ = rprops[_PROP_TYPE]
- if type_ != self._type:
- # Type can never be changed after first set
- if self._type:
- _logger.debug("Activity type changed by network; this "
- "is illegal")
- else:
- self._type = type_
- changed = True
+ val = bool(rprops.get(_PROP_PRIVATE, self._private))
+ if val != self._private:
+ changed_properties[_PROP_PRIVATE] = val
+ self._private = val
+
+ val = rprops.get(_PROP_TAGS, self._tags)
+ if isinstance(val, unicode) and val != self._tags:
+ changed_properties[_PROP_TAGS] = val
+ self._tags = val
+
+ val = rprops.get(_PROP_COLOR, self._color)
+ if isinstance(val, unicode):
+ try:
+ val = val.encode('ascii')
+ except UnicodeError:
+ _logger.debug('Invalid color %s', val)
+ else:
+ if val != self._color:
+ self._color = val
+ changed_properties[_PROP_COLOR] = val
+ validity_maybe_changed = True
+
+ val = rprops.get(_PROP_TYPE, self._type)
+ if isinstance(val, unicode):
+ try:
+ val = val.encode('ascii')
+ except UnicodeError:
+ _logger.debug('Invalid activity type %s', val)
+ else:
+ if val != self._type:
+ if self._type:
+ _logger.debug('Peer attempted to change activity '
+ 'type from %s to %s: ignoring',
+ self._type, val)
+ else:
+ self._type = val
+ changed_properties[_PROP_TYPE] = val
+ validity_maybe_changed = True
# Set custom properties
+ # FIXME: is this actually required? If so, it needs to go into
+ # the PropertiesChanged dict somehow
if len(cprops.keys()) > 0:
self._custom_props = cprops
- if changed:
+ if changed_properties:
+ self.PropertiesChanged(changed_properties)
+
+ if validity_maybe_changed:
self._update_validity()
def _split_properties(self, properties):
diff --git a/src/buddy.py b/src/buddy.py
index d41e1b4..94227ea 100644
--- a/src/buddy.py
+++ b/src/buddy.py
@@ -401,6 +401,16 @@ class Buddy(ExportedGObject):
props[_PROP_CURACT] = self.props.current_activity or ''
return props
+ def get_identifier_by_plugin(self, plugin):
+ """
+ :Parameters:
+ `plugin` : TelepathyPlugin
+ The Telepathy connection
+ :Returns: a tuple (Telepathy handle: integer,
+ unique identifier: str) or None
+ """
+ return self._handles.get(plugin)
+
@dbus.service.method(_BUDDY_INTERFACE,
in_signature='', out_signature='a(sou)')
def GetTelepathyHandles(self):
diff --git a/src/presenceservice.py b/src/presenceservice.py
index bc8d9e4..5f81fcf 100644
--- a/src/presenceservice.py
+++ b/src/presenceservice.py
@@ -174,6 +174,8 @@ class PresenceService(ExportedGObject):
if CONN_INTERFACE_BUDDY_INFO in conn:
def buddy_activities_changed(contact, activities):
+ _logger.debug('ActivitiesChanged on %s: (%u, %r)', tp,
+ contact, activities)
self._buddy_activities_changed(tp, contact, activities)
m = conn[CONN_INTERFACE_BUDDY_INFO].connect_to_signal(
'ActivitiesChanged', buddy_activities_changed)
@@ -255,6 +257,18 @@ class PresenceService(ExportedGObject):
for match in matches:
match.remove()
+ def get_buddy_by_path(self, path):
+ """Get the Buddy object corresponding to an object-path, or None.
+
+ :Parameters:
+ path : dbus.ObjectPath
+ The object-path of a buddy
+ :Returns: a Buddy object or None
+ """
+ if not path.startswith(BUDDY_PATH):
+ return None
+ return self._buddies.get(path[len(BUDDY_PATH):])
+
def get_buddy(self, objid):
buddy = self._buddies.get(objid)
if buddy is None:
@@ -373,6 +387,8 @@ class PresenceService(ExportedGObject):
handle_error(e, 'fetching current activity')
def got_activities(activities):
gobject.idle_add(self._run_contacts_online_queue)
+ _logger.debug('GetActivities() returned on %s contact %u: %r',
+ tp, contact, activities)
self._buddy_activities_changed(tp, contact, activities)
def get_activities():
try:
@@ -473,8 +489,6 @@ class PresenceService(ExportedGObject):
def _buddy_activities_changed(self, tp, contact_handle, activities):
activities = dict(activities)
- _logger.debug("Handle %s activities changed: %s", contact_handle,
- activities)
buddies = self._handles_buddies[tp]
buddy = buddies.get(contact_handle)
@@ -747,7 +761,8 @@ class PresenceService(ExportedGObject):
activity = Activity(self._session_bus, objid, self,
self._get_preferred_plugin(), 0,
id=actid, type=atype,
- name=name, color=color, local=True)
+ name=name, color=color, local=True,
+ private=private)
activity.connect("validity-changed",
self._activity_validity_changed_cb)
activity.connect("disappeared", self._activity_disappeared_cb)