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:
parent
87446bfb7f
commit
ee6c1b4283
@ -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)
|
||||||
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
|
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):
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user