From ee6c1b428389268892889bc634650429c61b38b8 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Wed, 30 May 2007 17:36:42 +0100 Subject: [PATCH] services/presence/: Have joined Activities track membership via group interface. This allows us to ignore the (trivially spoofable) PEP info for activities that we're actually in, in favour of looking at the actual members. --- services/presence/activity.py | 145 +++++++++++++++++++++++---- services/presence/presenceservice.py | 55 ++++++++-- 2 files changed, 176 insertions(+), 24 deletions(-) diff --git a/services/presence/activity.py b/services/presence/activity.py index 0743b2b8..2eb21f6c 100644 --- a/services/presence/activity.py +++ b/services/presence/activity.py @@ -22,7 +22,8 @@ from dbus.gobject_service import ExportedGObject from sugar import util import logging -from telepathy.interfaces import (CHANNEL_INTERFACE) +from telepathy.constants import CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES +from telepathy.interfaces import (CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP) _ACTIVITY_PATH = "/org/laptop/Sugar/Presence/Activities/" _ACTIVITY_INTERFACE = "org.laptop.Sugar.Presence.Activity" @@ -80,7 +81,7 @@ class Activity(ExportedGObject): _RESERVED_PROPNAMES = __gproperties__.keys() - def __init__(self, bus, object_id, tp, **kwargs): + def __init__(self, bus, object_id, ps, tp, **kwargs): """Initializes the activity and sets its properties to default values. :Parameters: @@ -88,6 +89,8 @@ class Activity(ExportedGObject): A connection to the D-Bus session bus `object_id` : int PS ID for this activity, used to construct the object-path + `ps` : presenceservice.PresenceService + The presence service `tp` : server plugin The server plugin object (stands for "telepathy plugin") :Keywords: @@ -112,16 +115,20 @@ class Activity(ExportedGObject): if not tp: raise ValueError("telepathy CM must be valid") + self._ps = ps self._object_id = object_id self._object_path = dbus.ObjectPath(_ACTIVITY_PATH + str(self._object_id)) - self._buddies = [] + self._buddies = set() + self._member_handles = set() self._joined = False # the telepathy client self._tp = tp + self._self_handle = None self._text_channel = None + self._text_channel_group_flags = 0 self._valid = False self._id = None @@ -376,8 +383,10 @@ class Activity(ExportedGObject): ret.append(buddy) return ret - def buddy_joined(self, buddy): - """Adds a buddy to this activity and sends a BuddyJoined signal + def buddy_apparently_joined(self, buddy): + """Adds a buddy to this activity and sends a BuddyJoined signal, + unless we can already see who's in the activity by being in it + ourselves. buddy -- Buddy object representing the buddy being added @@ -388,30 +397,54 @@ class Activity(ExportedGObject): This method is called by the PresenceService on the local machine. """ - if buddy not in self._buddies: - self._buddies.append(buddy) + if not self._joined: + self._add_buddies((buddy,)) + + def _add_buddies(self, buddies): + buddies = set(buddies) + + # disregard any who are already there + buddies -= self._buddies + + self._buddies |= buddies + + for buddy in buddies: buddy.add_activity(self) if self.props.valid: self.BuddyJoined(buddy.object_path()) - def buddy_left(self, buddy): - """Removes a buddy from this activity and sends a BuddyLeft signal. + def _remove_buddies(self, buddies): + buddies = set(buddies) + + # disregard any who are not already there + buddies &= self._buddies + + self._buddies -= buddies + + for buddy in buddies: + buddy.remove_activity(self) + if self.props.valid: + self.BuddyJoined(buddy.object_path()) + + if not self._buddies: + self.emit('disappeared') + + def buddy_apparently_left(self, buddy): + """Removes a buddy from this activity and sends a BuddyLeft signal, + unless we can already see who's in the activity by being in it + ourselves. buddy -- Buddy object representing the buddy being removed Removes a buddy from this activity if the buddy is in the buddy list. If this activity is "valid", a BuddyLeft signal is also sent. This method is called by the PresenceService on the local machine. - """ - if buddy in self._buddies: - self._buddies.remove(buddy) - buddy.remove_activity(self) - if self.props.valid: - self.BuddyLeft(buddy.object_path()) + if not self._joined: + self._remove_buddies((buddy,)) - if not self._buddies: - self.emit('disappeared') + def _text_channel_group_flags_changed_cb(self, flags): + self._text_channel_group_flags = flags def _handle_share_join(self, tp, text_channel): """Called when a join to a network activity was successful. @@ -426,7 +459,36 @@ class Activity(ExportedGObject): self._text_channel = text_channel self._text_channel[CHANNEL_INTERFACE].connect_to_signal('Closed', self._text_channel_closed_cb) - self._joined = True + if CHANNEL_INTERFACE_GROUP in self._text_channel: + group = self._text_channel[CHANNEL_INTERFACE_GROUP] + + # FIXME: make these method calls async? + + group.connect_to_signal('GroupFlagsChanged', + self._text_channel_group_flags_changed_cb) + self._text_channel_group_flags = group.GetGroupFlags() + + self._self_handle = group.GetSelfHandle() + + # 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 + members = set(group.GetMembers()) + added = members - self._member_handles + removed = self._member_handles - members + if added or removed: + self._text_channel_members_changed_cb('', added, removed, + (), (), 0, 0) + + # if we can see any member handles, we're probably able to see + # all members, so can stop caring about PEP announcements for this + # activity + self._joined = (self._self_handle in self._member_handles) + else: + self._joined = True + return True def _shared_cb(self, tp, activity_id, text_channel, exc, userdata): @@ -519,12 +581,59 @@ class Activity(ExportedGObject): if self._joined: self._text_channel[CHANNEL_INTERFACE].Close() + def _text_channel_members_changed_cb(self, message, added, removed, + local_pending, remote_pending, + actor, reason): + # Note: D-Bus calls this with list arguments, but after GetMembers() + # we call it with set and tuple arguments; we cope with any iterable. + + if (self._text_channel_group_flags & + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES): + map_chan = self._text_channel + else: + # we have global handles here + map_chan = None + + # disregard any who are already there + added = set(added) + added -= self._member_handles + self._member_handles |= added + + # for added people, we need a Buddy object + added_buddies = self._ps.map_handles_to_buddies(self._tp, + map_chan, + added) + self._add_buddies(added_buddies.itervalues()) + + # we treat all pending members as if they weren't there + removed = set(removed) + removed |= set(local_pending) + removed |= set(remote_pending) + # disregard any who aren't already there + removed &= self._member_handles + self._member_handles -= removed + + # for removed people, don't bother creating a Buddy just so we can + # say it left. If we don't already have a Buddy object for someone, + # then obviously they're not in self._buddies! + removed_buddies = self._ps.map_handles_to_buddies(self._tp, + map_chan, + removed, + create=False) + self._remove_buddies(removed_buddies.itervalues()) + + # if we were among those removed, we'll have to start believing + # the spoofable PEP-based activity tracking again. + if self._self_handle not in self._member_handles: + self._joined = False + def _text_channel_closed_cb(self): """Callback method called when the text channel is closed. This callback is set up in the _handle_share_join method. """ self._joined = False + self._self_handle = None self._text_channel = None def send_properties(self): diff --git a/services/presence/presenceservice.py b/services/presence/presenceservice.py index 84814f08..6f28bf5f 100644 --- a/services/presence/presenceservice.py +++ b/services/presence/presenceservice.py @@ -211,7 +211,8 @@ class PresenceService(ExportedGObject): def _new_activity(self, activity_id, tp): try: objid = self._get_next_object_id() - activity = Activity(self._session_bus, objid, tp, id=activity_id) + activity = Activity(self._session_bus, objid, self, tp, + id=activity_id) except Exception: # FIXME: catching bare Exception considered harmful _logger.debug("Invalid activity:", exc_info=1) @@ -259,7 +260,7 @@ class PresenceService(ExportedGObject): activity = self._new_activity(act, tp) if activity is not None: - activity.buddy_joined(buddy) + activity.buddy_apparently_joined(buddy) activities_left = old_activities - new_activities for act in activities_left: @@ -268,7 +269,7 @@ class PresenceService(ExportedGObject): if not activity: continue - activity.buddy_left(buddy) + activity.buddy_apparently_left(buddy) def _activity_invitation(self, tp, act_id): activity = self._activities.get(act_id) @@ -376,6 +377,48 @@ class PresenceService(ExportedGObject): "connection to %s:%s" % (handle, tp_conn_name, tp_conn_path)) + def map_handles_to_buddies(self, tp, tp_chan, handles, create=True): + """ + + :Parameters: + `tp` : Telepathy plugin + The server or link-local plugin + `tp_chan` : telepathy.client.Channel or None + If not None, the channel in which these handles are + channel-specific + `handles` : iterable over int or long + The handles to be mapped to Buddy objects + `create` : bool + If true (default), if a corresponding `Buddy` object is not + found, create one. + :Returns: + A dict mapping handles from `handles` to `Buddy` objects. + If `create` is true, the dict's keys will be exactly the + items of `handles` in some order. If `create` is false, + the dict will contain no entry for handles for which no + `Buddy` is already available. + :Raises LookupError: if `tp` is not a plugin attached to this PS. + """ + handle_to_buddy = self._handles_buddies[tp] + + ret = {} + missing = [] + for handle in handles: + buddy = handle_to_buddy.get(handle) + if buddy is None: + missing.append(handle) + else: + ret[handle] = buddy + + if missing and create: + handle_to_objid = tp.identify_contacts(tp_chan, missing) + for handle, objid in handle_to_objid.iteritems(): + buddy = self.get_buddy(objid) + ret[handle] = buddy + if tp_chan is None: + handle_to_buddy[handle] = buddy + return ret + @dbus.service.method(_PRESENCE_INTERFACE, in_signature='', out_signature="o") def GetOwner(self): @@ -405,9 +448,9 @@ class PresenceService(ExportedGObject): objid = self._get_next_object_id() # FIXME check which tp client we should use to share the activity color = self._owner.props.color - activity = Activity(self._session_bus, objid, self._server_plugin, - id=actid, type=atype, name=name, color=color, - local=True) + activity = Activity(self._session_bus, objid, self, + self._server_plugin, id=actid, type=atype, + name=name, color=color, local=True) activity.connect("validity-changed", self._activity_validity_changed_cb) self._activities[actid] = activity