
This makes sure we re-use an existing Buddy object if it's still referenced somewhere, rather than trying to make another and fighting over the object path.
513 lines
20 KiB
Python
513 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 logging
|
|
from weakref import WeakValueDictionary
|
|
|
|
import dbus
|
|
import dbus.service
|
|
import gobject
|
|
from dbus.gobject_service import ExportedGObject
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
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 sugar import util
|
|
|
|
from server_plugin import ServerPlugin
|
|
from linklocal_plugin import LinkLocalPlugin
|
|
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
|
|
|
|
# all Buddy objects
|
|
# identifier -> Buddy, GC'd when no more refs exist
|
|
self._buddies = WeakValueDictionary()
|
|
|
|
# the online buddies for whom we know the full public key
|
|
# base64 public key -> Buddy
|
|
self._buddies_by_pubkey = {}
|
|
|
|
# The online buddies (those who're available via some CM)
|
|
# TP plugin -> (handle -> Buddy)
|
|
self._handles_buddies = {}
|
|
|
|
# activity id -> Activity
|
|
self._activities = {}
|
|
|
|
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._buddy_validity_changed_cb(buddy, False)
|
|
|
|
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):
|
|
# in the presence of an out_signature, dbus-python will convert
|
|
# this set into an Array automatically (because it's iterable),
|
|
# so it's easy to use for uniquification (we want to avoid returning
|
|
# buddies who're visible on both Salut and Gabble twice)
|
|
|
|
# always include myself even if I have no handles
|
|
ret = set((self._owner,))
|
|
|
|
for handles_buddies in self._handles_buddies.itervalues():
|
|
for buddy in handles_buddies.itervalues():
|
|
if buddy.props.valid:
|
|
ret.add(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()
|