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.
This commit is contained in:
Simon McVittie 2007-05-30 17:36:42 +01:00
parent 87446bfb7f
commit ee6c1b4283
2 changed files with 176 additions and 24 deletions

View File

@ -22,7 +22,8 @@ from dbus.gobject_service import ExportedGObject
from sugar import util from sugar import util
import logging 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_PATH = "/org/laptop/Sugar/Presence/Activities/"
_ACTIVITY_INTERFACE = "org.laptop.Sugar.Presence.Activity" _ACTIVITY_INTERFACE = "org.laptop.Sugar.Presence.Activity"
@ -80,7 +81,7 @@ class Activity(ExportedGObject):
_RESERVED_PROPNAMES = __gproperties__.keys() _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. """Initializes the activity and sets its properties to default values.
:Parameters: :Parameters:
@ -88,6 +89,8 @@ class Activity(ExportedGObject):
A connection to the D-Bus session bus A connection to the D-Bus session bus
`object_id` : int `object_id` : int
PS ID for this activity, used to construct the object-path PS ID for this activity, used to construct the object-path
`ps` : presenceservice.PresenceService
The presence service
`tp` : server plugin `tp` : server plugin
The server plugin object (stands for "telepathy plugin") The server plugin object (stands for "telepathy plugin")
:Keywords: :Keywords:
@ -112,16 +115,20 @@ class Activity(ExportedGObject):
if not tp: if not tp:
raise ValueError("telepathy CM must be valid") raise ValueError("telepathy CM must be valid")
self._ps = ps
self._object_id = object_id self._object_id = object_id
self._object_path = dbus.ObjectPath(_ACTIVITY_PATH + self._object_path = dbus.ObjectPath(_ACTIVITY_PATH +
str(self._object_id)) str(self._object_id))
self._buddies = [] self._buddies = set()
self._member_handles = set()
self._joined = False self._joined = False
# the telepathy client # the telepathy client
self._tp = tp self._tp = tp
self._self_handle = None
self._text_channel = None self._text_channel = None
self._text_channel_group_flags = 0
self._valid = False self._valid = False
self._id = None self._id = None
@ -376,8 +383,10 @@ class Activity(ExportedGObject):
ret.append(buddy) ret.append(buddy)
return ret return ret
def buddy_joined(self, buddy): def buddy_apparently_joined(self, buddy):
"""Adds a buddy to this activity and sends a BuddyJoined signal """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 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. This method is called by the PresenceService on the local machine.
""" """
if buddy not in self._buddies: if not self._joined:
self._buddies.append(buddy) 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) buddy.add_activity(self)
if self.props.valid: if self.props.valid:
self.BuddyJoined(buddy.object_path()) self.BuddyJoined(buddy.object_path())
def buddy_left(self, buddy): def _remove_buddies(self, buddies):
"""Removes a buddy from this activity and sends a BuddyLeft signal. 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 buddy -- Buddy object representing the buddy being removed
Removes a buddy from this activity if the buddy is in the buddy list. 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. If this activity is "valid", a BuddyLeft signal is also sent.
This method is called by the PresenceService on the local machine. This method is called by the PresenceService on the local machine.
""" """
if buddy in self._buddies: if not self._joined:
self._buddies.remove(buddy) self._remove_buddies((buddy,))
buddy.remove_activity(self)
if self.props.valid:
self.BuddyLeft(buddy.object_path())
if not self._buddies: def _text_channel_group_flags_changed_cb(self, flags):
self.emit('disappeared') self._text_channel_group_flags = flags
def _handle_share_join(self, tp, text_channel): def _handle_share_join(self, tp, text_channel):
"""Called when a join to a network activity was successful. """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 = text_channel
self._text_channel[CHANNEL_INTERFACE].connect_to_signal('Closed', self._text_channel[CHANNEL_INTERFACE].connect_to_signal('Closed',
self._text_channel_closed_cb) self._text_channel_closed_cb)
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 self._joined = True
return True return True
def _shared_cb(self, tp, activity_id, text_channel, exc, userdata): def _shared_cb(self, tp, activity_id, text_channel, exc, userdata):
@ -519,12 +581,59 @@ class Activity(ExportedGObject):
if self._joined: if self._joined:
self._text_channel[CHANNEL_INTERFACE].Close() 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): def _text_channel_closed_cb(self):
"""Callback method called when the text channel is closed. """Callback method called when the text channel is closed.
This callback is set up in the _handle_share_join method. This callback is set up in the _handle_share_join method.
""" """
self._joined = False self._joined = False
self._self_handle = None
self._text_channel = None self._text_channel = None
def send_properties(self): def send_properties(self):

View File

@ -211,7 +211,8 @@ class PresenceService(ExportedGObject):
def _new_activity(self, activity_id, tp): def _new_activity(self, activity_id, tp):
try: try:
objid = self._get_next_object_id() 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: except Exception:
# FIXME: catching bare Exception considered harmful # FIXME: catching bare Exception considered harmful
_logger.debug("Invalid activity:", exc_info=1) _logger.debug("Invalid activity:", exc_info=1)
@ -259,7 +260,7 @@ class PresenceService(ExportedGObject):
activity = self._new_activity(act, tp) activity = self._new_activity(act, tp)
if activity is not None: if activity is not None:
activity.buddy_joined(buddy) activity.buddy_apparently_joined(buddy)
activities_left = old_activities - new_activities activities_left = old_activities - new_activities
for act in activities_left: for act in activities_left:
@ -268,7 +269,7 @@ class PresenceService(ExportedGObject):
if not activity: if not activity:
continue continue
activity.buddy_left(buddy) activity.buddy_apparently_left(buddy)
def _activity_invitation(self, tp, act_id): def _activity_invitation(self, tp, act_id):
activity = self._activities.get(act_id) activity = self._activities.get(act_id)
@ -376,6 +377,48 @@ class PresenceService(ExportedGObject):
"connection to %s:%s" % (handle, tp_conn_name, "connection to %s:%s" % (handle, tp_conn_name,
tp_conn_path)) 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, @dbus.service.method(_PRESENCE_INTERFACE,
in_signature='', out_signature="o") in_signature='', out_signature="o")
def GetOwner(self): def GetOwner(self):
@ -405,9 +448,9 @@ class PresenceService(ExportedGObject):
objid = self._get_next_object_id() objid = self._get_next_object_id()
# FIXME check which tp client we should use to share the activity # FIXME check which tp client we should use to share the activity
color = self._owner.props.color color = self._owner.props.color
activity = Activity(self._session_bus, objid, self._server_plugin, activity = Activity(self._session_bus, objid, self,
id=actid, type=atype, name=name, color=color, self._server_plugin, id=actid, type=atype,
local=True) name=name, color=color, local=True)
activity.connect("validity-changed", activity.connect("validity-changed",
self._activity_validity_changed_cb) self._activity_validity_changed_cb)
self._activities[actid] = activity self._activities[actid] = activity