sugar-toolkit-gtk3/services/presence/presenceservice.py
Simon McVittie ee6c1b4283 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.
2007-05-30 17:36:42 +01:00

499 lines
20 KiB
Python

# Copyright (C) 2007, Red Hat, Inc.
# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject
import dbus
import dbus.service
from dbus.gobject_service import ExportedGObject
from dbus.mainloop.glib import DBusGMainLoop
import logging
from telepathy.client import ManagerRegistry, Connection
from telepathy.interfaces import (CONN_MGR_INTERFACE, CONN_INTERFACE)
from telepathy.constants import (CONNECTION_STATUS_CONNECTING,
CONNECTION_STATUS_CONNECTED,
CONNECTION_STATUS_DISCONNECTED)
from server_plugin import ServerPlugin
from linklocal_plugin import LinkLocalPlugin
from sugar import util
from buddy import Buddy, ShellOwner
from activity import Activity
from psutils import pubkey_to_keyid
_PRESENCE_SERVICE = "org.laptop.Sugar.Presence"
_PRESENCE_INTERFACE = "org.laptop.Sugar.Presence"
_PRESENCE_PATH = "/org/laptop/Sugar/Presence"
_logger = logging.getLogger('s-p-s.presenceservice')
class NotFoundError(dbus.DBusException):
def __init__(self, msg):
dbus.DBusException.__init__(self, msg)
self._dbus_error_name = _PRESENCE_INTERFACE + '.NotFound'
class PresenceService(ExportedGObject):
__gtype_name__ = "PresenceService"
__gsignals__ = {
'connection-status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_BOOLEAN]))
}
def _create_owner(self):
# Overridden by TestPresenceService
return ShellOwner(self, self._session_bus)
def __init__(self):
self._next_object_id = 0
self._connected = False
self._buddies = {} # identifier -> Buddy
self._buddies_by_pubkey = {} # base64 public key -> Buddy
self._handles_buddies = {} # tp client -> (handle -> Buddy)
self._activities = {} # activity id -> Activity
self._session_bus = dbus.SessionBus()
self._session_bus.add_signal_receiver(self._connection_disconnected_cb,
signal_name="Disconnected",
dbus_interface="org.freedesktop.DBus")
# Create the Owner object
self._owner = self._create_owner()
key = self._owner.props.key
keyid = pubkey_to_keyid(key)
self._buddies['keyid/' + keyid] = self._owner
self._buddies_by_pubkey[key] = self._owner
self._registry = ManagerRegistry()
self._registry.LoadManagers()
# Set up the server connection
self._server_plugin = ServerPlugin(self._registry, self._owner)
self._handles_buddies[self._server_plugin] = {}
self._server_plugin.connect('status', self._server_status_cb)
self._server_plugin.connect('contact-online', self._contact_online)
self._server_plugin.connect('contact-offline', self._contact_offline)
self._server_plugin.connect('avatar-updated', self._avatar_updated)
self._server_plugin.connect('buddy-properties-changed',
self._buddy_properties_changed)
self._server_plugin.connect('buddy-activities-changed',
self._buddy_activities_changed)
self._server_plugin.connect('activity-invitation',
self._activity_invitation)
self._server_plugin.connect('private-invitation',
self._private_invitation)
self._server_plugin.connect('activity-properties-changed',
self._activity_properties_changed)
self._server_plugin.start()
# Set up the link local connection
self._ll_plugin = LinkLocalPlugin(self._registry, self._owner)
self._handles_buddies[self._ll_plugin] = {}
ExportedGObject.__init__(self, self._session_bus, _PRESENCE_PATH)
# for activation to work in a race-free way, we should really
# export the bus name only after we export our initial object;
# so this comes after the parent __init__
self._bus_name = dbus.service.BusName(_PRESENCE_SERVICE,
bus=self._session_bus)
def _connection_disconnected_cb(self, foo=None):
"""Log event when D-Bus kicks us off the bus for some reason"""
_logger.debug("Disconnected from session bus!!!")
def _server_status_cb(self, plugin, status, reason):
# FIXME: figure out connection status when we have a salut plugin too
old_status = self._connected
if status == CONNECTION_STATUS_CONNECTED:
self._connected = True
self._handles_buddies[plugin][plugin.self_handle] = self._owner
self._owner.add_telepathy_handle(plugin, plugin.self_handle)
else:
self._connected = False
if plugin.self_handle is not None:
self._handles_buddies.setdefault(plugin, {}).pop(
plugin.self_handle, None)
self._owner.remove_telepathy_handle(plugin, plugin.self_handle)
if self._connected != old_status:
self.emit('connection-status', self._connected)
def get_buddy(self, objid):
buddy = self._buddies.get(objid)
if buddy is None:
_logger.debug('Creating new buddy at .../%s', objid)
# we don't know yet this buddy
buddy = Buddy(self._session_bus, objid)
buddy.connect("validity-changed", self._buddy_validity_changed_cb)
buddy.connect("disappeared", self._buddy_disappeared_cb)
self._buddies[objid] = buddy
return buddy
def _contact_online(self, tp, objid, handle, props):
_logger.debug('Handle %u, .../%s is now online', handle, objid)
buddy = self.get_buddy(objid)
self._handles_buddies[tp][handle] = buddy
# store the handle of the buddy for this CM
buddy.add_telepathy_handle(tp, handle)
buddy.set_properties(props)
def _buddy_validity_changed_cb(self, buddy, valid):
if valid:
self.BuddyAppeared(buddy.object_path())
self._buddies_by_pubkey[buddy.props.key] = buddy
_logger.debug("New Buddy: %s (%s)", buddy.props.nick,
buddy.props.color)
else:
self.BuddyDisappeared(buddy.object_path())
self._buddies_by_pubkey.pop(buddy.props.key, None)
_logger.debug("Buddy left: %s (%s)", buddy.props.nick,
buddy.props.color)
def _buddy_disappeared_cb(self, buddy):
if buddy.props.valid:
self.BuddyDisappeared(buddy.object_path())
_logger.debug('Buddy left: %s (%s)', buddy.props.nick,
buddy.props.color)
self._buddies_by_pubkey.pop(buddy.props.key, None)
self._buddies.pop(buddy.props.objid, None)
def _contact_offline(self, tp, handle):
if not self._handles_buddies[tp].has_key(handle):
return
buddy = self._handles_buddies[tp].pop(handle)
# the handle of the buddy for this CM is not valid anymore
# (this might trigger _buddy_disappeared_cb if they are not visible
# via any CM)
buddy.remove_telepathy_handle(tp, handle)
def _get_next_object_id(self):
"""Increment and return the object ID counter."""
self._next_object_id = self._next_object_id + 1
return self._next_object_id
def _avatar_updated(self, tp, handle, avatar):
buddy = self._handles_buddies[tp].get(handle)
if buddy and not buddy.props.owner:
_logger.debug("Buddy %s icon updated" % buddy.props.nick)
buddy.props.icon = avatar
def _buddy_properties_changed(self, tp, handle, properties):
buddy = self._handles_buddies[tp].get(handle)
if buddy:
buddy.set_properties(properties)
_logger.debug("Buddy %s properties updated: %s", buddy.props.nick,
properties.keys())
def _new_activity(self, activity_id, tp):
try:
objid = self._get_next_object_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)
return None
activity.connect("validity-changed",
self._activity_validity_changed_cb)
activity.connect("disappeared", self._activity_disappeared_cb)
self._activities[activity_id] = activity
return activity
def _activity_disappeared_cb(self, activity):
_logger.debug("activity %s disappeared" % activity.props.id)
self.ActivityDisappeared(activity.object_path())
del self._activities[activity.props.id]
def _buddy_activities_changed(self, tp, contact_handle, activities):
acts = []
for act in activities:
acts.append(str(act))
_logger.debug("Handle %s activities changed: %s", contact_handle, acts)
buddies = self._handles_buddies[tp]
buddy = buddies.get(contact_handle)
if not buddy:
# We don't know this buddy
# FIXME: What should we do here?
# FIXME: Do we need to check if the buddy is valid or something?
_logger.debug("contact_activities_changed: buddy unknown")
return
old_activities = set()
for activity in buddy.get_joined_activities():
old_activities.add(activity.props.id)
new_activities = set(activities)
activities_joined = new_activities - old_activities
for act in activities_joined:
_logger.debug("Handle %s joined activity %s", contact_handle, act)
activity = self._activities.get(act)
if activity is None:
# new activity, can fail
activity = self._new_activity(act, tp)
if activity is not None:
activity.buddy_apparently_joined(buddy)
activities_left = old_activities - new_activities
for act in activities_left:
_logger.debug("Handle %s left activity %s", contact_handle, act)
activity = self._activities.get(act)
if not activity:
continue
activity.buddy_apparently_left(buddy)
def _activity_invitation(self, tp, act_id):
activity = self._activities.get(act_id)
if activity:
self.ActivityInvitation(activity.object_path())
def _private_invitation(self, tp, chan_path):
conn = tp.get_connection()
self.PrivateInvitation(str(conn.service_name), conn.object_path,
chan_path)
@dbus.service.signal(_PRESENCE_INTERFACE, signature="o")
def ActivityAppeared(self, activity):
pass
@dbus.service.signal(_PRESENCE_INTERFACE, signature="o")
def ActivityDisappeared(self, activity):
pass
@dbus.service.signal(_PRESENCE_INTERFACE, signature="o")
def BuddyAppeared(self, buddy):
pass
@dbus.service.signal(_PRESENCE_INTERFACE, signature="o")
def BuddyDisappeared(self, buddy):
pass
@dbus.service.signal(_PRESENCE_INTERFACE, signature="o")
def ActivityInvitation(self, activity):
pass
@dbus.service.signal(_PRESENCE_INTERFACE, signature="soo")
def PrivateInvitation(self, bus_name, connection, channel):
pass
@dbus.service.method(_PRESENCE_INTERFACE, in_signature='',
out_signature="ao")
def GetActivities(self):
ret = []
for act in self._activities.values():
if act.props.valid:
ret.append(act.object_path())
return ret
@dbus.service.method(_PRESENCE_INTERFACE, in_signature="s",
out_signature="o")
def GetActivityById(self, actid):
act = self._activities.get(actid, None)
if not act or not act.props.valid:
raise NotFoundError("The activity was not found.")
return act.object_path()
@dbus.service.method(_PRESENCE_INTERFACE, in_signature='',
out_signature="ao")
def GetBuddies(self):
ret = []
for buddy in self._buddies.values():
if buddy.props.valid:
ret.append(buddy.object_path())
return ret
@dbus.service.method(_PRESENCE_INTERFACE,
in_signature="ay", out_signature="o",
byte_arrays=True)
def GetBuddyByPublicKey(self, key):
buddy = self._buddies_by_pubkey.get(key)
if buddy is not None:
if buddy.props.valid:
return buddy.object_path()
raise NotFoundError("The buddy was not found.")
@dbus.service.method(_PRESENCE_INTERFACE, in_signature='sou',
out_signature='o')
def GetBuddyByTelepathyHandle(self, tp_conn_name, tp_conn_path, handle):
"""Get the buddy corresponding to a Telepathy handle.
:Parameters:
`tp_conn_name` : str
The well-known bus name of a Telepathy connection
`tp_conn_path` : dbus.ObjectPath
The object path of the Telepathy connection
`handle` : int or long
The handle of a Telepathy contact on that connection,
of type HANDLE_TYPE_CONTACT. This may not be a
channel-specific handle.
:Returns: the object path of a Buddy
:Raises NotFoundError: if the buddy is not found.
"""
for tp, handles in self._handles_buddies.iteritems():
conn = tp.get_connection()
if conn is None:
continue
if (conn.service_name == tp_conn_name
and conn.object_path == tp_conn_path):
buddy = handles.get(handle)
if buddy is not None and buddy.props.valid:
return buddy.object_path()
# either the handle is invalid, or we don't have a Buddy
# object for that buddy because we don't have all their
# details yet
raise NotFoundError("The buddy %u was not found on the "
"connection to %s:%s"
% (handle, tp_conn_name, tp_conn_path))
raise NotFoundError("The buddy %u was not found: we have no "
"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):
if not self._owner:
raise NotFoundError("The owner was not found.")
else:
return self._owner.object_path()
@dbus.service.method(_PRESENCE_INTERFACE, in_signature="sssa{sv}",
out_signature="o", async_callbacks=('async_cb', 'async_err_cb'))
def ShareActivity(self, actid, atype, name, properties, async_cb,
async_err_cb):
self._share_activity(actid, atype, name, properties,
(async_cb, async_err_cb))
@dbus.service.method(_PRESENCE_INTERFACE,
in_signature='', out_signature="so")
def GetPreferredConnection(self):
conn = self._server_plugin.get_connection()
return str(conn.service_name), conn.object_path
def cleanup(self):
for tp in self._handles_buddies:
tp.cleanup()
def _share_activity(self, actid, atype, name, properties, callbacks):
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,
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
activity._share(callbacks, self._owner)
# local activities are valid at creation by definition, but we can't
# connect to the activity's validity-changed signal until its already
# issued the signal, which happens in the activity's constructor
# for local activities.
self._activity_validity_changed_cb(activity, activity.props.valid)
def _activity_validity_changed_cb(self, activity, valid):
if valid:
self.ActivityAppeared(activity.object_path())
_logger.debug("New Activity: %s (%s)", activity.props.name,
activity.props.id)
else:
self.ActivityDisappeared(activity.object_path())
_logger.debug("Activity disappeared: %s (%s)", activity.props.name,
activity.props.id)
def _activity_properties_changed(self, tp, act_id, props):
activity = self._activities.get(act_id)
if activity:
activity.set_properties(props)
def main(test_num=0, randomize=False):
loop = gobject.MainLoop()
dbus_mainloop_wrapper = DBusGMainLoop(set_as_default=True)
if test_num > 0:
from pstest import TestPresenceService
ps = TestPresenceService(test_num, randomize)
else:
ps = PresenceService()
try:
loop.run()
except KeyboardInterrupt:
ps.cleanup()
_logger.debug('Ctrl+C pressed, exiting...')
if __name__ == "__main__":
main()