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-05-30 16:36:42 (GMT)
committer Simon McVittie <simon.mcvittie@collabora.co.uk>2007-05-30 16:36:42 (GMT)
commitee6c1b428389268892889bc634650429c61b38b8 (patch)
tree5161b16d277365e3919aaf5b0cc546884051b110
parent87446bfb7fcabe52089c4118f6fe9e62a9f8dfb4 (diff)
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.
-rw-r--r--services/presence/activity.py145
-rw-r--r--services/presence/presenceservice.py55
2 files changed, 176 insertions, 24 deletions
diff --git a/services/presence/activity.py b/services/presence/activity.py
index 0743b2b..2eb21f6 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 84814f0..6f28bf5 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